From 8dd0b47eb68394102cb905a021bb2f629a1df1d7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 05:13:10 +0100 Subject: [PATCH] rc: a const read through an interface field or a handle's value holds a count `const v = ix.v; ix.v = mv();` and `const v = GS.value; GS.value = mv();` freed what `v` still read: the const was folded into the read, which holds no count. #635 gave a const read out of a class field storage under rc; a read through an interface (ts.InterfaceSymbolRef) or out of a Shared handle (ts.SharedValueRef) is a field read as well, and now gets the same. Under own the folded read stays a borrow, rejected when the place is overwritten. Fuzzer (#646) R2 and R7. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 9 +- tslang/test/tester/CMakeLists.txt | 3 + .../tests/00const_interface_field_read.ts | 83 +++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tslang/test/tester/tests/00const_interface_field_read.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index c03a4244a..55edc19f4 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1725,7 +1725,8 @@ class MLIRGenImpl return readsLocalSlot(viewedValue(value)); } - // A value read out of a field, through any casts: what `h.c` is in `const c = h.c`. + // A value read out of a field, through any casts: what `h.c` is in `const c = h.c`, seen + // through an interface too (`const c = ix.c`), and what a handle holds (`const v = s.value`). static bool readsField(mlir::Value value) { while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) @@ -1734,7 +1735,8 @@ class MLIRGenImpl } auto loadOp = value ? value.getDefiningOp() : mlir_ts::LoadOp(); - return loadOp && loadOp.getReference().getDefiningOp(); + return loadOp && mlir::isa_and_nonnull(loadOp.getReference().getDefiningOp()); } // A value read out of a global variable, through any casts: what `G` is in `const n = G`. @@ -1898,7 +1900,8 @@ class MLIRGenImpl // 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. // - // Under rc, so does a const read out of a field (`const c = h.c`), an element (`const c = + // Under rc, so does a const read out of a field (`const c = h.c`, also through an interface, + // `const c = ix.c`, and a handle's value, `const c = s.value`), an element (`const c = // a[0]`, `const [c] = a`) or a global (`const c = G`): folded, `h.c = d`, `a[0] = d` or // `G = d` - here or in a call - freed what `c` still read. So does an interface or a bound // method made over such a read (`const i: I = h.v`, `const m = a[0].get`), which holds the diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9661bd9c4..af30a86d5 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -405,6 +405,7 @@ tslang_add_test(NAME test-compile-00-const-element-read COMMAND test-runner "${P tslang_add_test(NAME test-compile-00-const-global-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_global_read.ts") tslang_add_test(NAME test-compile-00-global-argument-reassigned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_reassigned.ts") tslang_add_test(NAME test-compile-00-global-argument-view COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_view.ts") +tslang_add_test(NAME test-compile-00-const-interface-field-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_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") @@ -984,6 +985,7 @@ tslang_add_test(NAME test-jit-00-const-element-read COMMAND test-runner -jit "${ tslang_add_test(NAME test-jit-00-const-global-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_global_read.ts") tslang_add_test(NAME test-jit-00-global-argument-reassigned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_reassigned.ts") tslang_add_test(NAME test-jit-00-global-argument-view COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_view.ts") +tslang_add_test(NAME test-jit-00-const-interface-field-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_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") @@ -1953,6 +1955,7 @@ set(TSLANG_CORPUS 00const_global_read.ts 00global_argument_reassigned.ts 00global_argument_view.ts + 00const_interface_field_read.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00const_interface_field_read.ts b/tslang/test/tester/tests/00const_interface_field_read.ts new file mode 100644 index 000000000..bda1eee0d --- /dev/null +++ b/tslang/test/tester/tests/00const_interface_field_read.ts @@ -0,0 +1,83 @@ +// A `const` read out of a field through an interface (`const v = ix.v`), or out of what a Shared +// handle holds (`const v = s.value`), keeps what it read alive when that 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 (00const_field_read.ts is the class field case). +// Allocations of the same size reuse anything freed before each read. +class V { + tag = ""; + constructor(public x: number) { + this.tag = "v" + x; + } +} + +class N { + constructor(public v: V) {} +} + +interface IX { + v: V; +} + +function churn() { + let keep: V[] = []; + for (let i = 0; i < 1000; i++) keep.push(new V(999)); + return keep.length; +} + +let GS = new Shared(new V(1)); + +function interfaceOfLiteral(i: number) { + const ix: IX = { v: new V(0) }; + ix.v = new V(i); + const v = ix.v; + ix.v = new V(9); + churn(); + assert(v.x == i && v.tag == "v" + i, "a const read through an interface over an object literal"); +} + +function interfaceOfClass(i: number) { + const ix: IX = new N(new V(0)); + ix.v = new V(i); + const v = ix.v; + ix.v = new V(9); + churn(); + assert(v.x == i && v.tag == "v" + i, "a const read through an interface over a class"); +} + +function interfaceInLoop() { + let ix: IX = { v: new V(1) }; + for (let i = 0; i < 3; i++) { + const v = ix.v; + ix.v = new V(i + 10); + churn(); + assert(v.x == (i == 0 ? 1 : i + 9), "a const read through an interface, in a loop"); + } +} + +function sharedValue(i: number) { + GS.value = new V(i); + const v = GS.value; + GS.value = new V(9); + churn(); + assert(v.x == i && v.tag == "v" + i, "a const read of a handle's value"); +} + +function sharedHandleReplaced(i: number) { + GS = new Shared(new V(i)); + const v = GS.value; + GS = new Shared(new V(9)); + churn(); + assert(v.x == i && v.tag == "v" + i, "a const read of a handle's value, the handle replaced"); +} + +function main() { + for (let i = 2; i < 5; i++) { + interfaceOfLiteral(i); + interfaceOfClass(i); + sharedValue(i); + sharedHandleReplaced(i); + } + + interfaceInLoop(); + print("done."); +}