From 2661129051ea45ae705968074afaf92fd31e04d9 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Thu, 22 Oct 2026 18:59:16 +0100 Subject: [PATCH] rc: a const interface or bound method made over a field read holds a count `const m = h.v.get` and `const i: I = h.v` were folded into the view itself, holding no reference of their own, so `h.v = d` freed the object `m()` and `i.get()` then ran on (a crash under rc; `let m = h.v.get` was fine). #637 gave such a const storage when its object is read out of a local's slot, and #640 made a kept bound method count its object; a view over a field read was left out. Under rc it now gets storage like a `let`, as a const read out of a field already does (#635). Under own the folded read stays a borrow. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 27 +++++--- tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00const_view_of_field.ts | 63 +++++++++++++++++++ 3 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 tslang/test/tester/tests/00const_view_of_field.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index b36188534..d6e7e6475 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1701,22 +1701,28 @@ 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) + // The value an interface or a bound method is made over: `v` seen through `I` in + // `const i: I = v`, or `this` of `const m = v.get`. + static mlir::Value viewedValue(mlir::Value value) { auto *definingOp = value ? value.getDefiningOp() : nullptr; if (auto newInterfaceOp = dyn_cast_or_null(definingOp)) { - return readsLocalSlot(newInterfaceOp.getThisVal()); + return newInterfaceOp.getThisVal(); } if (isa_and_nonnull(definingOp)) { - return readsLocalSlot(definingOp->getOperand(0)); + return definingOp->getOperand(0); } - return false; + return mlir::Value(); + } + + // An interface or a bound method made over a value read out of such a local. + static bool viewsLocalSlot(mlir::Value value) + { + return readsLocalSlot(viewedValue(value)); } // A value read out of a field, through any casts: what `h.c` is in `const c = h.c`. @@ -1862,8 +1868,10 @@ class MLIRGenImpl // it holds the block as `b` would. // // Under rc, so does a const read out of a field (`const c = h.c`): folded, `h.c = d` freed - // what `c` still read. Under own the folded read is a borrow, and a use after the overwrite - // is an error there. Not yet an element: a destructuring past the end of an array + // what `c` still read. So does an interface or a bound method made over such a read + // (`const i: I = h.v`, `const m = h.v.get`), which holds the block as `c` would. Under + // own the folded read is a borrow, and a use after the overwrite is an error there. Not + // yet an element: a destructuring past the end of an array // (`const [a, b, c] = pair`) reads whatever follows its data, and a count taken of that // faults, as it does for a `let` today. // @@ -1884,7 +1892,8 @@ class MLIRGenImpl || MLIRTypeHelper::isConstTupleHoldingArray(type) || ((isa(type) || MLIRTypeHelper::isSharedHandleType(type) || ((readsLocalSlot(initial) || viewsLocalSlot(initial)) && mth.ownsHeapMemory(location, type)) - || (compileOptions.isRefCounted() && readsField(initial) && mth.ownsHeapMemory(location, type)) + || (compileOptions.isRefCounted() && (readsField(initial) || readsField(viewedValue(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 bcdd3c37c..f5789cb85 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -390,6 +390,7 @@ tslang_add_test(NAME test-compile-00-parse-number-range COMMAND test-runner "${P tslang_add_test(NAME test-compile-00-global-argument-held COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_held.ts") tslang_add_test(NAME test-compile-00-narrowed-assign-other-member COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-compile-00-bound-method-count COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00bound_method_count.ts") +tslang_add_test(NAME test-compile-00-const-view-of-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_view_of_field.ts") 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") @@ -962,6 +963,7 @@ tslang_add_test(NAME test-jit-00-parse-number-range COMMAND test-runner -jit "${ tslang_add_test(NAME test-jit-00-global-argument-held COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_held.ts") tslang_add_test(NAME test-jit-00-narrowed-assign-other-member COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-jit-00-bound-method-count COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00bound_method_count.ts") +tslang_add_test(NAME test-jit-00-const-view-of-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_view_of_field.ts") 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") @@ -1924,6 +1926,7 @@ set(TSLANG_CORPUS 00global_argument_held.ts 00narrowed_assign_other_member.ts 00bound_method_count.ts + 00const_view_of_field.ts 00const_record_owned_fields.ts 00generator_temporaries.ts 00array_length_valid.ts diff --git a/tslang/test/tester/tests/00const_view_of_field.ts b/tslang/test/tester/tests/00const_view_of_field.ts new file mode 100644 index 000000000..67f6ea1a6 --- /dev/null +++ b/tslang/test/tester/tests/00const_view_of_field.ts @@ -0,0 +1,63 @@ +// A `const` interface or bound method made over a field read keeps the object alive when the field +// is given a new value, as a `let` does. Folded, the const was the view itself and held no +// reference of its own: under rc `h.v = mkv(..)` freed the object `m()` and `i.get()` then ran on. +// Each call follows allocations of the same size, so a freed object is reused. +interface I { + get(): number; +} + +class W { + constructor(public x: number) {} +} + +class V { + items: number[] = [1, 2, 3]; + constructor(public w: W) {} + + get() { + return this.w.x; + } +} + +class H { + v: V = mkv(0); +} + +function mkv(n: number) { + return new V(new W(n)); +} + +function churn(): number { + let s: number = 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 boundMethod(n: number) { + const h = new H(); + h.v = mkv(n); + const m = h.v.get; + h.v = mkv(n + 1); + churn(); + return m() == n && h.v.w.x == n + 1; +} + +function interfaceView(n: number) { + const h = new H(); + h.v = mkv(n); + const i: I = h.v; + h.v = mkv(n + 1); + churn(); + return i.get() == n && h.v.w.x == n + 1; +} + +function main() { + for (let n = 0; n < 20; n++) { + assert(boundMethod(n), "a const bound method over a field"); + assert(interfaceView(n), "a const interface over a field"); + } + + print("done."); +}