From 599935267ef7f160bf2584b413807593e7690f9c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 01:21:08 +0100 Subject: [PATCH] rc: a global lent through a view is held from its read A global, or a global's field, given to a call as a view - an interface, a base class, an object literal holding it - and given a new value by a later argument of the same call (`f(G, (G = mk(), x))` with an interface parameter, `f({ v: G.v }, (G = mk(), x))`) was freed before the call ran. The view is made at the call, after every argument is evaluated, and holdGlobalArguments did not see through it: `globalOf` stopped at `ts.NewInterface`, and an object literal holds what it was given with no count until it is boxed for the interface, at the call too. Under rc the hold now keeps the value behind the view - the object a cast or an interface is made over, or each value stored into the literal's slot - retained where it is read (#647). The fuzzer (#646) found these as A1's view variant; they failed on #633 as well. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/OwnedReturnConsumptionPass.cpp | 81 +++++++++++++ tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00global_argument_view.ts | 109 ++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 tslang/test/tester/tests/00global_argument_view.ts diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index 65827c780..a2ad4ea4b 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -448,6 +448,81 @@ class OwnedReturnConsumptionPass return it != assigns.end() && it->getValue().contains(global); }; + // What a hold keeps of an argument: the value read before the call. Under rc a view of it is + // made at the call, after every argument is evaluated - after a later one may have given + // the global a new value - so the value read is the one behind it: the object a cast or an + // interface is made over (`f(G, (G = mk(), x))` with an interface parameter), or the values + // an object literal was given (`f({ v: G.v }, ..)`), which the literal holds with no count + // until it is boxed for the view, at the call too. + auto readsOf = [&](mlir::Operation *call, mlir::Value argument, llvm::SmallVectorImpl &reads) { + if (!compileOptions.isRefCounted()) + { + reads.push_back(argument); + return; + } + + auto value = argument; + for (;;) + { + if (auto castOp = value.getDefiningOp()) + { + value = castOp.getIn(); + } + else if (auto newInterfaceOp = value.getDefiningOp()) + { + value = newInterfaceOp.getThisVal(); + } + else + { + break; + } + } + + // an object literal boxed for an interface: stored into a new value_ref + auto newOp = value.getDefiningOp(); + if (newOp && mlir::isa(newOp.getInstance().getType())) + { + for (auto *user : newOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == newOp.getInstance()) + { + value = storeOp.getValue(); + break; + } + } + } + + // an object literal: a read of the slot it was built in, in the call's block + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (varOp && !varOp->hasAttr(OWNED_LOCAL_ATTR_NAME) && varOp->getBlock() == call->getBlock() && + mlir::isa(value.getType())) + { + for (auto *user : varOp->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()) + { + reads.push_back(storeOp.getValue()); + } + } + } + + return; + } + + reads.push_back(value); + }; + llvm::SmallVector> held; module.walk([&](mlir::Operation *op) { if (!own_facts::isCall(op) || op->hasTrait() || @@ -457,6 +532,7 @@ class OwnedReturnConsumptionPass } llvm::SmallPtrSet seen; + llvm::SmallVector reads; for (auto argument : own_facts::callArgs(op)) { // a method's object: `this` of the method bound to it @@ -469,6 +545,11 @@ class OwnedReturnConsumptionPass } } + readsOf(op, argument, reads); + } + + for (auto argument : reads) + { if (!mth.ownsHeapMemory(op->getLoc(), argument.getType()) || !seen.insert(argument).second) { continue; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 242cace14..9661bd9c4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -404,6 +404,7 @@ tslang_add_test(NAME test-compile-00-const-field-read COMMAND test-runner "${PRO 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-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-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") @@ -982,6 +983,7 @@ tslang_add_test(NAME test-jit-00-const-field-read COMMAND test-runner -jit "${PR 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-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-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") @@ -1950,6 +1952,7 @@ set(TSLANG_CORPUS 00const_element_read.ts 00const_global_read.ts 00global_argument_reassigned.ts + 00global_argument_view.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00global_argument_view.ts b/tslang/test/tester/tests/00global_argument_view.ts new file mode 100644 index 000000000..2e065bd34 --- /dev/null +++ b/tslang/test/tester/tests/00global_argument_view.ts @@ -0,0 +1,109 @@ +// An argument read out of a global and given to a call as a view - an interface, a base class, an +// object literal holding it - stays alive when a later argument gives the global a new value: +// `check(G, (G = mk(2), 0))` with an interface parameter, `check({ c: G.c }, (G = mk(2), 0))`. +// The view is made at the call, after every argument is evaluated, so under rc a hold taken on the +// view (00global_argument_reassigned.ts) came after the later argument had freed the block, and an +// object literal held what it was given with no count until then. Each read follows allocations of +// the same size, so a freed block is reused. +class Base { + tag = ""; + constructor(public x: number) { + this.tag = "c" + x; + } +} + +class C extends Base { + v: number[] = []; +} + +class H { + c: C = mk(3); +} + +interface IC { + x: number; + tag: string; +} + +interface IHolder { + c: C; +} + +function mk(x: number): C { + const r = new C(x); + r.v.push(x); + return r; +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.tag = "junk"; + c.v.push(99); + junk.push(c); + } +} + +let G: C = mk(1); +const GH = new H(); + +function thrower(): number { + throw "thrown"; +} + +function viaInterface(c: IC, n: number, x: number) { + churn(); + return c.x == x && c.tag == "c" + x; +} + +function viaBase(c: Base, n: number, x: number) { + churn(); + return c.x == x && c.tag == "c" + x; +} + +function viaHolder(h: IHolder, n: number, x: number) { + churn(); + return h.c.x == x && h.c.tag == "c" + x && h.c.v[0] == x; +} + +function viaRecord(h: { c: C }, n: number, x: number) { + churn(); + return h.c.x == x && h.c.tag == "c" + x && h.c.v[0] == x; +} + +function inLoop() { + for (let i = 0; i < 3; i++) { + const x = G.x; + assert(viaInterface(G, (G = mk(10 + i), 0), x), "an interface over a global, in a loop"); + const y = G.x; + assert(viaHolder({ c: G }, (G = mk(20 + i), 0), y), "an object literal holding a global, in a loop"); + } +} + +function inTryCatch() { + G = mk(30); + let caught = false; + try { + viaInterface(G, (G = mk(31), thrower()), 30); + } catch (e) { + caught = true; + } + + churn(); + assert(caught && G.x == 31 && G.tag == "c31", "a later argument throws after the assignment"); +} + +function main() { + assert(viaInterface(G, (G = mk(2), 0), 1), "an interface over a global"); + assert(viaBase(G, (G = mk(3), 0), 2), "a global as its base class"); + assert(viaInterface(GH.c, (GH.c = mk(4), 0), 3), "an interface over a global's field"); + assert(viaHolder({ c: G }, (G = mk(5), 0), 3), "an object literal holding a global"); + assert(viaHolder({ c: GH.c }, (GH.c = mk(6), 0), 4), "an object literal holding a global's field"); + assert(viaRecord({ c: G }, (G = mk(7), 0), 5), "an object literal given as a record"); + inLoop(); + inTryCatch(); + churn(); + assert(G.x == 31 && GH.c.x == 6, "the globals' last values"); + print("done."); +}