From 7bfdfb815f753c02cdaa629217628f22d050dcb3 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 05:25:22 +0100 Subject: [PATCH 1/2] rc: hold an argument whose place the call overwrites An argument is borrowed, and two shapes overwrote the place it was read from while the call still used it: - a later argument of the same call assigns it: `f(n, (n = mk(), n))` for a local, a parameter or a local's field (A1). #647 covered a global; a local was still freed before the call ran. - the callee is given the object as well: `f(n, n.items[0])`, where `f` overwrites `p.items[0]` and then reads its other parameter (R8). Under rc holdGlobalArguments now also holds an argument read out of a slot or a place that something between the read and the call overwrites, and an argument read out of a field or an element of a value given as another argument of the call: retained where it is read, released after the call. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/OwnedReturnConsumptionPass.cpp | 116 ++++++++++++++++++ tslang/test/tester/CMakeLists.txt | 3 + .../tests/00argument_place_overwritten.ts | 95 ++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 tslang/test/tester/tests/00argument_place_overwritten.ts diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index a2ad4ea4b..4fa369a1c 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -555,6 +555,18 @@ class OwnedReturnConsumptionPass continue; } + // An argument read out of a slot or a place that a later argument of the same call + // overwrites (`f(n, (n = mk(), n))`), or out of a field or an element of an object + // the call is given as well (`f(n, n.items[0])`, which `f` may overwrite through + // its other parameter): either one frees the block the argument read. Under rc the + // caller holds it, as it holds a global's (A1, R8 of the fuzzer). + if (compileOptions.isRefCounted() && + (overwrittenBeforeCall(argument, op) || readFromOtherArgument(argument, op))) + { + held.push_back({op, argument}); + continue; + } + auto global = globalOf(argument, false); auto isHandle = MLIRTypeHelper::isSharedHandleType(argument.getType()); if (global.empty() || (isHandle ? !mayAssign(op, global) : !compileOptions.isRefCounted())) @@ -639,6 +651,110 @@ class OwnedReturnConsumptionPass } } + static mlir::Value stripCasts(mlir::Value value) + { + while (auto castOp = value.getDefiningOp()) + { + value = castOp.getIn(); + } + + return value; + } + + // Do two references name the same place? One SSA reference, one global, or one field of the + // same value - each access to a field builds a `ts.PropertyRef` of its own. + static bool samePlace(mlir::Value a, mlir::Value b) + { + if (a == b) + { + return true; + } + + auto globalA = a.getDefiningOp(); + auto globalB = b.getDefiningOp(); + if (globalA && globalB) + { + return globalA.getGlobalName() == globalB.getGlobalName(); + } + + auto fieldA = a.getDefiningOp(); + auto fieldB = b.getDefiningOp(); + return fieldA && fieldB && fieldA.getPosition() == fieldB.getPosition() && + sameValue(fieldA->getOperand(0), fieldB->getOperand(0)); + } + + // Are two values one value: one SSA value, or reads of one place? + static bool sameValue(mlir::Value a, mlir::Value b) + { + a = stripCasts(a); + b = stripCasts(b); + if (a == b) + { + return true; + } + + auto loadA = a.getDefiningOp(); + auto loadB = b.getDefiningOp(); + return loadA && loadB && samePlace(loadA.getReference(), loadB.getReference()); + } + + // Is the argument read out of a slot or a place that something between the read and the call + // overwrites - a later argument (`f(n, (n = mk(), n))`)? + static bool overwrittenBeforeCall(mlir::Value argument, mlir::Operation *call) + { + auto loadOp = stripCasts(argument).getDefiningOp(); + if (!loadOp || loadOp->getBlock() != call->getBlock()) + { + return false; + } + + auto reference = loadOp.getReference(); + for (auto *op = loadOp->getNextNode(); op && op != call; op = op->getNextNode()) + { + auto overwrites = op->walk([&](mlir::Operation *nested) { + auto storeOp = mlir::dyn_cast(nested); + auto releaseSlotOp = mlir::dyn_cast(nested); + auto place = storeOp ? storeOp.getReference() : releaseSlotOp ? releaseSlotOp.getSlot() : mlir::Value(); + return place && samePlace(place, reference) ? mlir::WalkResult::interrupt() + : mlir::WalkResult::advance(); + }); + + if (overwrites.wasInterrupted()) + { + return true; + } + } + + return false; + } + + // Is the argument read out of a field or an element of a value the call is given as another + // argument (`f(n, n.items[0])`)? + static bool readFromOtherArgument(mlir::Value argument, mlir::Operation *call) + { + auto value = stripCasts(argument); + for (auto depth = 0; depth < 8; depth++) + { + auto loadOp = value.getDefiningOp(); + auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr; + if (!mlir::isa_and_nonnull(place)) + { + return false; + } + + value = place->getOperand(0); + for (auto other : own_facts::callArgs(call)) + { + if (stripCasts(other) != stripCasts(argument) && sameValue(other, value)) + { + return true; + } + } + } + + return false; + } + // Where an exception out of `op` is taken first within its function: the region of the try // around it that runs then, in the order its landing pad takes them (TryOpLowering) - the // cleanup, else the catch clause, else the finally, for a call in its body; the finally for a diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9661bd9c4..7b426dc28 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-argument-place-overwritten COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_overwritten.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-argument-place-overwritten COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_overwritten.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 + 00argument_place_overwritten.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00argument_place_overwritten.ts b/tslang/test/tester/tests/00argument_place_overwritten.ts new file mode 100644 index 000000000..ec9b07025 --- /dev/null +++ b/tslang/test/tester/tests/00argument_place_overwritten.ts @@ -0,0 +1,95 @@ +// An argument read out of a local, a parameter or a field stays alive when the call overwrites +// where it was read: a later argument assigns it (`check(n, (n = mk(2), n), 1)`), or the callee +// overwrites a field or an element of an object it is given together with what was read out of +// it (`overwrite(n, n.items[0])`). The argument is borrowed, so under rc the overwrite freed the +// block the parameter still read. Each read follows allocations of the same size, so a freed block +// is reused. +class V { + tag = ""; + constructor(public x: number) { + this.tag = "v" + x; + } +} + +class N { + v: V; + items: V[]; + constructor(x: number) { + this.v = new V(x); + this.items = [new V(x + 100)]; + } +} + +function mk(x: number) { + return new N(x); +} + +function churn() { + const junk: N[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new N(999)); + } +} + +function check(n: N, other: N, x: number) { + churn(); + return n.v.x == x && n.v.tag == "v" + x && other.v.x > 0; +} + +function localReassigned() { + let n = mk(1); + return check(n, (n = mk(2), n), 1) && n.v.x == 2; +} + +function localReassignedInLoop() { + let n = mk(0); + for (let i = 1; i < 4; i++) { + if (!check(n, (n = mk(i), n), i - 1)) { + return false; + } + } + + return n.v.x == 3; +} + +function parameterReassigned(p: N) { + return check(p, (p = mk(7), p), 6); +} + +class H { + n = mk(0); +} + +function fieldReassigned() { + const h = new H(); + h.n = mk(3); + return check(h.n, (h.n = mk(4), h.n), 3) && h.n.v.x == 4; +} + +function overwriteElement(n: N, item: V) { + n.items[0] = new V(5); + churn(); + return item.x == 108 && item.tag == "v108"; +} + +function overwriteField(n: N, v: V) { + n.v = new V(5); + churn(); + return v.x == 9 && v.tag == "v9"; +} + +function main() { + for (let i = 0; i < 3; i++) { + assert(localReassigned(), "a local given before an argument that assigns it"); + assert(localReassignedInLoop(), "a local given before an argument that assigns it, in a loop"); + assert(parameterReassigned(mk(6)), "a parameter given before an argument that assigns it"); + assert(fieldReassigned(), "a local's field given before an argument that assigns it"); + + let n = mk(8); + assert(overwriteElement(n, n.items[0]), "an element given with its object, overwritten through it"); + n = mk(9); + assert(overwriteField(n, n.v), "a field given with its object, overwritten through it"); + } + + print("done."); +} From 10b6e918cd13c3375bfca4b2cea3d1ee32209ed2 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 06:37:09 +0100 Subject: [PATCH 2/2] rc: hold an argument read out of a value a later argument reassigns `f({ v: n.v }, (n = mk(), 0))` and `f(n.items[0], (n = mk(), 0))`: the argument is read out of a field or an element of `n`, and the later argument's assignment to `n` gives back the object it was read out of. overwrittenBeforeCall looked only at the argument's own place; it now walks out through the fields and elements it was read through, and holds the argument when any of those values' places is overwritten before the call. Found by the fuzzer (#646) past the first 1000 programs. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/OwnedReturnConsumptionPass.cpp | 48 ++++++++++++------- .../tests/00argument_place_overwritten.ts | 37 ++++++++++++-- 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index 4fa369a1c..277c0f818 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -700,29 +700,45 @@ class OwnedReturnConsumptionPass // Is the argument read out of a slot or a place that something between the read and the call // overwrites - a later argument (`f(n, (n = mk(), n))`)? + // + // So is one read out of a field or an element of a value read so: overwriting `n` in + // `f({ v: n.v }, (n = mk(), 0))` gives back the object whose field the argument read. static bool overwrittenBeforeCall(mlir::Value argument, mlir::Operation *call) { - auto loadOp = stripCasts(argument).getDefiningOp(); - if (!loadOp || loadOp->getBlock() != call->getBlock()) + auto value = stripCasts(argument); + for (auto depth = 0; depth < 8; depth++) { - return false; - } + auto loadOp = value.getDefiningOp(); + if (!loadOp || loadOp->getBlock() != call->getBlock()) + { + return false; + } - auto reference = loadOp.getReference(); - for (auto *op = loadOp->getNextNode(); op && op != call; op = op->getNextNode()) - { - auto overwrites = op->walk([&](mlir::Operation *nested) { - auto storeOp = mlir::dyn_cast(nested); - auto releaseSlotOp = mlir::dyn_cast(nested); - auto place = storeOp ? storeOp.getReference() : releaseSlotOp ? releaseSlotOp.getSlot() : mlir::Value(); - return place && samePlace(place, reference) ? mlir::WalkResult::interrupt() - : mlir::WalkResult::advance(); - }); + auto reference = loadOp.getReference(); + for (auto *op = loadOp->getNextNode(); op && op != call; op = op->getNextNode()) + { + auto overwrites = op->walk([&](mlir::Operation *nested) { + auto storeOp = mlir::dyn_cast(nested); + auto releaseSlotOp = mlir::dyn_cast(nested); + auto place = + storeOp ? storeOp.getReference() : releaseSlotOp ? releaseSlotOp.getSlot() : mlir::Value(); + return place && samePlace(place, reference) ? mlir::WalkResult::interrupt() + : mlir::WalkResult::advance(); + }); - if (overwrites.wasInterrupted()) + if (overwrites.wasInterrupted()) + { + return true; + } + } + + auto *place = reference.getDefiningOp(); + if (!mlir::isa_and_nonnull(place)) { - return true; + return false; } + + value = stripCasts(place->getOperand(0)); } return false; diff --git a/tslang/test/tester/tests/00argument_place_overwritten.ts b/tslang/test/tester/tests/00argument_place_overwritten.ts index ec9b07025..307f1ce2e 100644 --- a/tslang/test/tester/tests/00argument_place_overwritten.ts +++ b/tslang/test/tester/tests/00argument_place_overwritten.ts @@ -1,9 +1,10 @@ // An argument read out of a local, a parameter or a field stays alive when the call overwrites -// where it was read: a later argument assigns it (`check(n, (n = mk(2), n), 1)`), or the callee -// overwrites a field or an element of an object it is given together with what was read out of -// it (`overwrite(n, n.items[0])`). The argument is borrowed, so under rc the overwrite freed the -// block the parameter still read. Each read follows allocations of the same size, so a freed block -// is reused. +// where it was read: a later argument assigns it (`check(n, (n = mk(2), n), 1)`) or the object it +// was read out of (`checkItem(n.items[0], (n = mk(2), 0), 101)`), or the callee overwrites a field +// or an element of an object it is given together with what was read out of it +// (`overwrite(n, n.items[0])`). The argument is borrowed, so under rc the overwrite freed the block +// the parameter still read. Each read follows allocations of the same size, so a freed block is +// reused. class V { tag = ""; constructor(public x: number) { @@ -66,6 +67,30 @@ function fieldReassigned() { return check(h.n, (h.n = mk(4), h.n), 3) && h.n.v.x == 4; } +interface IV { + v: V; +} + +function checkView(iv: IV, n: number, x: number) { + churn(); + return iv.v.x == x && iv.v.tag == "v" + x; +} + +function checkItem(v: V, n: number, x: number) { + churn(); + return v.x == x && v.tag == "v" + x; +} + +function localsFieldInLiteral() { + let n = mk(1); + return checkView({ v: n.v }, (n = mk(2), 0), 1); +} + +function localsElement() { + let n = mk(1); + return checkItem(n.items[0], (n = mk(2), 0), 101); +} + function overwriteElement(n: N, item: V) { n.items[0] = new V(5); churn(); @@ -84,6 +109,8 @@ function main() { assert(localReassignedInLoop(), "a local given before an argument that assigns it, in a loop"); assert(parameterReassigned(mk(6)), "a parameter given before an argument that assigns it"); assert(fieldReassigned(), "a local's field given before an argument that assigns it"); + assert(localsFieldInLiteral(), "a field of a local, in an object literal, before an argument that assigns the local"); + assert(localsElement(), "an element of a local given before an argument that assigns the local"); let n = mk(8); assert(overwriteElement(n, n.items[0]), "an element given with its object, overwritten through it");