From 005d759e567b1f91c4d78dbc76e85b298e45bc0f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Thu, 22 Oct 2026 16:18:44 +0100 Subject: [PATCH] rc: a const read out of a field keeps a count of its own `const c = h.c` was folded into the read itself, holding no reference, so `h.c = d` freed the block `c` still read (and a `return` through `finally` read it after the field's overwrite). Under rc such a const now gets storage like a `let`, as a const read out of a local's slot already does (#454). Under own the folded read stays a borrow, rejected when used after the overwrite. Element reads are not covered yet: a destructuring past the end of an array reads whatever follows its data, and a count taken of that faults, as it does for a `let` today (00array_rest_copy). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 21 +++++- tslang/test/tester/CMakeLists.txt | 3 + .../test/tester/tests/00const_field_read.ts | 64 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00const_field_read.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index eb65db6a8..3c6be6682 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1701,6 +1701,18 @@ class MLIRGenImpl return varOp && varOp->hasAttr(OWNED_LOCAL_ATTR_NAME); } + // A value read out of a field, through any casts: what `h.c` is in `const c = h.c`. + static bool readsField(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + auto loadOp = value ? value.getDefiningOp() : mlir_ts::LoadOp(); + return loadOp && loadOp.getReference().getDefiningOp(); + } + // 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 +1839,13 @@ 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 or a call has no slot to lose it from. + // + // 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 + // (`const [a, b, c] = pair`) reads whatever follows its data, and a count taken of that + // faults, as it does for a `let` today. // // 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 @@ -1846,6 +1864,7 @@ class MLIRGenImpl || MLIRTypeHelper::isConstTupleHoldingArray(type) || ((isa(type) || MLIRTypeHelper::isSharedHandleType(type) || (readsLocalSlot(initial) && mth.ownsHeapMemory(location, type)) + || (compileOptions.isRefCounted() && readsField(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..9849b192d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -393,6 +393,7 @@ tslang_add_test(NAME test-compile-00-generator-temporaries COMMAND test-runner " 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-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-const-field-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.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") tslang_add_test(NAME test-compile-00-arrays7 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-compile-00-arrays8-tuple-spread COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -960,6 +961,7 @@ tslang_add_test(NAME test-jit-00-generator-temporaries COMMAND test-runner -jit 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-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-const-field-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.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") tslang_add_test(NAME test-jit-00-arrays7 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-jit-00-arrays8-tuple-spread COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -1917,6 +1919,7 @@ set(TSLANG_CORPUS 00array_length_valid.ts 00string_view_copy.ts 00c_string_copy.ts + 00const_field_read.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00const_field_read.ts b/tslang/test/tester/tests/00const_field_read.ts new file mode 100644 index 000000000..60e79af00 --- /dev/null +++ b/tslang/test/tester/tests/00const_field_read.ts @@ -0,0 +1,64 @@ +// A `const` read out of a field keeps what it read alive when the field is given a new value. +// Folded, the const was the read itself and held no reference of its own: under rc the overwrite +// freed the block it still read. Allocations of the same size reuse anything freed before each +// read. +class C { + constructor(public x: number) {} +} + +class H { + c: C = new C(1); + n: C | null = new C(2); +} + +class Junk { + a = -1; + b = -2; +} + +function churn() { + let keep: Junk[] = []; + for (let i = 0; i < 1000; i++) keep.push(new Junk()); + return keep.length; +} + +function field(i: number) { + const h = new H(); + h.c = new C(i); + const c = h.c; + h.c = new C(9); + churn(); + return c.x == i && h.c.x == 9; +} + +function nullableField(i: number) { + const h = new H(); + h.n = new C(i); + const c = h.n; + h.n = null; + churn(); + return c != null && c.x == i; +} + +function readInFinally(h: H) { + const c = h.c; + let x = 0; + try { + h.c = new C(9); + return x; + } finally { + churn(); + x = c.x; + assert(x == 1, "the read survives the field's overwrite through finally"); + } +} + +function main() { + for (let i = 0; i < 20; i++) { + assert(field(i), "a field read"); + assert(nullableField(i), "a nullable field read"); + } + + readInFinally(new H()); + print("done."); +}