diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 8d3ef55c0..ee0cea73b 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -2328,6 +2328,10 @@ class MLIRGenImpl { builder.create(location, variableDeclarationInfo.initial); } + + // A declaration in a loop runs again on the next turn, into the same field: what + // the last turn stored there is given back first. The field starts out null. + builder.create(location, variableDeclarationInfo.storage); } auto storeOp = builder.create(location, variableDeclarationInfo.initial, variableDeclarationInfo.storage); diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 4a4d69b87..02960878e 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -2354,7 +2354,16 @@ class OwnershipInferencePass continue; } - // A result that borrows what it was given borrows this too, and is walked as it. + // A result that borrows what it was given borrows this too, and is walked as it. A + // call known to make its result (`__own_fresh_result`: through a field that only + // ever holds one function) hands back a block of the caller's own - the `{value, + // done}` record of `it.next()` points nowhere into `it`, and what it does after + // its next call cannot be a use of the method read out of `it`. + if (isCall(user) && user->hasAttr(OWN_FRESH_RESULT_ATTR_NAME)) + { + continue; + } + for (auto result : user->getResults()) { if (mayPointInto(result) && seen.insert(result).second) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 6960525fe..3e981b3e0 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3395,7 +3395,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_field_let_borrow own_element_let_borrow own_borrow_merge_after_call own_call_no_drops own_getter_borrow own_param_kept own_any_box own_string_length_borrow - own_closure_borrow own_closure_escape own_generator own_interface_view + own_closure_borrow own_closure_escape own_generator own_generator_loop_local own_interface_view own_async own_record_return own_generator_borrow own_global_script own_global_init own_constant_data own_record_local own_nullable_view own_declared_constant own_spread_build own_interface_field_call @@ -3451,6 +3451,17 @@ add_test(NAME test-own-string-copy-in-store-block "-DFILE=${PROJECT_SOURCE_DIR}/test/tester/own/own_string_copy_branch.ts" -P "${CMAKE_CURRENT_SOURCE_DIR}/own-string-copy-block.cmake") +# A local declared in a generator's loop gives back what the last turn stored in its field. The +# program means the same under every model; no run can see the leak, so the IR is read. +foreach(loop_local_mm rc none gc) + tslang_add_test(NAME test-jit-${loop_local_mm}-own_generator_loop_local COMMAND test-runner -jit -mm=${loop_local_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_generator_loop_local.ts") + tslang_add_test(NAME test-compile-${loop_local_mm}-own_generator_loop_local COMMAND test-runner -mm=${loop_local_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_generator_loop_local.ts") +endforeach() +add_test(NAME test-rc-generator-loop-local-release + COMMAND ${CMAKE_COMMAND} -DTSLANG=$ + "-DFILE=${PROJECT_SOURCE_DIR}/test/tester/own/own_generator_loop_local.ts" + -P "${CMAKE_CURRENT_SOURCE_DIR}/generator-loop-local-release.cmake") + # A `const` record literal's slot is released under rc (#485); no run can see the leak, so the IR is read. add_test(NAME test-rc-const-record-release COMMAND ${CMAKE_COMMAND} -DTSLANG=$ diff --git a/tslang/test/tester/generator-loop-local-release.cmake b/tslang/test/tester/generator-loop-local-release.cmake new file mode 100644 index 000000000..82809cd49 --- /dev/null +++ b/tslang/test/tester/generator-loop-local-release.cmake @@ -0,0 +1,28 @@ +# A local declared in a generator's loop is a field of the state object, stored again on every turn: +# under rc the generator's `next` releases what the field held before the store. Without it every +# turn but the last leaked, and no run shows that, so the IR is read: each generator in +# own_generator_loop_local.ts has a `ts.ReleaseSlot` of its `string[]` field. +cmake_minimum_required(VERSION 3.17.3) +execute_process(COMMAND "${TSLANG}" --emit=mlir-affine --no-default-lib -mm=rc "${FILE}" + OUTPUT_VARIABLE out ERROR_VARIABLE err RESULT_VARIABLE result) +# the IR is printed to stderr +string(APPEND out "${err}") +if(NOT result EQUAL 0) + message(FATAL_ERROR "tslang failed (${result}):\n${out}") +endif() +foreach(generator rows untilBreak pairs) + string(FIND "${out}" "ts.Func @\".f_${generator}..f_${generator}..obj" begin) + if(begin EQUAL -1) + message(FATAL_ERROR "no `next` of generator `${generator}` in:\n${out}") + endif() + string(SUBSTRING "${out}" ${begin} -1 body) + string(SUBSTRING "${body}" 1 -1 rest) + string(FIND "${rest}" "ts.Func @" end) + if(NOT end EQUAL -1) + math(EXPR end "${end} + 1") + string(SUBSTRING "${body}" 0 ${end} body) + endif() + if(NOT body MATCHES "\"ts[.]ReleaseSlot\"[(]%[0-9]+[)] : [(]!ts[.]ref>[)]") + message(FATAL_ERROR "`${generator}` does not release its loop local's old value:\n${body}") + endif() +endforeach() diff --git a/tslang/test/tester/own/own_generator_loop_local.ts b/tslang/test/tester/own/own_generator_loop_local.ts new file mode 100644 index 000000000..9bef2c8e1 --- /dev/null +++ b/tslang/test/tester/own/own_generator_loop_local.ts @@ -0,0 +1,90 @@ +// A local declared in a generator's loop is a field of the state object, and its declaration runs +// again on every turn, into the same field. What the last turn stored there is given back first; +// before, nothing gave it back, and 500000 runs of `untilBreak` grew by 150 MB under rc and own. +// The leak itself shows only in measure.ps1 (and generator-loop-local-release.cmake reads the IR); +// this checks that the release frees the old value, not the new one: each local is read after a +// churn(), which reuses freed blocks, so a value freed too early reads garbage. +// +// Under own the release makes `next()` a call that may drop, and a caller holds what it borrowed +// out of one record across the next call: a for...of over a generator, and two records by hand. +// +// Strings are made in another function: a temporary made in a generator's own body is never +// released (spec 18.5), which would hide the leak this covers. +function label(i: number) { + return "s" + i; +} + +function churn() { + const junk: string[][] = []; + for (let i = 0; i < 32; i++) { + junk.push([label(i), label(i + 1)]); + } +} + +// a `const` in a `for`, read after a churn() on its own turn +function* rows(n: number) { + for (let i = 0; i < n; i++) { + const row: string[] = [label(i), label(i + 1)]; + churn(); + yield row.length + row[0].length; + } +} + +// a `const` in a `while (true)`, the last one left by `break`: the state's release gives it back +function* untilBreak() { + let i = 0; + while (true) { + const result: string[] = [label(i++), "x"]; + if (i > 3) { + break; + } + + yield result.length; + } +} + +// yields a fresh array each turn while a local is given back in the same `next()` +function* pairs() { + let i = 0; + while (i < 3) { + const keep: string[] = [label(i), "k"]; + i++; + churn(); + yield [keep.length + keep[0].length + i, 7]; + } +} + +function main() { + let t = 0; + for (let n = 0; n < 2000; n++) { + for (const v of rows(3)) { + t += v; + } + + for (const v of untilBreak()) { + churn(); + t += v; + } + + // the loop variable borrows the record's value across the next `next()` + for (const g of pairs()) { + churn(); + t += g[0] + g.length; + } + + // by hand: the first record is read after the second `next()` + const ps = pairs(); + const a = ps.next(); + const b = ps.next(); + churn(); + t += a.value[0] + b.value[0]; + + // given up after the first value: the state still gives back its local + const half = rows(3); + t += half.next().value; + } + + // rows 4 + 4 + 4, untilBreak 2 + 2 + 2, pairs (5 + 2) + (6 + 2) + (7 + 2), by hand 5 + 6, half 4 + assert(t == 2000 * (12 + 6 + 24 + 11 + 4), "t"); + print("done."); +}