From dcc65d4966bb8abf823aee45feca42ffaaf42104 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 19:57:03 +0100 Subject: [PATCH] rc: a global object literal takes over what its fields were given (#461) `let o = { s: mk() }` at module level was freed at start under rc (and own). The literal is built in a temporary slot: the call's result is stored into its field, and the whole literal is read out and given to ts.GlobalResult. The store took no reference, so the call's +1 was given back at the end of the initializer region as a discarded temporary, and the global's field pointed at freed memory. Inside a function the same literal survives only because the block it ends with is the function's. claimGlobalInitializers now follows a global's value read out of a literal's temporary slot (one that owns nothing of its own) into the stores to that slot's fields: each owned producer stored there, through the same views, is marked consumed, and a field that is itself such a literal is followed into. The global then keeps each count. Test: 00global_object_literal (compile and JIT, and in the rc/none corpus): a let and a const literal global with class fields from calls, a nested literal, and a reassignment, read after allocations of the same size have reused anything freed. It failed under rc before the change. The #461 repro prints made1 under rc and own (it printed k15 and k31). The own corpus is unchanged. Fixes #461. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/OwnedReturnConsumptionPass.cpp | 72 +++++++++++++++++++ tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00global_object_literal.ts | 43 +++++++++++ 3 files changed, 118 insertions(+) create mode 100644 tslang/test/tester/tests/00global_object_literal.ts diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index c7b183445..f2f4e559b 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -304,6 +304,19 @@ class OwnedReturnConsumptionPass return; } + // An object literal (`let o = { s: mk() }`) is built in a temporary slot and read out + // whole: what its fields were given goes into the global with it (#461), nested + // literals included. + if (auto literalLoad = value.getDefiningOp()) + { + if (auto literalSlot = literalLoad.getReference().getDefiningOp(); + literalSlot && !literalSlot->hasAttr(OWNED_LOCAL_ATTR_NAME)) + { + claimStoredIntoLiteral(literalSlot); + return; + } + } + // A field or element read (`holder.child`, `new P().child`, `mkP().child`, // `mkArr()[0]`) carries no reference: the object or array keeps its count, and a // temporary one gives it back at the end of the region. The global takes a count of its @@ -321,6 +334,65 @@ class OwnedReturnConsumptionPass }); } + // Each owned producer stored into a field of an object literal's temporary slot, through the + // views of claimGlobalInitializers, is taken over by whoever takes the literal; a field that is + // itself a literal read out of its own temporary slot is followed into. + void claimStoredIntoLiteral(mlir_ts::VariableOp literalSlot) + { + for (auto *user : literalSlot->getUsers()) + { + auto fieldRef = mlir::dyn_cast(user); + if (!fieldRef) + { + continue; + } + + for (auto *fieldUser : fieldRef->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != fieldRef.getResult()) + { + continue; + } + + auto stored = storeOp.getValue(); + while (auto *definingOp = stored.getDefiningOp()) + { + if (auto castOp = mlir::dyn_cast(definingOp)) + { + stored = castOp.getIn(); + } + else if (auto optionalValueOp = mlir::dyn_cast(definingOp)) + { + stored = optionalValueOp.getIn(); + } + else if (auto createUnionInstanceOp = mlir::dyn_cast(definingOp)) + { + stored = createUnionInstanceOp.getIn(); + } + else + { + break; + } + } + + if (producesOwnedResult(stored)) + { + stored.getDefiningOp()->setAttr(OWNED_RESULT_CONSUMED_ATTR_NAME, + mlir::UnitAttr::get(&getContext())); + } + else if (auto innerLoad = stored.getDefiningOp()) + { + if (auto innerSlot = innerLoad.getReference().getDefiningOp(); + innerSlot && !innerSlot->hasAttr(OWNED_LOCAL_ATTR_NAME)) + { + claimStoredIntoLiteral(innerSlot); + } + } + } + } + } + // A conditional expression whose branches disagree about ownership. // // `cond ? a : f()` builds a `ts.If` with a result, and the two branches hand back references diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 8dfd1022b..0f53f727a 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -467,6 +467,7 @@ tslang_add_test(NAME test-compile-00-void COMMAND test-runner "${PROJECT_SOURCE_ tslang_add_test(NAME test-compile-00-in COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00in.ts") tslang_add_test(NAME test-compile-00-in-method-names COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00in_method_names.ts") tslang_add_test(NAME test-compile-00-class-structural-extends COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_structural_extends.ts") +tslang_add_test(NAME test-compile-00-global-object-literal COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_object_literal.ts") 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") @@ -954,6 +955,7 @@ tslang_add_test(NAME test-jit-00-void COMMAND test-runner -jit "${PROJECT_SOURCE tslang_add_test(NAME test-jit-00-in COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00in.ts") tslang_add_test(NAME test-jit-00-in-method-names COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00in_method_names.ts") tslang_add_test(NAME test-jit-00-class-structural-extends COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_structural_extends.ts") +tslang_add_test(NAME test-jit-00-global-object-literal COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_object_literal.ts") 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") @@ -1771,6 +1773,7 @@ set(TSLANG_CORPUS 00if_return.ts 00in_method_names.ts 00in.ts + 00global_object_literal.ts 00infer.ts 00global_union_from_call.ts 00instanceof.ts diff --git a/tslang/test/tester/tests/00global_object_literal.ts b/tslang/test/tester/tests/00global_object_literal.ts new file mode 100644 index 000000000..28d7e2bda --- /dev/null +++ b/tslang/test/tester/tests/00global_object_literal.ts @@ -0,0 +1,43 @@ +// A module-level object literal keeps what its fields were given (#461). Under rc the call's +// reference was given back at the end of the global's initializer, and nothing else held one, so +// the field pointed at freed memory. Each field is read after allocations of the same size have +// reused anything freed. +class Node { + v = 0; + name = ""; +} + +function mk(name: string) { + const n = new Node(); + n.name = name; + return n; +} + +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; +} + +let o = { s: mk("made" + 1), n: 7 }; +const p = { a: mk("first" + 2), b: mk("second" + 3) }; +let t = { inner: { s: mk("inner" + 4) } }; + +function main() { + assert(churn() == 1000); + assert(o.s.name == "made1" && o.n == 7, "a global literal keeps its field's object"); + assert(p.a.name == "first2" && p.b.name == "second3", "a const global literal keeps both fields"); + assert(t.inner.s.name == "inner4", "a nested global literal keeps its object"); + + o = { s: mk("next" + 5), n: 8 }; + assert(churn() == 1000); + assert(o.s.name == "next5" && o.n == 8, "a reassigned global literal keeps the new object"); + + print("done."); +}