From 1f5039d17ca3a5e2e34169355258741e10904f4c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 20:21:19 +0100 Subject: [PATCH 1/2] A const object literal holding a borrowed field has a slot of its own (#460) Under rc, `const o = { s: p.child }` held nothing: overwriting `p.child` freed what `o.s` still pointed at. An object literal hands its fields over unretained - its receiver is the one that retains them, as a `let` does through the record's retain routine - but a `const` initialised with the literal is folded, a read of the literal's temporary slot, so nothing retained the field. processConstRef now gives identity storage (as for #454) to a const record read out whole from a literal's temporary slot when one of its fields was given a value with no reference of its own: a read of another object's field or element (`{ s: p.child }`), a read of an owning local (`{ x: a }`), or a literal nested in this one. The const then retains its fields as a `let` does, in every model. A literal of fresh values, constants or spread fields is unchanged. Not covered: a spread of a local's record (`const o2 = { ...o }`) still holds `o`'s fields unretained under rc; giving it storage turns one own corpus file (00spread_assignment) into an error, so it is left for its own change. Test: 00const_object_literal_field (compile and JIT, and in the rc/none corpus): a literal of a field read, a literal (and a nested one) of a local, and a loop, each read after allocations of the same size have reused anything freed. It failed under rc before the change. The own corpus is unchanged. Fixes #460. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 63 ++++++++++++++++++- tslang/test/tester/CMakeLists.txt | 3 + .../tests/00const_object_literal_field.ts | 49 +++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00const_object_literal_field.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index faade2dbf..aab9d7012 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1475,6 +1475,60 @@ class MLIRGenImpl return varOp && varOp->hasAttr(OWNED_LOCAL_ATTR_NAME); } + // 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 given a value read out of another object's field + // or element, or out of an owning local: what `{ s: p.child }` and `{ x: a }` are. Such a + // value carries no reference, and the literal takes none for it. + static bool readsLiteralSlot(mlir::Value value) + { + auto loadOp = value ? value.getDefiningOp() : mlir_ts::LoadOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || varOp->hasAttr(OWNED_LOCAL_ATTR_NAME)) + { + return false; + } + + for (auto *user : varOp->getUsers()) + { + auto fieldRef = dyn_cast(user); + if (!fieldRef) + { + continue; + } + + for (auto *fieldUser : fieldRef->getUsers()) + { + auto storeOp = dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != fieldRef.getResult()) + { + continue; + } + + auto stored = storeOp.getValue(); + while (auto castOp = stored.getDefiningOp()) + { + stored = castOp.getIn(); + } + + auto storedLoad = stored.getDefiningOp(); + auto *place = storedLoad ? storedLoad.getReference().getDefiningOp() : nullptr; + if (place && isa(place)) + { + return true; + } + + // a local that owns what it holds (`{ x: a }`, #454's shape), or a literal + // nested in this one + if (readsLocalSlot(stored) || readsLiteralSlot(stored)) + { + return true; + } + } + } + + return false; + } + mlir::LogicalResult processConstRef(mlir::Location location, mlir::OpBuilder &builder, const GenContext &genContext) { if (mlir::failed(getVariableTypeAndInit(location, genContext))) @@ -1518,10 +1572,17 @@ class MLIRGenImpl // (`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. + // + // 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 + // `let` does - so folded, `o` held none of them, and `p.child = d` freed what `o.s` + // still read (#460). MLIRTypeHelper mth(builder.getContext(), compileOptions); needsIdentityStorage = mth.hasBoundMethodField(type) || isa(type) || ((isa(type) || MLIRTypeHelper::isSharedHandleType(type) - || (readsLocalSlot(initial) && mth.ownsHeapMemory(location, type))) + || (readsLocalSlot(initial) && mth.ownsHeapMemory(location, type)) + || (readsLiteralSlot(initial) && isa(type) + && mth.ownsHeapMemory(location, type))) && !(varClass == VariableType::ConstRef) && !(initial && initial.getDefiningOp())); if (needsIdentityStorage) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 8dfd1022b..46eb88fa4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -470,6 +470,7 @@ tslang_add_test(NAME test-compile-00-class-structural-extends COMMAND test-runne tslang_add_test(NAME test-compile-00-instanceof COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof.ts") tslang_add_test(NAME test-compile-00-const-of-let COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_of_let.ts") tslang_add_test(NAME test-compile-00-instanceof-union COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof_union.ts") +tslang_add_test(NAME test-compile-00-const-object-literal-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_object_literal_field.ts") tslang_add_test(NAME test-compile-00-nullable-to-any COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullable_to_any.ts") tslang_add_test(NAME test-compile-00-ternary-new-or-null COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00ternary_new_or_null.ts") tslang_add_test(NAME test-compile-00-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class.ts") @@ -957,6 +958,7 @@ tslang_add_test(NAME test-jit-00-class-structural-extends COMMAND test-runner -j tslang_add_test(NAME test-jit-00-instanceof COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof.ts") tslang_add_test(NAME test-jit-00-const-of-let COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_of_let.ts") tslang_add_test(NAME test-jit-00-instanceof-union COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof_union.ts") +tslang_add_test(NAME test-jit-00-const-object-literal-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_object_literal_field.ts") tslang_add_test(NAME test-jit-00-nullable-to-any COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullable_to_any.ts") tslang_add_test(NAME test-jit-00-ternary-new-or-null COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00ternary_new_or_null.ts") tslang_add_test(NAME test-jit-00-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class.ts") @@ -1772,6 +1774,7 @@ set(TSLANG_CORPUS 00in_method_names.ts 00in.ts 00infer.ts + 00const_object_literal_field.ts 00global_union_from_call.ts 00instanceof.ts 00const_of_let.ts diff --git a/tslang/test/tester/tests/00const_object_literal_field.ts b/tslang/test/tester/tests/00const_object_literal_field.ts new file mode 100644 index 000000000..b22b64c19 --- /dev/null +++ b/tslang/test/tester/tests/00const_object_literal_field.ts @@ -0,0 +1,49 @@ +// A `const` object literal keeps what its fields hold (#460). Under rc the const was folded into a +// read of the literal's temporary slot, which took no reference for a field read out of another +// object (`{ s: p.child }`), so overwriting `p.child` freed what the literal still held. Each field +// is read after allocations of the same size have reused anything freed. +class Node { + v = 0; + name = ""; +} + +class P { + child: Node = new Node(); +} + +function churn() { + let keep: Node[] = []; + for (let i = 0; i < 1000; i++) { + const n = new Node(); + n.v = -i; + n.name = "junk" + i; + keep.push(n); + } + + return keep.length; +} + +function main() { + const p = new P(); + p.child.name = "first" + 1; + const o = { s: p.child, n: 2 }; + p.child = new Node(); + assert(churn() == 1000); + assert(o.s.name == "first1" && o.n == 2, "a const literal keeps the field it read"); + + let a = new Node(); + a.name = "local" + 3; + const t = { x: a, inner: { y: a } }; + a = new Node(); + assert(churn() == 1000); + assert(t.x.name == "local3" && t.inner.y.name == "local3", "a const literal keeps a local it read"); + + for (let i = 0; i < 1000; i++) { + const q = new P(); + const held = { c: q.child }; + q.child = new Node(); + held.c.v = i; + } + + print("done."); +} From 7146544a4bfefbfe1182cb835a0d3df97d2b3078 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 21:20:24 +0100 Subject: [PATCH 2/2] A const literal counts only fields that own a block (#460) Linux CI failed 00object_boxed_infra under rc (compile and JIT, glibc abort): `const copy = { ...it }` spreads a generator object, whose fields are a method bound to it (`next`) and a reference to its capture box (`.captured`). Both are read out of `it`'s fields, so readsLiteralSlot counted the literal as holding borrowed fields and gave `copy` storage and a record retain/release - which do not pair up for a bound method, so the release freed what it did not hold. Windows' heap let it pass. Only a field whose type owns a block now counts, and a bound method does not: `copy` is folded again, as on main, and `{ s: p.child }` still gets its storage. Checked on Linux (WSL, GCC): 00object_boxed_infra and 00const_object_literal_field pass under rc, none and the default model, compile and JIT. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index aab9d7012..da33f0c86 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1479,7 +1479,7 @@ class MLIRGenImpl // owns nothing of its own), with a field given a value read out of another object's field // or element, or out of an owning local: what `{ s: p.child }` and `{ x: a }` are. Such a // value carries no reference, and the literal takes none for it. - static bool readsLiteralSlot(mlir::Value value) + static bool readsLiteralSlot(MLIRTypeHelper &mth, mlir::Location location, mlir::Value value) { auto loadOp = value ? value.getDefiningOp() : mlir_ts::LoadOp(); auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); @@ -1504,6 +1504,16 @@ class MLIRGenImpl continue; } + // only a field that owns a block counts: not a reference (a generator's + // `.captured`), and not a method bound to the object it was read from (`{ ...it }` + // of a generator), whose routines do not pair up for it + auto storedType = storeOp.getValue().getType(); + if (!mth.ownsHeapMemory(location, storedType) || + isa(storedType)) + { + continue; + } + auto stored = storeOp.getValue(); while (auto castOp = stored.getDefiningOp()) { @@ -1519,7 +1529,7 @@ class MLIRGenImpl // a local that owns what it holds (`{ x: a }`, #454's shape), or a literal // nested in this one - if (readsLocalSlot(stored) || readsLiteralSlot(stored)) + if (readsLocalSlot(stored) || readsLiteralSlot(mth, location, stored)) { return true; } @@ -1581,7 +1591,7 @@ class MLIRGenImpl needsIdentityStorage = mth.hasBoundMethodField(type) || isa(type) || ((isa(type) || MLIRTypeHelper::isSharedHandleType(type) || (readsLocalSlot(initial) && mth.ownsHeapMemory(location, type)) - || (readsLiteralSlot(initial) && isa(type) + || (readsLiteralSlot(mth, location, initial) && isa(type) && mth.ownsHeapMemory(location, type))) && !(varClass == VariableType::ConstRef) && !(initial && initial.getDefiningOp()));