From d82cfbea07682100bb21b7410b72911be02f3dc4 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 30 Oct 2026 23:39:07 +0000 Subject: [PATCH 1/5] -mm=rc: a conditional between record literals keeps the objects its branch made `c ? { start: this.pos, dir: new V(1) } : { ... }`: each branch yields the literal it built, whose field holds an object the branch made, and the branch gave that object back before it ended. The receiver's retain after the `ts.If` - which retains the literal's fields - came too late, and the field was freed under it (heap corruption, 0xC0000374; hand-written fuzz seed review7__pM1). consumeConditionalResults now treats such a branch as it treats one yielding a field of a temporary it made: the branch retains what it yields. Not under own, which settles a literal's fields itself (and rejects this shape). Test: tests/00conditional_record_literal_temporary.ts (gc, and rc/none through the corpus). Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/OwnedReturnConsumptionPass.cpp | 29 ++++++++++++- tslang/test/tester/CMakeLists.txt | 6 +++ .../00conditional_record_literal_temporary.ts | 42 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00conditional_record_literal_temporary.ts diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index e649eaf56..5ef623697 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -1656,8 +1656,35 @@ class OwnedReturnConsumptionPass (mlir::isa_and_nonnull(slot) && slot->hasAttr(OWNED_LOCAL_ATTR_NAME))); }; + // ...or some branch yields a record literal it built whose field holds a temporary it made + // (`c ? { start: this.pos, dir: new V(1) } : ...`): the branch gives the temporary back before + // it ends, so the receiver's retain after the `ts.If` - which retains the fields - comes too + // late, and the field is freed under it. The branch retains it first. Not under -mm=own, + // which settles a literal's fields itself. + auto yieldsLiteralOfBranchTemporary = [&](mlir_ts::ResultOp resultOp) { + auto loadOp = producerOf(resultOp->getOperand(0)).getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (compileOptions.memoryModel == MemoryModelOwn || !varOp || + varOp->getParentRegion() != resultOp->getParentRegion()) + { + return false; + } + + return llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) { + auto propertyRefOp = mlir::dyn_cast(user); + return propertyRefOp && llvm::any_of(propertyRefOp->getUsers(), [&](mlir::Operation *fieldUser) { + auto storeOp = mlir::dyn_cast(fieldUser); + auto stored = storeOp && storeOp.getReference() == propertyRefOp.getResult() + ? producerOf(storeOp.getValue()) + : mlir::Value(); + return stored && stored.getDefiningOp() && ::typescript::mayTakeOverReference(stored) && + stored.getDefiningOp()->getParentRegion() == resultOp->getParentRegion(); + }); + }); + }; + if (llvm::none_of(yields, carriesOwned) && llvm::none_of(yields, borrowsBranchTemporary) && - llvm::none_of(yields, readsNamedObjectField)) + llvm::none_of(yields, readsNamedObjectField) && llvm::none_of(yields, yieldsLiteralOfBranchTemporary)) { return; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e31523c15..028852046 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -614,6 +614,7 @@ tslang_add_test(NAME test-compile-00-class-accessor-implements-field COMMAND tes tslang_add_test(NAME test-compile-00-object-literal-contextual-lambda COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-compile-own-00-object-literal-contextual-lambda COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-compile-00-interface-field-getter-owned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") +tslang_add_test(NAME test-compile-00-conditional-record-literal-temporary COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") tslang_add_test(NAME test-compile-00-call-arg-param-field-global COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") tslang_add_test(NAME test-compile-00-class-accessor-untyped-setter COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_untyped_setter.ts") tslang_add_test(NAME test-compile-00-class-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") @@ -1254,6 +1255,7 @@ tslang_add_test(NAME test-jit-00-class-accessor-implements-field COMMAND test-ru tslang_add_test(NAME test-jit-00-object-literal-contextual-lambda COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-jit-own-00-object-literal-contextual-lambda COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-jit-00-interface-field-getter-owned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") +tslang_add_test(NAME test-jit-00-conditional-record-literal-temporary COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") tslang_add_test(NAME test-jit-00-call-arg-param-field-global COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") tslang_add_test(NAME test-jit-00-class-accessor-untyped-setter COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_untyped_setter.ts") tslang_add_test(NAME test-jit-00-class-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") @@ -2269,6 +2271,7 @@ set(TSLANG_CORPUS 00class_accessor_implements_field.ts 00object_literal_contextual_lambda.ts 00interface_field_getter_owned.ts + 00conditional_record_literal_temporary.ts 00call_arg_param_field_global.ts 00class_accessor_untyped_setter.ts 00class_accessor.ts @@ -4751,6 +4754,9 @@ set(own_error_cases "own_err_borrows_not_generator|'@borrows' of 'copyOf' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_borrows_mixed_return|'@borrows' of 'pick' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_marker_unknown_param|'@keeps' names 'd', which is not a parameter of this function" + "own_err_literal_element_overwritten|:20:24: error: '[^']*' borrows 'HG[.]v' and cannot be stored, returned or captured" + "own_err_record_field_overwritten|:23:11: error: '[^']*' borrows an element and cannot be stored, returned or captured" + "own_err_record_borrow_in_try|:31:14: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_marker_identifier_arg|:8:8: error: '@keeps' takes the names of parameters as strings: '@keeps[(]\"it\"[)]'" "own_err_marker_override|'Derived.set' overrides 'Base.set', and has to say the same '@keeps', '@borrows', '@lends' and '@record'" "own_err_lends_missing|'firstOf' could return a borrow or keep a parameter, but it can be called from another module. '@lends' or '@keeps' would say it in its signature" diff --git a/tslang/test/tester/tests/00conditional_record_literal_temporary.ts b/tslang/test/tester/tests/00conditional_record_literal_temporary.ts new file mode 100644 index 000000000..e7e14603f --- /dev/null +++ b/tslang/test/tester/tests/00conditional_record_literal_temporary.ts @@ -0,0 +1,42 @@ +// A conditional between two object literals, one of which borrows a field (`{ start: this.pos, ... }`), +// each holding an object the branch makes (`new V(i)`). Under rc the branch gave its new object back +// before the receiver after the conditional retained the literal's fields, so the field was freed under +// it (heap corruption). The branch now retains the literal it yields. Each value is read after churn() +// has reused anything freed too early. +class V { + constructor(public x: number) {} +} + +interface Ray { + start: V; + dir: V; +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) keep.push(new V(77777)); + return keep.length; +} + +class Cam { + pos: V = new V(3); + + pick(i: number) { + const t: Ray = i == 1 ? { start: this.pos, dir: new V(i) } : { start: new V(i), dir: new V(i + 10) }; + churn(); + return t.start.x + t.dir.x; + } +} + +function main() { + const c = new Cam(); + let s = 0; + for (let i = 0; i < 4; i++) { + s += c.pick(i); + churn(); + assert(c.pos.x == 3, "pos"); + } + + assert(s == 0 + 10 + (3 + 1) + 2 + 12 + 3 + 13, "sum"); + print("done."); +} From edeb47a9f96a19614123e420f9f355a83fdf737c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 30 Oct 2026 23:39:13 +0000 Subject: [PATCH 2/5] -mm=own: three borrowing literals that compiled and crashed (fuzz seeds 73095, 73544, 74808) A 2000-program mm_fuzz round (seeds 73000-74999) found three own crashes among 1735 accepted programs; each is now a compile error. - An array literal argument that borrows its elements (`f([HG.v])`, classifyArgumentArrays) given to a callee that may overwrite an element (`p[0] = v`) or destroy anything: the overwrite freed the global's value and the new element was lost with the shallow release. Such an array no longer borrows, so the element read is the error it is anywhere else. - A borrowing record literal (`{ v: n.items[0] }`) given to a callee whose known overwrites include a field (`p.v = ...`): walkRecord treats it as a write of the field here, which keeps the record. - walkRecord treated a `ts.Invoke` (a call inside `try`) as a plain branch and skipped it, so a borrowing record given to a call in a `try` had no use there: `f({ v: n.items[0] }, (n = mk(), n))` freed the element before the call read it. Tests: own_err_literal_element_overwritten, own_err_record_field_overwritten, own_err_record_borrow_in_try (each compiled without error before). Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/OwnershipInferenceClosures.cpp | 12 +++++++ .../TypeScript/OwnershipInferenceRecords.cpp | 18 +++++++++- .../own_err_literal_element_overwritten.ts | 21 +++++++++++ .../own/own_err_record_borrow_in_try.ts | 36 +++++++++++++++++++ .../own/own_err_record_field_overwritten.ts | 24 +++++++++++++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/own/own_err_literal_element_overwritten.ts create mode 100644 tslang/test/tester/own/own_err_record_borrow_in_try.ts create mode 100644 tslang/test/tester/own/own_err_record_field_overwritten.ts diff --git a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp index ac842603b..e87d8bcb3 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp @@ -525,6 +525,18 @@ namespace own_inference return; } + // a callee that may overwrite an element (`p[0] = v`) gives back the old one, which is not + // the array's to give: the borrowed value is freed under its owner, and the new one is + // lost with the array's shallow release. So is a callee that may destroy anything + auto elementSlot = mlir_ts::RefType::get(mlir::cast(result.getType()).getElementType()); + auto places = callDropFields(call); + if (!callNoDrops(call) && (!places || llvm::any_of(*places, [&](auto &place) { + return place.first == -1 && place.second == elementSlot; + }))) + { + return; + } + // rc's references for the elements go: the array borrows them, for as long as the call setAttrTracked(arrayOp, OWN_BORROWING_ARRAY_ATTR_NAME); fieldBorrowRetains.insert(retains.begin(), retains.end()); diff --git a/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp b/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp index d708f9a71..147df9933 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp @@ -467,7 +467,8 @@ namespace own_inference continue; } - if (auto branchOp = mlir::dyn_cast(user)) + // (not a call in a `try`, a `ts.Invoke`: a branch too, but its operands are the call's) + if (auto branchOp = mlir::dyn_cast(user); branchOp && !isCall(user)) { if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) { @@ -519,6 +520,21 @@ namespace own_inference continue; } + // a callee that may overwrite a field of what it is given (`p.v = ...`) gives back + // the old value, which may be one the record borrows - as a write of the field here + // would + if (pending.record && !isCalleeOperand(use) && !callNoDrops(user)) + { + auto places = callDropFields(user); + if (places && llvm::any_of(*places, [](auto &place) { + return place.first >= 0 || place.first == OWN_INTERFACE_FIELD_POSITION; + })) + { + keep(user); + continue; + } + } + // a result that borrows it - a plain borrow (`id(r)`), or a borrowing record that // holds it - points where it points auto args = callArgs(user); diff --git a/tslang/test/tester/own/own_err_literal_element_overwritten.ts b/tslang/test/tester/own/own_err_literal_element_overwritten.ts new file mode 100644 index 000000000..1a8bdba27 --- /dev/null +++ b/tslang/test/tester/own/own_err_literal_element_overwritten.ts @@ -0,0 +1,21 @@ +// -mm=own rejects: an array literal that borrows its element (`[HG.v]`) given to a function that +// overwrites an element (`p[0] = ...`). The overwrite would free the global's value, which the array +// never owned, and the new element would be lost with the array. +class V { + constructor(public x: number) {} +} + +class H { + v: V = new V(1); +} + +const HG = new H(); + +function replaceFirst(p: V[]) { + p[0] = new V(3); + return p[0].x; +} + +function main() { + print(replaceFirst([HG.v]), HG.v.x); +} diff --git a/tslang/test/tester/own/own_err_record_borrow_in_try.ts b/tslang/test/tester/own/own_err_record_borrow_in_try.ts new file mode 100644 index 000000000..207b80db3 --- /dev/null +++ b/tslang/test/tester/own/own_err_record_borrow_in_try.ts @@ -0,0 +1,36 @@ +// -mm=own rejects: an object literal that borrows an element of `n` (`{ v: n.items[0] }`) given to a +// call whose later argument gives `n` a new value, which frees the element the literal borrows. Inside +// a `try` the calls are invokes, and the literal's use by the call was not seen. +class V { + constructor(public x: number) {} +} + +class N { + items: V[]; + constructor(x: number) { + this.items = [new V(x)]; + } +} + +interface IV { + v: V; +} + +function mk(x: number) { + return new N(x); +} + +function read(p: IV, q: N) { + return p.v.x + q.items[0].x; +} + +function main() { + let n = mk(7); + let s = 0; + try { + s += read({ v: n.items[0] }, (n = mk(1), n)); + } finally { + } + + print(s); +} diff --git a/tslang/test/tester/own/own_err_record_field_overwritten.ts b/tslang/test/tester/own/own_err_record_field_overwritten.ts new file mode 100644 index 000000000..8fac8663d --- /dev/null +++ b/tslang/test/tester/own/own_err_record_field_overwritten.ts @@ -0,0 +1,24 @@ +// -mm=own rejects: an object literal that borrows its field (`{ v: n.items[0] }`) given to a function +// that overwrites that field through its interface (`p.v = ...`). The overwrite would free the +// element, which the literal never owned. +class V { + constructor(public x: number) {} +} + +class N { + items: V[] = [new V(1)]; +} + +interface IV { + v: V; +} + +function replace(p: IV) { + p.v = new V(8); + return p.v.x; +} + +function main() { + const n = new N(); + print(replace({ v: n.items[0] }), n.items[0].x); +} From e5f304baaa3bdd1279e723c46478ba6e7e3926c7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 30 Oct 2026 23:54:36 +0000 Subject: [PATCH 3/5] -mm=own: a getter-filled interface field read put in a `let` is freed at the returns `let x = i.item; for (...) use(x); return x.n` - a read through an interface field a getter may fill, held in a `let` read across blocks - was never freed: #751 releases such a read only when it and what is borrowed from it stay in its block. Two million calls kept two million Boxes (280.5 MB peak; 4.3 MB now). releaseCellReads now also frees a read that is the initializer of a `let` nobody assigns or captures, whose reads stay in the function (staysInBlock over any block), when the read reaches every return and the object it reads through is a parameter's, which outlives them: the same conditional release, at each return. A read returned or stored still leaks on the getter's path. Tests: own_interface_getter_let (own, no counting, rc, gc, none) and test-own-interface-getter-let-released, which reads the IR (fails before the fix). Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipInferencePass.cpp | 80 ++++++++++++++++--- .../lib/TypeScript/OwnershipInferencePass.h | 3 + tslang/test/tester/CMakeLists.txt | 11 ++- .../tester/own-interface-getter-let.cmake | 28 +++++++ .../tester/own/own_interface_getter_let.ts | 55 +++++++++++++ 5 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 tslang/test/tester/own-interface-getter-let.cmake create mode 100644 tslang/test/tester/own/own_interface_getter_let.ts diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 80dd0ab57..c91960686 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -56,7 +56,8 @@ namespace own_inference } // Does nothing `value` gives - its own value, a field read out of it, a call's result that may be it - leave - // `block`? Then the last of them that uses it is collected into `last`. A scalar is not followed: it is a copy. + // `block` (the function, when null)? Then the last of them that uses it is collected into `last` (in `block` + // only). A scalar is not followed: it is a copy. static bool staysInBlock(mlir::Value value, mlir::Block *block, mlir::Operation *&last, int depth = 0) { auto isScalar = [](mlir::Type type) { @@ -75,12 +76,12 @@ namespace own_inference for (auto *user : value.getUsers()) { - if (user->getBlock() != block) + if (block && user->getBlock() != block) { return false; } - if (!last || last->isBeforeInBlock(user)) + if (block && (!last || last->isBeforeInBlock(user))) { last = user; } @@ -95,12 +96,12 @@ namespace own_inference { for (auto *refUser : user->getResult(0).getUsers()) { - if (refUser->getBlock() != block) + if (block && refUser->getBlock() != block) { return false; } - if (last->isBeforeInBlock(refUser)) + if (block && last->isBeforeInBlock(refUser)) { last = refUser; } @@ -161,20 +162,77 @@ namespace own_inference continue; } + mlir::OpBuilder builder(loadOp->getContext()); + auto release = [&](mlir::Operation *at) { + auto releaseOp = builder.create(loadOp.getLoc(), loadOp.getResult()); + releaseOp->setAttr(OWN_CELL_READ_ATTR_NAME, builder.getUnitAttr()); + }; + mlir::Operation *last = nullptr; - if (!staysInBlock(loadOp.getResult(), loadOp->getBlock(), last) || !last || - last->hasTrait()) + if (staysInBlock(loadOp.getResult(), loadOp->getBlock(), last) && last && + !last->hasTrait()) { + builder.setInsertionPointAfter(last); + release(last); continue; } - mlir::OpBuilder builder(last->getContext()); - builder.setInsertionPointAfter(last); - auto releaseOp = builder.create(loadOp.getLoc(), loadOp.getResult()); - releaseOp->setAttr(OWN_CELL_READ_ATTR_NAME, builder.getUnitAttr()); + // ...or put in a `let` (`let x = i.item`) that is only read, across blocks too, and nothing read + // out of it leaves the function: freed at the function's returns, when the read reaches them all + // and the object it reads through is a parameter's, which outlives them + auto exits = localHeldToExits(loadOp); + for (auto *exit : exits) + { + builder.setInsertionPoint(exit); + release(exit); + } } } + llvm::SmallVector OwnershipInferencePass::localHeldToExits(mlir_ts::LoadOp read) + { + auto result = read.getResult(); + auto varOp = result.hasOneUse() ? mlir::dyn_cast(*result.getUsers().begin()) : mlir_ts::VariableOp(); + if (!varOp || varOp.getInitializer() != result || varOp.getCaptured().value_or(false)) + { + return {}; + } + + // only read, and what is read stays in the function + for (auto *user : varOp->getUsers()) + { + mlir::Operation *ignored = nullptr; + auto slotRead = mlir::dyn_cast(user); + if (!mlir::isa(user) && + (!slotRead || !staysInBlock(slotRead.getResult(), /*block=*/nullptr, ignored))) + { + return {}; + } + } + + // read through a parameter: `i.item` of an interface parameter `i` + auto refOp = read.getReference().getDefiningOp(); + auto objectRead = refOp ? refOp->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + auto objectSlot = objectRead ? objectRead.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto index = -1; + if (!objectSlot || !isParameterSlot(objectSlot, index)) + { + return {}; + } + + // every return, after the read + auto f = getFunction(); + mlir::DominanceInfo dominanceInfo(f); + llvm::SmallVector exits; + auto all = true; + f.walk([&](mlir_ts::ReturnInternalOp returnOp) { + exits.push_back(returnOp); + all = all && dominanceInfo.properlyDominates(read.getOperation(), returnOp.getOperation()); + }); + + return all ? exits : llvm::SmallVector(); + } + bool OwnershipInferencePass::analyze(llvm::SetVector &toErase) { auto f = getFunction(); diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 9c389f5bb..62bfd2329 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -84,6 +84,9 @@ class OwnershipInferencePass // what is borrowed from it stay in its block: a release after their last use, for the getter's path only. One // nothing uses - what an assignment `i.f = v` leaves of the place it wrote - goes. void releaseCellReads(); + // The returns of the function, where a read through a parameter's interface field put in a `let` that is + // only read is released (releaseCellReads); none when the read does not reach them all or may leave. + llvm::SmallVector localHeldToExits(mlir_ts::LoadOp read); // The analysis of one function; true when it passed. In a dry run it erases nothing and strips // no facts. diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 028852046..3efb97200 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -4093,7 +4093,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot own_equal_strings_into_any own_any_copies own_array_copies own_string_pushed_twice own_rest_takes own_shares_elements - own_rest_keeps_declared) + own_rest_keeps_declared own_interface_getter_let) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. @@ -4132,7 +4132,7 @@ foreach(keeps_mm rc gc none) own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures own_literal_variable_closures own_temporary_field_take own_union_narrowed_calls own_named_field_take own_let_field_take own_literal_captures own_literal_object_captures own_literal_interface_captures own_literal_let_captures own_interface_generic_method own_back_reference own_field_function_result own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot own_equal_strings_into_any - own_rest_keeps_declared) + own_rest_keeps_declared own_interface_getter_let) tslang_add_test(NAME test-jit-${keeps_mm}-${keeps_test} COMMAND test-runner -jit -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") tslang_add_test(NAME test-compile-${keeps_mm}-${keeps_test} COMMAND test-runner -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") endforeach() @@ -4198,6 +4198,13 @@ add_test(NAME test-own-caught-value-call set_tests_properties(test-own-caught-value-call PROPERTIES PASS_REGULAR_EXPRESSION "ts.Func" FAIL_REGULAR_EXPRESSION "error|Stack dump") +# A read through an interface field a getter may fill, put in a `let` that is only read, is freed at the +# function's returns under own, on the getter's path only; no run can see the leak, so the IR is read. +add_test(NAME test-own-interface-getter-let-released + COMMAND ${CMAKE_COMMAND} -DTSLANG=$ + "-DFILE=${PROJECT_SOURCE_DIR}/test/tester/own/own_interface_getter_let.ts" + -P "${CMAKE_CURRENT_SOURCE_DIR}/own-interface-getter-let.cmake") + # A rest parameter its function says it keeps (`@keeps`) is released once emptied under own, as an # inferred keep's is (spec 72); no run can see the leak, so the IR is read. add_test(NAME test-own-rest-keeps-declared-released diff --git a/tslang/test/tester/own-interface-getter-let.cmake b/tslang/test/tester/own-interface-getter-let.cmake new file mode 100644 index 000000000..fb201b5fa --- /dev/null +++ b/tslang/test/tester/own-interface-getter-let.cmake @@ -0,0 +1,28 @@ +# Under own a read through an interface field put in a `let` that is only read (own_interface_getter_let.ts) is +# released at the function's returns on the getter's path (localHeldToExits). No run +# shows a leak, so the IR is read (--emit=mlir-affine): `heldInLet` releases the read before it returns. +cmake_minimum_required(VERSION 3.17.3) +execute_process(COMMAND "${TSLANG}" --emit=mlir-affine --no-default-lib -mm=own "${FILE}" + OUTPUT_VARIABLE out ERROR_VARIABLE err RESULT_VARIABLE result) +# the IR is printed to stderr +string(APPEND out "${err}") +if(NOT result EQUAL 0) + message(FATAL_ERROR "tslang failed (${result}):\n${out}") +endif() + +# the body of `heldInLet`, from its `ts.Func` to the next one +string(FIND "${out}" "ts.Func @heldInLet " begin) +if(begin EQUAL -1) + message(FATAL_ERROR "no function `heldInLet` in:\n${out}") +endif() +string(SUBSTRING "${out}" ${begin} -1 body) +string(SUBSTRING "${body}" 1 -1 rest) +string(FIND "${rest}" "ts.Func @" end) +if(NOT end EQUAL -1) + math(EXPR end "${end} + 1") + string(SUBSTRING "${body}" 0 ${end} body) +endif() + +if(NOT body MATCHES "\"ts[.]Release\"[(]%[0-9]+[)] {__own_cell_read}") + message(FATAL_ERROR "`heldInLet` does not release the read through the interface field:\n${body}") +endif() diff --git a/tslang/test/tester/own/own_interface_getter_let.ts b/tslang/test/tester/own/own_interface_getter_let.ts new file mode 100644 index 000000000..57be81dec --- /dev/null +++ b/tslang/test/tester/own/own_interface_getter_let.ts @@ -0,0 +1,55 @@ +// -mm=own: a read through an interface's field that a getter may fill, put in a `let` that is only read - +// across a loop, out of the read's block - is freed at the function's returns, on the getter's path only +// (OwnershipInferencePass::localHeldToExits). It leaked before: two million calls kept two million Boxes +// (280 MB, 4.3 MB now). Through a plain field the same read is a borrow, and is not freed. Values are read +// after churn() has reused anything freed too early. +class Box { + data: number[] = [1, 2, 3]; + constructor(public n: number) {} +} + +interface I { + item: Box; +} + +class Fresh implements I { + private k = 0; + get item() { this.k = this.k + 1; return new Box(10 * this.k); } + set item(x: Box) { this.k = x.n; } +} + +class Plain implements I { + item = new Box(2); +} + +function churn() { + const junk: Box[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new Box(999)); + } +} + +function heldInLet(i: I) { + let x = i.item; + let total: number = 0; + for (let k = 0; k < 3; k++) { + churn(); + total = total + x.data.length; + } + + return total + x.n; +} + +function main() { + const f: I = new Fresh(); + const p: I = new Plain(); + let s: number = 0; + for (let k = 0; k < 4; k++) { + s = s + heldInLet(f) + heldInLet(p); + churn(); + } + + assert(s == (9 + 10) + (9 + 20) + (9 + 30) + (9 + 40) + 4 * (9 + 2), "sum"); + assert(p.item.n == 2 && p.item.data.length == 3, "plain item kept"); + print("done."); +} From a588196d7e779e20057b3802e4e1dd2075dd5aef Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 00:16:13 +0000 Subject: [PATCH 4/5] -mm=own: a getter-filled interface field read that would leave its block is an error User ruling (2026-10-31): `return i.item`, where a class of the module fills `item` with a getter, is a compile error rather than a leak. On the getter's path the read is a new value only its own release frees; returned (a borrow of `i` to the caller), stored, or kept where neither the in-block release nor the `let` release reaches, it was lost. releaseCellReads now reports such a read when a class in the module fills a field of that name with accessors (its `...vtbl..accessor` cell); a field no class fills that way is a borrow on every path and may still be returned. A method called on the read (`i.item.get()`) is followed by staysInBlock now (the bound method, its object and function, the call), so it is freed rather than rejected. DefaultLib src/lib.ts compiles under own and rc with no errors. Test: own_err_getter_read_returned. Release ctest 6506/6506; mm_fuzz seeds 76000-76399 clean. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipInferencePass.cpp | 57 +++++++++++++++++-- .../lib/TypeScript/OwnershipInferencePass.h | 2 + tslang/test/tester/CMakeLists.txt | 1 + .../own/own_err_getter_read_returned.ts | 25 ++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 tslang/test/tester/own/own_err_getter_read_returned.ts diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index c91960686..f5389d21f 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -116,10 +116,17 @@ namespace own_inference continue; } - // an argument: the result may be it, or borrowed from it - auto callIndirectOp = mlir::dyn_cast(user); - if ((mlir::isa(user) || callIndirectOp) && - (!callIndirectOp || callIndirectOp.getCallee() != value)) + // a method of it (`i.item.get()`): the method bound to it, its object and its function, which only + // the call below uses + if (mlir::isa(user) && + user->getNumResults() == 1 && follow(user->getResult(0))) + { + continue; + } + + // an argument, or the method called: the result may be it, or borrowed from it + if (mlir::isa(user)) { for (auto result : user->getResults()) { @@ -186,9 +193,51 @@ namespace own_inference builder.setInsertionPoint(exit); release(exit); } + + // ...else a getter's new value would be lost: returned, stored, or kept where nothing frees it. Only + // a field some class of this module fills with accessors has one; its value cannot leave its read + auto refOp = loadOp.getReference().getDefiningOp(); + if (!exits.empty() || !refOp) + { + continue; + } + + auto fieldAttr = mlir::dyn_cast(refOp.getIdentifier()); + auto filler = fieldAttr ? accessorFiller(fieldAttr.getValue()) : llvm::StringRef(); + if (!filler.empty()) + { + loadOp.emitError("'") + << fieldAttr.getValue() << "' is read through an interface, and '" << filler + << "' fills it with a getter, whose new value -mm=own frees only where it is read: it cannot " + "be returned, kept or outlive its block; read the field where it is used"; + signalPassFailure(); + } } } + llvm::StringRef OwnershipInferencePass::accessorFiller(llvm::StringRef field) + { + // the cell of a class's accessors for an interface's field: `...vtbl..accessor` + auto suffix = (llvm::Twine(".") + field + ".accessor").str(); + auto module = getFunction()->getParentOfType(); + llvm::StringRef found; + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto name = globalOp.getSymName(); + auto vtbl = name.find("..vtbl."); + if (vtbl != llvm::StringRef::npos && name.ends_with(suffix) && vtbl + 6 + suffix.size() == name.size()) + { + // the class: up to the interface's name + auto head = name.take_front(vtbl); + found = head.take_front(head.rfind('.') == llvm::StringRef::npos ? head.size() : head.rfind('.')); + return mlir::WalkResult::interrupt(); + } + + return mlir::WalkResult::advance(); + }); + + return found; + } + llvm::SmallVector OwnershipInferencePass::localHeldToExits(mlir_ts::LoadOp read) { auto result = read.getResult(); diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 62bfd2329..414e585f0 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -87,6 +87,8 @@ class OwnershipInferencePass // The returns of the function, where a read through a parameter's interface field put in a `let` that is // only read is released (releaseCellReads); none when the read does not reach them all or may leave. llvm::SmallVector localHeldToExits(mlir_ts::LoadOp read); + // The class of this module that fills an interface's field of this name with accessors, if any. + llvm::StringRef accessorFiller(llvm::StringRef field); // The analysis of one function; true when it passed. In a dry run it erases nothing and strips // no facts. diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 3efb97200..7e79e7d9a 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -4761,6 +4761,7 @@ set(own_error_cases "own_err_borrows_not_generator|'@borrows' of 'copyOf' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_borrows_mixed_return|'@borrows' of 'pick' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_marker_unknown_param|'@keeps' names 'd', which is not a parameter of this function" + "own_err_getter_read_returned|:19:12: error: 'item' is read through an interface, and 'Fresh' fills it with a getter" "own_err_literal_element_overwritten|:20:24: error: '[^']*' borrows 'HG[.]v' and cannot be stored, returned or captured" "own_err_record_field_overwritten|:23:11: error: '[^']*' borrows an element and cannot be stored, returned or captured" "own_err_record_borrow_in_try|:31:14: error: '[^']*' borrows a field but is used here after it may be released or overwritten" diff --git a/tslang/test/tester/own/own_err_getter_read_returned.ts b/tslang/test/tester/own/own_err_getter_read_returned.ts new file mode 100644 index 000000000..739a39c58 --- /dev/null +++ b/tslang/test/tester/own/own_err_getter_read_returned.ts @@ -0,0 +1,25 @@ +// -mm=own rejects: a read through an interface's field that a class fills with a getter, returned. On +// the getter's path the read is a new value only the read's own release frees; returned, it would be a +// borrow of `i` to the caller, and leak (one Box per call). A field no class fills with accessors is a +// borrow on every path, and may be returned. +class Box { + constructor(public n: number) {} +} + +interface I { + item: Box; +} + +class Fresh implements I { + get item() { return new Box(1); } + set item(x: Box) {} +} + +function pick(i: I) { + return i.item; +} + +function main() { + const f: I = new Fresh(); + print(pick(f).n); +} From 9804fff0e73b869ac8459afa57f587a8ff9136c5 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 00:32:07 +0000 Subject: [PATCH 5/5] -mm=own: a callback may give a captured object a new value `bag.forEach(b => { last = new Box(b.n) })` was rejected: the assignment through a captured variable's cell in the closure body was a drop of anything (fieldDropsInBody), so a method that walks the array a field holds and calls the callback had its element borrow "used after it may be released". Spec 52 made this precise for numbers and strings; this does it for objects and arrays. - The assignment is a place drop of its own, OWN_CAPTURED_CELL_POSITION: it frees what only the cell owns, so it drops a chain read through a captured variable's cell (dropsChain), nothing else. - A call given anything read through a variable a closure assigns, when the call may run such a closure, is an error where it is made (checkArgumentsAssigned): no parameter of the callee can be under the value the closure frees. `bag.forEach(b => { bag = new Bag() })` is that error. Tests: own_callback_assigns_capture (own, no counting, rc, gc, none; rejected before), own_err_callback_assigns_receiver, own_err_callback_assigns_borrowed. DefaultLib src/lib.ts compiles under own and rc with no errors. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/OwnershipFacts.h | 5 ++ .../OwnershipInferenceBorrowers.cpp | 28 ++++++++++- .../OwnershipInferencePlaceReads.cpp | 12 +++++ .../lib/TypeScript/OwnershipSignaturePass.cpp | 6 +++ tslang/test/tester/CMakeLists.txt | 7 ++- .../own/own_callback_assigns_capture.ts | 49 +++++++++++++++++++ .../own/own_err_callback_assigns_borrowed.ts | 26 ++++++++++ .../own/own_err_callback_assigns_receiver.ts | 19 +++++++ .../own_err_interface_arg_assigned_in_call.ts | 28 +++++++++++ 9 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 tslang/test/tester/own/own_callback_assigns_capture.ts create mode 100644 tslang/test/tester/own/own_err_callback_assigns_borrowed.ts create mode 100644 tslang/test/tester/own/own_err_callback_assigns_receiver.ts create mode 100644 tslang/test/tester/own/own_err_interface_arg_assigned_in_call.ts diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 23a6c9f5c..dbfe3b070 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -70,6 +70,11 @@ constexpr int64_t OWN_INTERFACE_FIELD_POSITION = -3; // The position of a captured variable's cell holding a string, assigned: the old string goes, and a // string owns nothing else, so only a borrow that is a string may be it (spec 52) constexpr int64_t OWN_CAPTURED_STRING_POSITION = -4; +// The position of a captured variable's cell holding a block (an object, an array), assigned: the old +// value goes with all it owns, and only the cell owns it, so only a borrow read through a captured +// variable's cell may be under it. A call given anything read through a variable a closure assigns +// is an error where it is made (checkArgumentsAssigned), so no parameter of the callee is under it. +constexpr int64_t OWN_CAPTURED_CELL_POSITION = -5; // On a function whose body relies on a fact its callers cannot all know (a parameter it keeps, a // result that borrows): why, for the error the body gets instead. #define OWN_FACTS_LOST_ATTR_NAME "__own_facts_lost" diff --git a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp index 151c8e412..bbced418e 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp @@ -29,6 +29,13 @@ namespace own_inference } } + // what a view of it shows (`f(n, ...)` with `n` given as an interface): the value read + for (auto depth = 0; depth < 8 && isView(argument.getDefiningOp()) && !argument.getDefiningOp(); + depth++) + { + argument = argument.getDefiningOp()->getOperand(0); + } + auto *read = argument.getDefiningOp(); auto local = mlir::dyn_cast_or_null(slotOf(argument)); if (!read || !local || !ownsHeap(argument) || isHandle(argument)) @@ -48,12 +55,31 @@ namespace own_inference return (storeOp && storeOp.getReference() == local.getResult()) || (releaseSlotOp && releaseSlotOp.getSlot() == local.getResult()); }); + // ...or the call itself, when it may run a closure that gives a captured object a new value + // (OWN_CAPTURED_CELL_POSITION): its callee's borrow of the argument would not see that + auto places = callDropFields(call); + if (!write && assignedByClosure && places && llvm::any_of(*places, [](auto &place) { + return place.first == OWN_CAPTURED_CELL_POSITION; + })) + { + write = call; + } + if (!write) { continue; } - if (reporting()) + if (reporting() && write == call) + { + auto name = varName(local); + auto callee = calleeName(call); + call->emitError("'") << name << "' is lent to '" << (callee.empty() ? "a function" : callee) + << "', which may run a closure that gives '" << name + << "' a new value; -mm=own cannot prove it outlives the call"; + signalPassFailure(); + } + else if (reporting()) { auto name = varName(local); auto callee = calleeName(call); diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index 4c26f15ae..43f988945 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -732,6 +732,18 @@ namespace own_inference return !notString; } + // a captured object assigned frees what only its cell owns: a chain read through + // a captured variable's cell may be under it, nothing else (OWN_CAPTURED_CELL_POSITION) + if (place.first == OWN_CAPTURED_CELL_POSITION) + { + return llvm::any_of(chain.roots, [](auto &root) { + auto loadOp = root.first.template getDefiningOp(); + auto through = loadOp ? loadOp.getReference() : mlir::Value(); + return isCellVariable(root.first) || isLoadedCell(root.first) || + (through && (isCellVariable(through) || isLoadedCell(through))); + }); + } + return overwritesChain(place.first, place.second, chain); }); } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 1fcc41db3..e56862755 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1508,6 +1508,12 @@ namespace own_signature // a captured string assigned (`each += v` in a callback): it frees that string only drops.add({OWN_CAPTURED_STRING_POSITION, slot.getType()}); } + else if (isLoadedCell(slot) && !touchesHandle(op)) + { + // a captured object assigned (`last = new Box()` in a callback): it frees what the cell + // owns, which only a read through a captured variable may borrow + drops.add({OWN_CAPTURED_CELL_POSITION, slot.getType()}); + } else if (slot.getDefiningOp() || isLoadedCell(slot) || (!isPlace(slot) && touchesHandle(op) && !isCountedReadSlot(slot))) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7e79e7d9a..5c9b6a2b4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -4093,7 +4093,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot own_equal_strings_into_any own_any_copies own_array_copies own_string_pushed_twice own_rest_takes own_shares_elements - own_rest_keeps_declared own_interface_getter_let) + own_rest_keeps_declared own_interface_getter_let own_callback_assigns_capture) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. @@ -4132,7 +4132,7 @@ foreach(keeps_mm rc gc none) own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures own_literal_variable_closures own_temporary_field_take own_union_narrowed_calls own_named_field_take own_let_field_take own_literal_captures own_literal_object_captures own_literal_interface_captures own_literal_let_captures own_interface_generic_method own_back_reference own_field_function_result own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot own_equal_strings_into_any - own_rest_keeps_declared own_interface_getter_let) + own_rest_keeps_declared own_interface_getter_let own_callback_assigns_capture) tslang_add_test(NAME test-jit-${keeps_mm}-${keeps_test} COMMAND test-runner -jit -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") tslang_add_test(NAME test-compile-${keeps_mm}-${keeps_test} COMMAND test-runner -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") endforeach() @@ -4761,6 +4761,9 @@ set(own_error_cases "own_err_borrows_not_generator|'@borrows' of 'copyOf' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_borrows_mixed_return|'@borrows' of 'pick' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_marker_unknown_param|'@keeps' names 'd', which is not a parameter of this function" + "own_err_callback_assigns_receiver|:17:5: error: '[^']*' is lent to 'Bag[.]forEach', which may run a closure that gives '[^']*' a new value" + "own_err_callback_assigns_borrowed|:25:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_arg_assigned_in_call|:27:11: error: '[^']*' is lent to 'f', but is given a new value before the call takes it" "own_err_getter_read_returned|:19:12: error: 'item' is read through an interface, and 'Fresh' fills it with a getter" "own_err_literal_element_overwritten|:20:24: error: '[^']*' borrows 'HG[.]v' and cannot be stored, returned or captured" "own_err_record_field_overwritten|:23:11: error: '[^']*' borrows an element and cannot be stored, returned or captured" diff --git a/tslang/test/tester/own/own_callback_assigns_capture.ts b/tslang/test/tester/own/own_callback_assigns_capture.ts new file mode 100644 index 000000000..6bc6e8fa9 --- /dev/null +++ b/tslang/test/tester/own/own_callback_assigns_capture.ts @@ -0,0 +1,49 @@ +// -mm=own: a callback that gives a captured object a new value (`last = new Box(b.n)`), run by a method +// that walks the array a field holds. The assignment was a drop of anything, so the method's borrow of +// each element was "used after it may be released". It frees what only the captured variable owns +// (OWN_CAPTURED_CELL_POSITION): a borrow read through a captured variable may be under it, nothing else. +// Values are read after churn() has reused anything freed too early. The same program under every model. +class Box { + constructor(public n: number) {} +} + +class Bag { + items: Box[] = [new Box(1), new Box(2), new Box(3)]; + + forEach(f: (b: Box) => void) { + for (const b of this.items) { + f(b); + churn(); + } + } +} + +function churn() { + const junk: Box[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new Box(999)); + } +} + +function main() { + const bag = new Bag(); + + let total: number = 0; + bag.forEach(b => { total = total + b.n; }); + + // the last one, replaced on each call + let last = new Box(0); + bag.forEach(b => { last = new Box(b.n * 10); }); + churn(); + + // the largest, replaced on some calls only + let top = new Box(0); + bag.forEach(b => { if (b.n > top.n) top = new Box(b.n); }); + churn(); + + assert(total == 6, "total"); + assert(last.n == 30, "last"); + assert(top.n == 3, "top"); + assert(bag.items.length == 3 && bag.items[0].n == 1 && bag.items[2].n == 3, "items kept"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_callback_assigns_borrowed.ts b/tslang/test/tester/own/own_err_callback_assigns_borrowed.ts new file mode 100644 index 000000000..c5a6e523a --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_assigns_borrowed.ts @@ -0,0 +1,26 @@ +// -mm=own rejects: `inner` borrows a field of `last`, and a callback run across the call gives `last` a +// new value, freeing the field `inner` reads afterwards. +class Box { + constructor(public n: number) {} +} + +class Pair { + inner: Box; + constructor(n: number) { this.inner = new Box(n); } +} + +class Bag { + items: Box[] = [new Box(1), new Box(2)]; + + forEach(f: (b: Box) => void) { + for (const b of this.items) f(b); + } +} + +function main() { + const bag = new Bag(); + let last = new Pair(0); + const inner = last.inner; + bag.forEach(b => { last = new Pair(b.n); }); + print(inner.n, last.inner.n); +} diff --git a/tslang/test/tester/own/own_err_callback_assigns_receiver.ts b/tslang/test/tester/own/own_err_callback_assigns_receiver.ts new file mode 100644 index 000000000..ffbc6453e --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_assigns_receiver.ts @@ -0,0 +1,19 @@ +// -mm=own rejects: `bag` is lent to `forEach`, which walks the array `bag` holds while it runs a callback +// that gives `bag` a new value - freeing the array under the walk. +class Box { + constructor(public n: number) {} +} + +class Bag { + items: Box[] = [new Box(1), new Box(2)]; + + forEach(f: (b: Box) => void) { + for (const b of this.items) f(b); + } +} + +function main() { + let bag = new Bag(); + bag.forEach(b => { bag = new Bag(); }); + print(bag.items.length); +} diff --git a/tslang/test/tester/own/own_err_interface_arg_assigned_in_call.ts b/tslang/test/tester/own/own_err_interface_arg_assigned_in_call.ts new file mode 100644 index 000000000..9cdb9483e --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_arg_assigned_in_call.ts @@ -0,0 +1,28 @@ +// -mm=own rejects: `n` is lent to `f` as an interface, and a later argument gives `n` a new value, +// freeing the object before the call reads it (fuzz seed 77055). Given as itself, the same program was +// rejected; seen through the interface view the check missed it, and the call read freed memory. +class V { + constructor(public x: number) {} +} + +class N { + v: V; + constructor(x: number) { this.v = new V(x); } +} + +interface IX { + v: V; +} + +function mk(i: number) { + return new N(i); +} + +function f(p: IX, q: N) { + return p.v.x + q.v.x; +} + +function main() { + let n = mk(8); + print(f(n, (n = mk(3), mk(5)))); +}