From 49ad6c3ec9899a71e98607301a5715e9cc847763 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Thu, 22 Oct 2026 16:32:18 +0100 Subject: [PATCH] A const interface or bound method made over a local holds what it shows `const i: I = v` and `const m = v.get` were folded: a view of a read of `v`'s slot with no reference of its own, so `v = mkv(2)` freed the object `i` and `m` still used (rc and own). Like a const read straight out of a local's slot (#454), such a const now gets storage like a `let`. An interface is then counted under rc and owned under own. A bound method is still not counted under rc (its `this` carries a null tag), so `const m = v.get` stays broken there, as `let m = v.get` is; under own it is now rejected, as the `let` is. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 24 +++++++++- tslang/test/tester/CMakeLists.txt | 8 ++++ .../own_err_const_bound_method_reassigned.ts | 23 ++++++++++ .../tests/00const_interface_of_local.ts | 44 +++++++++++++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/own/own_err_const_bound_method_reassigned.ts create mode 100644 tslang/test/tester/tests/00const_interface_of_local.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index eb65db6a8..9ac9022a8 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1701,6 +1701,24 @@ class MLIRGenImpl return varOp && varOp->hasAttr(OWNED_LOCAL_ATTR_NAME); } + // An interface or a bound method made over a value read out of such a local: `v` seen + // through `I` in `const i: I = v`, or `this` of `const m = v.get`. + static bool viewsLocalSlot(mlir::Value value) + { + auto *definingOp = value ? value.getDefiningOp() : nullptr; + if (auto newInterfaceOp = dyn_cast_or_null(definingOp)) + { + return readsLocalSlot(newInterfaceOp.getThisVal()); + } + + if (isa_and_nonnull(definingOp)) + { + return readsLocalSlot(definingOp->getOperand(0)); + } + + return false; + } + // A record read out whole from the temporary slot an object literal is built in (one that // owns nothing of its own), with a field that owns a block and that only the literal's // receiver counts - a `let` does, with RetainSlot - so a `const` of it needs storage like a @@ -1827,7 +1845,9 @@ class MLIRGenImpl // So does a const of anything that owns a block, read out of a local variable's slot // (`let a = new C(); const b = a;`, a parameter too): folded, it would be a read of // that slot, holding no reference of its own, and `a = d` would free what `b` still - // reads (#454). A const of a const, a call or a field has no slot to lose it from. + // reads (#454). A const of a const, a call or a field has no slot to lose it from. So + // does an interface or a bound method made over such a read (`const i: I = a`, + // `const m = a.get`): it holds the block as `b` would. // // And a const record whose fields own blocks (`const o = { s: p.child }`): the literal // hands its fields over unretained - its receiver is the one that retains them, as a @@ -1845,7 +1865,7 @@ class MLIRGenImpl needsIdentityStorage = mth.hasBoundMethodField(type) || isa(type) || MLIRTypeHelper::isConstTupleHoldingArray(type) || ((isa(type) || MLIRTypeHelper::isSharedHandleType(type) - || (readsLocalSlot(initial) && mth.ownsHeapMemory(location, type)) + || ((readsLocalSlot(initial) || viewsLocalSlot(initial)) && mth.ownsHeapMemory(location, type)) || (readsLiteralSlot(mth, location, initial) && isa(type) && mth.ownsHeapMemory(location, type)) || (initial && initial.getDefiningOp() && isa(type) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 4bc6a05f7..2c867c45b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -391,6 +391,7 @@ tslang_add_test(NAME test-compile-00-narrowed-assign-other-member COMMAND test-r tslang_add_test(NAME test-compile-00-const-record-owned-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-compile-00-generator-temporaries COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_temporaries.ts") tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") +tslang_add_test(NAME test-compile-00-const-interface-of-local COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts") tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") tslang_add_test(NAME test-compile-00-c-string-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts") tslang_add_test(NAME test-compile-00-array-data-string-alias COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") @@ -958,6 +959,7 @@ tslang_add_test(NAME test-jit-00-narrowed-assign-other-member COMMAND test-runne tslang_add_test(NAME test-jit-00-const-record-owned-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-jit-00-generator-temporaries COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_temporaries.ts") tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") +tslang_add_test(NAME test-jit-00-const-interface-of-local COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts") tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") tslang_add_test(NAME test-jit-00-c-string-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts") tslang_add_test(NAME test-jit-00-array-data-string-alias COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") @@ -1915,6 +1917,7 @@ set(TSLANG_CORPUS 00const_record_owned_fields.ts 00generator_temporaries.ts 00array_length_valid.ts + 00const_interface_of_local.ts 00string_view_copy.ts 00c_string_copy.ts 00array_data_string_alias.ts @@ -3552,6 +3555,10 @@ add_test(NAME test-rc-generator-temporaries tslang_add_test(NAME test-jit-own-00-generator-temporaries COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_temporaries.ts") tslang_add_test(NAME test-compile-own-00-generator-temporaries COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_temporaries.ts") +# A const interface made over a local keeps the object when the local is assigned again, under own too. +tslang_add_test(NAME test-jit-own-00-const-interface-of-local COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts") +tslang_add_test(NAME test-compile-own-00-const-interface-of-local COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts") + # Shared (spec 23): counted handles. These are not in the own_test list, whose # test-own-no-counting check forbids __tslang_inc_ref: a handle is counted under own by design. # own_shared_user_named: a program's own class named Shared wins over the built-in (spec 23.1). @@ -3627,6 +3634,7 @@ set(own_error_cases "own_err_move_some_paths_field|is moved here on some paths only" "own_err_borrow_and_move|is moved here while .* borrows it" "own_err_borrow_bound_method|borrows .* but is used here after .* is released or overwritten" + "own_err_const_bound_method_reassigned|takes a second reference" "own_err_borrow_catch_local|borrows .* but is used here after .* is released or overwritten" "own_err_borrow_finally_local|borrows .* but is used here after .* is released or overwritten" "own_err_borrow_derived_ref|borrows .* but is used here after .* is released or overwritten" diff --git a/tslang/test/tester/own/own_err_const_bound_method_reassigned.ts b/tslang/test/tester/own/own_err_const_bound_method_reassigned.ts new file mode 100644 index 000000000..cf11d7631 --- /dev/null +++ b/tslang/test/tester/own/own_err_const_bound_method_reassigned.ts @@ -0,0 +1,23 @@ +// -mm=own rejects: `m` is `v.get` bound to the object `v` holds, and `v = mkv(2)` frees that +// object before `m()` runs. The method keeps no count of its own. +class W { + constructor(public x: number) {} +} + +class V { + constructor(public w: W) {} + get() { + return this.w.x; + } +} + +function mkv(n: number) { + return new V(new W(n)); +} + +function main() { + let v = mkv(1); + const m = v.get; + v = mkv(2); + print(m() + v.w.x); +} diff --git a/tslang/test/tester/tests/00const_interface_of_local.ts b/tslang/test/tester/tests/00const_interface_of_local.ts new file mode 100644 index 000000000..09e52148f --- /dev/null +++ b/tslang/test/tester/tests/00const_interface_of_local.ts @@ -0,0 +1,44 @@ +// A `const` interface made over a local (`const i: I = v`) keeps the object alive when the local +// is given a new value. Folded, the interface was a view of the read and held no reference of its +// own: under rc and own the assignment freed the object it still showed. Allocations of the same +// size reuse anything freed before each read. +class W { + constructor(public x: number) {} +} + +class V { + items: number[] = [1, 2, 3]; + constructor(public w: W) {} +} + +interface I { + w: W; +} + +function mkv(n: number) { + return new V(new W(n)); +} + +function churn(): number { + let s = 0; + const a: V[] = []; + for (let i = 0; i < 64; i++) a.push(mkv(1000 + i)); + for (const e of a) s += e.w.x; + return s; +} + +function view(n: number) { + let v = mkv(n); + const i: I = v; + v = mkv(n + 1); + churn(); + return i.w.x == n && v.w.x == n + 1; +} + +function main() { + for (let n = 0; n < 20; n++) { + assert(view(n), "an interface over a local outlives the local's assignment"); + } + + print("done."); +}