Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2328,6 +2328,10 @@ class MLIRGenImpl
{
builder.create<mlir_ts::RetainOp>(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<mlir_ts::ReleaseSlotOp>(location, variableDeclarationInfo.storage);
}

auto storeOp = builder.create<mlir_ts::StoreOp>(location, variableDeclarationInfo.initial, variableDeclarationInfo.storage);
Expand Down
11 changes: 10 additions & 1 deletion tslang/lib/TypeScript/OwnershipInferencePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 12 additions & 1 deletion tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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=$<TARGET_FILE:tslang>
"-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=$<TARGET_FILE:tslang>
Expand Down
28 changes: 28 additions & 0 deletions tslang/test/tester/generator-loop-local-release.cmake
Original file line number Diff line number Diff line change
@@ -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<!ts[.]array<!ts[.]string>>[)]")
message(FATAL_ERROR "`${generator}` does not release its loop local's old value:\n${body}")
endif()
endforeach()
90 changes: 90 additions & 0 deletions tslang/test/tester/own/own_generator_loop_local.ts
Original file line number Diff line number Diff line change
@@ -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.");
}
Loading