From 8843580d48e6bb7666ef092b39b04089ebc1f144 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Thu, 22 Oct 2026 18:59:23 +0100 Subject: [PATCH] rc: a const read out of an element keeps a count of its own `const c = arr[0]` (and `const [c] = arr`) was folded into the read itself, holding no reference, so `arr[0] = d` freed the block `c` still read. Under rc such a const now gets storage like a `let`, as a const read out of a field already does (#635). That needed an out-of-bounds destructuring fixed first: `const [a, b, c] = pair` read whatever followed the array's data. Under rc a count taken of it faulted (for a `let` too), and under gc/none it was undefined behaviour that LLVM used - `print(c == null)` after it made main print nothing at all. An element past the end of an array is now the default of its type: null for a reference (TS has undefined there), 0 for a number. A runtime array checks its length; a const array literal's size is known, so no read is emitted. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 30 ++++++-- tslang/lib/TypeScript/MLIRGenVariables.cpp | 36 +++++++-- tslang/test/tester/CMakeLists.txt | 3 + .../test/tester/tests/00const_element_read.ts | 75 +++++++++++++++++++ 4 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 tslang/test/tester/tests/00const_element_read.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index b36188534..b64143faf 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1731,6 +1731,25 @@ class MLIRGenImpl return loadOp && loadOp.getReference().getDefiningOp(); } + // A value read out of an element, through any casts: what `a[0]` is in `const c = a[0]`, and + // what `b` is in `const [b] = a`, read there only when `a` is long enough (the `then` of an if). + static bool readsElement(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + if (auto ifOp = value ? value.getDefiningOp() : mlir_ts::IfOp()) + { + auto resultOp = dyn_cast(ifOp.getThenRegion().front().getTerminator()); + return resultOp && readsElement(resultOp.getOperand(mlir::cast(value).getResultNumber())); + } + + 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 @@ -1861,11 +1880,9 @@ 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`): 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. + // Under rc, so does a const read out of a field (`const c = h.c`) or an element (`const c = + // a[0]`, `const [c] = a`): folded, `h.c = d` or `a[0] = d` freed what `c` still read. Under + // own the folded read is a borrow, and a use after the overwrite is an error there. // // 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 @@ -1884,7 +1901,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) || readsElement(initial)) + && mth.ownsHeapMemory(location, type)) || (readsLiteralSlot(mth, location, initial) && isa(type) && mth.ownsHeapMemory(location, type)) || (initial && initial.getDefiningOp() && isa(type) diff --git a/tslang/lib/TypeScript/MLIRGenVariables.cpp b/tslang/lib/TypeScript/MLIRGenVariables.cpp index 273f4b055..f7270cdf1 100644 --- a/tslang/lib/TypeScript/MLIRGenVariables.cpp +++ b/tslang/lib/TypeScript/MLIRGenVariables.cpp @@ -415,6 +415,12 @@ namespace mlirgen return V(mlirGenArrayRestCopy(location, mlir::cast(arrayType), arrayValue, index, genContext)); } + // past the end: the default of the element type, null for a reference (see below) + if (index >= constArrayType.getSize()) + { + return V(builder.create(location, constArrayType.getElementType())); + } + // TODO: unify it with ElementAccess auto constIndex = builder.create(location, builder.getI32Type(), builder.getI32IntegerAttr(index)); @@ -429,12 +435,30 @@ namespace mlirgen return V(mlirGenArrayRestCopy(location, arrayType, init, index, genContext)); } - // TODO: unify it with ElementAccess - auto constIndex = builder.create(location, builder.getI32Type(), - builder.getI32IntegerAttr(index)); - auto elemRef = builder.create( - location, mlir_ts::RefType::get(arrayType.getElementType()), init, constIndex); - return V(builder.create(location, arrayType.getElementType(), elemRef)); + // An element past the end of the array (`const [a, b, c] = pair`) is the default + // of its type - null for a reference, where TS has undefined - not whatever follows + // the array's data: a read of that was undefined behaviour, and under rc a count + // taken of it faulted. + auto lengthValue = builder.create(location, builder.getIndexType(), init); + auto indexValue = builder.create(location, mth.getIndexType(), + mth.getIndexAttrValue(index)); + auto inBoundsValue = V(builder.create(location, getBooleanType(), + builder.getI32IntegerAttr((int)SyntaxKind::GreaterThanToken), lengthValue, indexValue)); + + auto result = conditionalValue(location, inBoundsValue, + [&]() { + // TODO: unify it with ElementAccess + auto constIndex = builder.create(location, builder.getI32Type(), + builder.getI32IntegerAttr(index)); + auto elemRef = builder.create( + location, mlir_ts::RefType::get(arrayType.getElementType()), init, constIndex); + return ValueOrLogicalResult( + V(builder.create(location, arrayType.getElementType(), elemRef))); + }, + [&](mlir::Type elementType) { + return ValueOrLogicalResult(V(builder.create(location, elementType))); + }); + return result.failed_or_no_value() ? mlir::Value() : V(result); }) .Default([&](auto type) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index bcdd3c37c..d72970f33 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -398,6 +398,7 @@ tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PRO tslang_add_test(NAME test-compile-00-global-copy-counted COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.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-const-element-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_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") @@ -970,6 +971,7 @@ tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PR tslang_add_test(NAME test-jit-00-global-copy-counted COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.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-const-element-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_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") @@ -1932,6 +1934,7 @@ set(TSLANG_CORPUS 00global_copy_counted.ts 00c_string_copy.ts 00const_field_read.ts + 00const_element_read.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00const_element_read.ts b/tslang/test/tester/tests/00const_element_read.ts new file mode 100644 index 000000000..376c6cba7 --- /dev/null +++ b/tslang/test/tester/tests/00const_element_read.ts @@ -0,0 +1,75 @@ +// A `const` read out of an element keeps what it read alive when the element 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. A destructuring past the end of an array gives the element type's default (null for a +// reference), not whatever follows the array's data. +class C { + constructor(public x: number) {} +} + +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 element(i: number) { + const arr: C[] = [new C(0), new C(0)]; + arr[0] = new C(i); + const c = arr[0]; + arr[0] = new C(9); + churn(); + return c.x == i && arr[0].x == 9; +} + +function destructured(i: number) { + const arr: C[] = [new C(0), new C(i)]; + const [a, c] = arr; + arr[1] = new C(9); + churn(); + return c.x == i && a.x == 0 && arr[1].x == 9; +} + +function stringElement(i: number) { + const words: string[] = ["a", "b"]; + words[0] = "w" + i; + const w = words[0]; + words[0] = "x" + i; + churn(); + return w == "w" + i; +} + +function pastTheEnd() { + const pair: C[] = [new C(1), new C(2)]; + const [a, b, c] = pair; + let [d, e, f] = pair; + assert(a.x == 1 && b.x == 2 && d.x == 1 && e.x == 2, "in bounds"); + assert(c == null && f == null, "a class element past the end is null"); + + const words: string[] = ["a", "b"]; + const [w0, w1, w2] = words; + assert(w0 == "a" && w1 == "b" && w2 == null, "a string element past the end is null"); + + const nums: number[] = [1, 2]; + const [n0, n1, n2] = nums; + assert(n0 == 1 && n1 == 2 && n2 == 0, "a number element past the end is 0"); + + const [k0, k1] = [7]; + assert(k0 == 7 && k1 == 0, "past the end of an array literal"); +} + +function main() { + for (let i = 1; i < 4; i++) { + assert(element(i), "an element's overwrite frees what a const read of it holds"); + assert(destructured(i), "an element's overwrite frees what a const destructured from it holds"); + assert(stringElement(i), "a string element"); + } + + pastTheEnd(); + print("done."); +}