From 7fae379a04e21a6e386a52e1c6c5ee99e18bad85 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 23:31:48 +0000 Subject: [PATCH 1/3] -mm=own: Array and Array> compile, captured strings in callbacks - A string stored by a statement that may run again (fill's loop) is not a move of the parameter: each store takes a copy (spec 50). - A string or a handle read out of a place and stored into a place rc does not retain it for (a yield's record, `if (c) yield v`) gets a copy or a count of its own; a handle given to a callback that may release things gets a count for the call (spec 50). - A for...of local over handles, declared from a counted read and never assigned, is not a drop when released: the array still holds a count (spec 51). ArraySplice items are counted receivers, as push's are. - A callback that assigns a captured string frees only that string (OWN_CAPTURED_STRING_POSITION); a call given a closure it does not keep is judged by its callees' place drops (spec 52). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/OwnershipFacts.h | 3 + .../lib/TypeScript/OwnershipInferencePass.cpp | 28 +++++++ .../lib/TypeScript/OwnershipInferencePass.h | 6 +- .../OwnershipInferencePlaceReads.cpp | 66 +++++++++++++++- .../lib/TypeScript/OwnershipSignaturePass.cpp | 47 ++++++++++- .../lib/TypeScript/OwnershipSignaturePass.h | 3 + tslang/test/tester/CMakeLists.txt | 13 ++-- .../own/own_captured_string_callback.ts | 53 +++++++++++++ .../own/own_err_captured_string_drops.ts | 24 ++++++ .../tester/own/own_shared_counted_reads.ts | 77 +++++++++++++++++++ tslang/test/tester/own/own_string_copies.ts | 61 +++++++++++++++ 11 files changed, 369 insertions(+), 12 deletions(-) create mode 100644 tslang/test/tester/own/own_captured_string_callback.ts create mode 100644 tslang/test/tester/own/own_err_captured_string_drops.ts create mode 100644 tslang/test/tester/own/own_shared_counted_reads.ts create mode 100644 tslang/test/tester/own/own_string_copies.ts diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index f1a971d8c..26d33f127 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -67,6 +67,9 @@ namespace mlir_ts = mlir::typescript; // The position a place reached through an interface is given: its position in the object is not // known (spec 28) 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; // 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/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index a22b580fa..599702f71 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -562,6 +562,21 @@ namespace own_inference continue; } + // the place owns the copy, or a count of a handle's of its own (spec 23.2) + if (auto storeOp = mlir::dyn_cast(call)) + { + mlir::OpBuilder builder(storeOp); + if (isHandle(value)) + { + builder.create(storeOp->getLoc(), value); + continue; + } + + auto copy = builder.create(storeOp->getLoc(), value.getType(), value); + storeOp.getValueMutable().assign(copy); + continue; + } + // a callee that keeps it takes the copy; else it is released after the call auto args = callArgs(call); auto kept = llvm::any_of(ownedParams(call), [&](int32_t index) { @@ -569,6 +584,19 @@ namespace own_inference }); mlir::OpBuilder builder(call); + // a handle: a count of its own for the call, given back after it (spec 50) + if (isHandle(value)) + { + builder.create(call->getLoc(), value); + if (!kept) + { + builder.setInsertionPointAfter(call); + builder.create(call->getLoc(), value); + } + + continue; + } + auto copy = builder.create(call->getLoc(), value.getType(), value); for (auto &operand : call->getOpOperands()) { diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 23c19ddfd..096c23913 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -232,7 +232,8 @@ class OwnershipInferencePass // rc's references for boxes, which the closures decide llvm::DenseSet captureRetains; // A string read out of a place, given to a call that may release it (the dry run's): the call - // gets a copy instead, released after it (spec 22), read -> call + // gets a copy instead, released after it (spec 22), read -> call; or stored into a place, which + // owns the copy (spec 50), read -> store llvm::SmallVector> callCopies; // A move out of a field of an owning record local, being checked (spec 48): its retain and its // taker, which walkBorrowed takes for uses of the borrow and not for keeps @@ -827,6 +828,9 @@ class OwnershipInferencePass bool fieldTakeHolds(mlir::Operation *read, const FieldTake &take, llvm::ArrayRef uses); // Can `call` be given a copy of `value`, a string read out of a place, in its place (callCopies)? static bool copiesInto(mlir::Operation *call, mlir::Value value); + // Does `store` put `value`, a string read out of a place, into another place with no retain of + // rc's for it, so that the place can take a copy instead (callCopies)? + bool storesCopyOf(mlir::Operation *store, mlir::Value value); // A local that holds a borrow through `user`, and the field path under it that holds it (empty: // the local's value itself): a store of it into the local, or into a field of the local's value diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index 04c9ac8ae..b746e2628 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -57,6 +57,13 @@ namespace own_inference continue; } + // the count a call was given, given back right after it (spec 50) + if (auto *call = mlir::isa(user) ? user->getPrevNode() : nullptr; + call && isCall(call) && isCountedHandleReceiver(call, current)) + { + continue; + } + // a return and an `any` box take the value, though passesOn would hand them on if (isHandOff(user, current)) { @@ -126,6 +133,20 @@ namespace own_inference return retainedRightBefore(user, unshiftOp.getItems(), value); } + if (auto spliceOp = mlir::dyn_cast(user)) + { + return retainedRightBefore(user, spliceOp.getItems(), value); + } + + // a call given a count of its own (callCopies, spec 50): retained right before it, and + // given back right after it unless the callee keeps it + if (isCall(user) && !mlir::isa(user) && retainedRightBefore(user, user->getOperands(), value)) + { + auto *next = user->getNextNode(); + auto releaseOp = mlir::dyn_cast_or_null(next); + return isKeptArgument(user, value) || (releaseOp && releaseOp.getReference() == value); + } + if (auto castOp = mlir::dyn_cast(user)) { return mlir::isa(castOp.getType()) && retainedRightBefore(user, user->getOperands(), value); @@ -680,12 +701,28 @@ namespace own_inference // Callees that destroy only the old values of places they overwrite drop the chain as // those stores would here. Not a chain whose places are not all known (a borrowing - // call's result is somewhere under its arguments). - if (mayDrop && !givenClosure && !chain.anyPlace) + // call's result is somewhere under its arguments). A closure the call runs and does not + // keep is one of its callback parameter's family, whose bodies its places count (spec 52) + auto keepsClosure = givenClosure && llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { + return holdsCaptures(operand) && isKeptArgument(drop, operand); + }); + if (mayDrop && !keepsClosure && !chain.anyPlace) { if (auto places = callDropFields(drop)) { - mayDrop = llvm::any_of(*places, [&](auto &place) { return overwritesChain(place.first, place.second, chain); }); + // a captured string assigned frees that string only: a borrow of anything else + // is not it, nor under it (spec 52) + auto value = borrowed ? borrowed : (read ? read->getResult(0) : mlir::Value()); + auto notString = value && mlir::isa(value.getType()); + mayDrop = llvm::any_of(*places, [&](auto &place) { + if (place.first == OWN_CAPTURED_STRING_POSITION) + { + return !notString; + } + + return overwritesChain(place.first, place.second, chain); + }); } } @@ -710,10 +747,23 @@ namespace own_inference { // given once: a copy given twice would have two owners. Not an invoke, whose release after // it would go on the normal path only. - return isCopyableString(value.getType()) && !mlir::isa(call) && + return (isCopyableString(value.getType()) || isHandle(value)) && !mlir::isa(call) && llvm::count(call->getOperands(), value) == 1; } + bool OwnershipInferencePass::storesCopyOf(mlir::Operation *store, mlir::Value value) + { + auto storeOp = mlir::dyn_cast(store); + if (!storeOp || storeOp.getValue() != value || !(isCopyableString(value.getType()) || isHandle(value)) || + !isPlace(storeOp.getReference())) + { + return false; + } + + // a retained one is copyForRetain's: the retain becomes the copy + return llvm::none_of(value.getUsers(), [](mlir::Operation *user) { return mlir::isa(user); }); + } + bool OwnershipInferencePass::checkPlaceRead(mlir::Operation *read, llvm::SetVector &toErase) { auto loadOp = mlir::dyn_cast(read); @@ -793,6 +843,14 @@ namespace own_inference callCopies.push_back({read, kept}); } + // ...and one stored into a place rc does not retain it for, as a yield's record field + // (`if (c) yield v` of a string `v` the generator's state holds): the place takes a copy, + // or a handle a count (spec 50) + if (dryRun && kept && storesCopyOf(kept, result)) + { + callCopies.push_back({read, kept}); + } + if (!kept && isAssignmentOf(letKept, letKeeps)) { reportPlaceBorrowerAssigned(letKept, varName(letKeeps), place); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index c7607bed4..c818e792c 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -765,6 +765,13 @@ namespace own_signature return false; } + // a string stored by a statement that may run again (`a[i] = value` in `fill`'s loop) is + // no one move: each store takes a copy, and the parameter stays a borrow (spec 50) + if (isCopyableString(used.getType()) && mayRunAgain(user)) + { + return false; + } + auto ref = storeOp.getReference(); return storeOp.getValue() == used && (isPlace(ref) || ref.getDefiningOp()); } @@ -1339,7 +1346,14 @@ namespace own_signature else if (auto releaseSlotOp = mlir::dyn_cast(op)) { auto slot = releaseSlotOp.getSlot(); - if (slot.getDefiningOp() || isLoadedCell(slot) || (!isPlace(slot) && touchesHandle(op))) + auto slotType = mlir::dyn_cast(slot.getType()); + if (isLoadedCell(slot) && slotType && isCopyableString(slotType.getElementType())) + { + // a captured string assigned (`each += v` in a callback): it frees that string only + drops.add({OWN_CAPTURED_STRING_POSITION, slot.getType()}); + } + else if (slot.getDefiningOp() || isLoadedCell(slot) || + (!isPlace(slot) && touchesHandle(op) && !isCountedReadSlot(slot))) { drops.any = true; } @@ -1547,6 +1561,35 @@ namespace own_signature }); } + bool OwnershipSignaturePass::isCountedReadSlot(mlir::Value slot) + { + // `for (const v of this)` over handles: the local is declared from a read, which rc counts + // (`ts.RetainSlot`), and is never assigned. Where the read came from still holds a count of + // its own while the local lives - a store into it, a pop, a call that does either is a + // drop of its own - so the local's release is never the last (spec 51). + auto varOp = slot.getDefiningOp(); + if (!varOp || varOp.getCaptured().value_or(false) || !varOp.getInitializer() || + !varOp.getInitializer().getDefiningOp()) + { + return false; + } + + auto counted = false; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + counted = true; + } + else if (!mlir::isa(user)) + { + return false; + } + } + + return counted; + } + bool OwnershipSignaturePass::dropsInBody(mlir_ts::FuncOp funcOp) { auto drops = false; @@ -1570,7 +1613,7 @@ namespace own_signature // is its declaring function's auto slot = releaseSlotOp.getSlot(); drops = slot.getDefiningOp() || (isPlace(slot) && reachesOutside(slot, funcOp)) || - isLoadedCell(slot) || (!isPlace(slot) && touchesHandle(op)); + isLoadedCell(slot) || (!isPlace(slot) && touchesHandle(op) && !isCountedReadSlot(slot)); } else if (mlir::isa(op)) { diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 0a25bd580..a5087e065 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -1090,6 +1090,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper --emit=mlir-affine --no-default-lib -mm=${shared_verify_mm} --verify-ownership "${PROJECT_SOURCE_DIR}/test/tester/own/${shared_test}.ts") @@ -4671,7 +4673,8 @@ set(own_error_cases "own_err_callback_forwarded_drops|:15:17: error: 'this value' borrows an element but is used here after it may be released or overwritten" "own_err_callback_forwarded_keeps|:32:18: error: '[^']*' keeps '#0' for good, but -mm=own cannot make every caller make it immortal: the function is called through a value" "own_err_optional_record_field_kept|:15:12: error: '[^']*' borrows the result of 'findIn' and cannot be stored, returned or captured" - "own_err_generator_take_used_after|:14:9: error: '[^']*' borrows a field and cannot be stored, returned or captured") + "own_err_generator_take_used_after|:14:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_captured_string_drops|:20:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_captured_string_callback.ts b/tslang/test/tester/own/own_captured_string_callback.ts new file mode 100644 index 000000000..62d66f3d2 --- /dev/null +++ b/tslang/test/tester/own/own_captured_string_callback.ts @@ -0,0 +1,53 @@ +// A callback that assigns a captured string (`each += v`) frees only the string the variable held +// (spec 52): a call that runs it, given the array a field holds, drops nothing that borrow reads. +// Each step churns, so an array freed too early reads garbage; every model runs it. +class C { + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) junk.push(new C(999)); +} + +function label(i: number) { + return "s" + i; +} + +class Names { + constructor(private data: string[] = []) {} + + forEach(func: (v: string) => void) { + this.data.forEach(func); + } + + count(func: (v: string) => boolean) { + let n = 0; + for (const v of this.data) if (func(v)) n++; + return n; + } +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + const names = new Names([label(1), label(22), label(333)]); + let each = ""; + names.forEach((v) => { + each += v; + }); + churn(); + if (each == "s1s22s333") ok++; + + let seen = ""; + const n = names.count((v) => { + seen = v; + return v.length > 2; + }); + churn(); + if (n == 2 && seen == "s333") ok++; + } + + assert(ok == 300 * 2, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_captured_string_drops.ts b/tslang/test/tester/own/own_err_captured_string_drops.ts new file mode 100644 index 000000000..5dc873168 --- /dev/null +++ b/tslang/test/tester/own/own_err_captured_string_drops.ts @@ -0,0 +1,24 @@ +// A callback that assigns a captured string frees the string the variable held; a string borrowed +// across a call that may run it may be that string (spec 52). Not a borrow of anything else: the +// array walked is not a string, nor under one. +class Holder { + constructor(public name: string, private data: string[] = []) {} + + each(f: (v: string) => void) { + this.data.forEach(f); + } +} + +function label(i: number) { + return "s" + i; +} + +function main() { + const h = new Holder(label(1), [label(2)]); + let last = label(0); + const name = h.name; + h.each((v) => { + last = v + name; + }); + print(name, last); +} diff --git a/tslang/test/tester/own/own_shared_counted_reads.ts b/tslang/test/tester/own/own_shared_counted_reads.ts new file mode 100644 index 000000000..d8daec485 --- /dev/null +++ b/tslang/test/tester/own/own_shared_counted_reads.ts @@ -0,0 +1,77 @@ +// An array of handles, walked by functions given a callback, through a class that holds the +// array in a field. A `for...of` local over handles is a count of its own, and its release is +// never the last (spec 51): the array still holds one, so the walk drops nothing the caller +// borrows. A handle given to a callback gets a count of its own for the call, and one yielded on +// some paths only (`if (f(v)) yield v`) goes into the record `next()` returns with one (spec 50). +// A handle spliced in is counted, as one pushed is. Each step churns, so a payload freed too early +// reads garbage; every model runs it. +class Item { + constructor(public name: string) {} + + toString() { + return this.name; + } +} + +function churn() { + const junk: Item[] = []; + for (let i = 0; i < 64; i++) junk.push(new Item("junk")); +} + +function every(a: Shared[], f: (v: Shared) => boolean) { + for (const v of a) if (!f(v)) return false; + return true; +} + +function* matching(a: Shared[], f: (v: Shared) => boolean) { + for (const v of a) if (f(v)) yield v; +} + +class Items { + constructor(private data: Shared[] = []) {} + + push(v: Shared) { + this.data.push(v); + } + + insert(at: int, v: Shared) { + this.data.splice(at, 0, v); + } + + all(f: (v: Shared) => boolean) { + return every(this.data, f); + } + + join() { + let s = ""; + for (const v of this.data) s += v.value.name; + return s; + } + + @borrows("this", "f") + matching(f: (v: Shared) => boolean) { + return matching(this.data, f); + } +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + const items = new Items(); + items.push(new Shared(new Item("a"))); + items.push(new Shared(new Item("bb"))); + items.insert(1, new Shared(new Item("c"))); + churn(); + if (items.join() == "acbb") ok++; + if (items.all((v) => v.value.name.length > 0)) ok++; + churn(); + + const kept: Shared[] = []; + for (const v of items.matching((v) => v.value.name.length == 1)) kept.push(v); + churn(); + if (kept.length == 2 && kept[1].value.name == "c") ok++; + } + + assert(ok == 300 * 3, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_string_copies.ts b/tslang/test/tester/own/own_string_copies.ts new file mode 100644 index 000000000..2861b42c7 --- /dev/null +++ b/tslang/test/tester/own/own_string_copies.ts @@ -0,0 +1,61 @@ +// A string stored by a statement that runs again - into each element in a loop, as `fill` does - +// is no one move of the parameter: each store takes a copy, and the parameter stays a borrow (spec +// 50). A string a generator's state holds, yielded on some paths only (`if (c) yield v`), goes into +// the record `next()` returns as a copy of its own. Each step churns, so a string freed too early +// reads garbage; every model runs it. +class C { + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) junk.push(new C(999)); +} + +function label(i: number) { + return "s" + i; +} + +function fillAll(a: string[], value: string) { + for (let i = 0; i < a.length; i++) a[i] = value; +} + +function* longer(a: string[], n: int) { + for (const v of a) if (v.length > n) yield v; +} + +class Names { + constructor(private data: string[] = []) {} + + fill(value: string) { + fillAll(this.data, value); + } + + at(i: int) { + return this.data[i]; + } +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + const a = [label(1), label(2), label(3)]; + const v = label(i); + fillAll(a, v); + churn(); + if (a[0] == v && a[2] == v) ok++; + + const n = new Names([label(1), label(2)]); + n.fill(label(7)); + churn(); + if (n.at(1) == "s7") ok++; + + const kept: string[] = []; + for (const s of longer([label(1), label(22), label(333)], 2)) kept.push(s); + churn(); + if (kept.length == 2 && kept[1] == "s333") ok++; + } + + assert(ok == 300 * 3, "ok"); + print("done."); +} From 5ef5fb69e9a862191a7cd25a89748297c23e4823 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 23:50:05 +0000 Subject: [PATCH 2/3] -mm=own: callbacks tested with &&, captured, named again; specs 50-54 - `f && f(x)` of an optional function is `undefined | T` in every model: a function is never falsy, so the false branch is `undefined`, not the function (spec 53). It used to fail to compile. - A slot whose type owns no block (a captured number assigned in a callback) releases nothing: no drop (spec 52). - A callback parameter captured by a closure that only calls it (`a.forEach((v) => f(v))`) keeps its family: the closure's reads of the cell out of its box resolve through the parameter's family, whether the closure is bound or, inlined, called by name (spec 54). - Spec sections 50-54, and the user doc's limits. Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 12 +- .../2026-09-24-own-memory-model-design.md | 102 ++++++++++ tslang/lib/TypeScript/MLIRGenImpl.h | 6 +- tslang/lib/TypeScript/OwnershipFacts.h | 8 + .../lib/TypeScript/OwnershipInferencePass.cpp | 3 +- .../OwnershipSignatureCallbacks.cpp | 192 +++++++++++++++++- .../lib/TypeScript/OwnershipSignaturePass.cpp | 7 +- .../lib/TypeScript/OwnershipSignaturePass.h | 19 +- tslang/test/tester/CMakeLists.txt | 7 +- .../test/tester/own/own_callback_and_call.ts | 45 ++++ .../test/tester/own/own_callback_captured.ts | 88 ++++++++ .../own/own_err_callback_captured_drops.ts | 30 +++ 12 files changed, 496 insertions(+), 23 deletions(-) create mode 100644 tslang/test/tester/own/own_callback_and_call.ts create mode 100644 tslang/test/tester/own/own_callback_captured.ts create mode 100644 tslang/test/tester/own/own_err_callback_captured_drops.ts diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index 23ed8ea9d..397a2f675 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -746,8 +746,11 @@ a reference gives it back on every path out of a function. elements with `[a[i], a[j]] = [a[j], a[i]]`, which moves nothing; swap two elements or two fields the same way in your own code. A swap through a local (`let t = a[i]; a[i] = a[j]; a[j] = t`) gives an object in `t` a second owner, which is an error. A callback your function only calls, - tests or hands on to another function (as `sort` hands its comparison to a helper) is known by - the functions its callers pass; one stored, captured or returned is not. `copyWithin` works on + tests (`compare && compare(a, b)`), hands on to another function (as `sort` hands its comparison + to a helper), names again (`const g = f`) or captures in a closure that calls it (`a.forEach((v) + => f(v))`) is known by the functions its callers pass; one stored in a field, returned, or + captured in a closure that is itself kept in a local is not. A callback may assign the numbers + and strings it captures (`total += v.n`). `copyWithin` works on numbers and strings (a string is copied where it moves); over class instances it would leave one object in two elements, an error by design. - Iterator helpers work, the lazy ones (`map`, `filter`, `take`, `drop`) taking their source over @@ -763,8 +766,9 @@ a reference gives it back on every path out of a function. - `reduce` and `reduceRight` over an array of arrays or objects are errors: with no initial value they start from an element, and may return it as it is, a second owner of it. - The `Array` class holds values, strings or `Shared` handles, as `Map` does: `new - Array()` works, and `new Array()` is a compile error that points at `Shared`. - `Array` and `Array>` do not compile yet. + Array()`, `new Array()` and `new Array>()` work, and `new + Array()` is a compile error that points at `Shared`. A string stored again and again + (`fill`) is copied each time. - A parameter with a default (`constructor(private items: T[] = [])`) is kept like any other parameter, whether the caller passes it or not. - `Shared` cycles are never freed. diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index b7b79b491..9acd5bf98 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -4907,3 +4907,105 @@ Not here: `array_reduceRight` over an array of arrays, which may start from an e it as it is, a second owner, is marked "not own". `Array` (`fill` keeps a defaulted string parameter in a loop) and `Array>` (the class's methods hand callbacks on through a vtable, whose families stay open) still do not compile. + +## 50. Strings copied where a store runs again, or where nothing retains them, 2026-10-27 + +`Array` (the class) did not compile: `fill` stores its `value` parameter into each element in +a loop. A store into a place kept the parameter (`keeps`), the callers moved it in, and the second +store was `'value' is moved inside a loop but was made outside it`. And a generator that yields on +some paths only (`for (const v of this) if (func(v)) yield v` in `filter2`) reads `v` out of its +state's field into the record `next()` returns; the field is read for the test too, so it is no +field that only yields (§48), and the store into the record was `borrows a field and cannot be +stored`. + +- **A store that may run again** (`mayRunAgain`: in a loop) of a string parameter is no keep: the + parameter stays a borrow, and each store takes a copy, as `copyForRetain` makes of rc's retain + for it. +- **A store into a place rc retains nothing for** (a field of the record a yield builds) of a + string or a handle read out of a place (`storesCopyOf`): the place takes a `ts.StringCopy`, or a + handle a count of its own (`ts.Retain`), made in the dry run's `callCopies` list. +- **A handle given to a call that may release it** (a callback, through `copiesInto`): the call gets + a count of its own, `ts.Retain` right before and `ts.Release` right after, unless it keeps it. + `isCountedHandleReceiver` takes such a call, and `handleReadUses` the release after it, for a + counted use of the read (§23.3). + +`own_string_copies` fills arrays with a string parameter, directly and through a class, and keeps +the strings a generator yields on some paths, churning between steps, under own, rc, gc and none. + +## 51. A walk over handles drops nothing, 2026-10-27 + +`Array>` did not compile: every method that hands `this.data` to a function that walks it +(`join`, `every`, `some`, `find`, ...) was `borrows a field but is used here after it may be +released or overwritten`. A `for (const v of this)` over handles makes `v` a counted copy, and its +`ts.ReleaseSlot` touches a handle: `dropsInBody` took the walk for one that may destroy the last +reference to a payload, and with it anything. + +- **A counted read local** (`isCountedReadSlot`): declared from a read (`ts.Load`), retained by rc + (`ts.RetainSlot`), never assigned. Where the read came from holds a count of its own while the + local lives - a store into it, a pop, a call that does either is a drop of its own - so the + local's release is never the last. Neither `dropsInBody` nor `fieldDropsInBody` counts it. +- **`ts.ArraySplice`'s items** are counted receivers of a handle read (`isCountedHandleReceiver`), as + `push`'s and `unshift`'s are. + +`own_shared_counted_reads` walks an array of handles a class holds, by a function given a callback, +by a `for...of`, by a generator that yields some, and splices one in, churning between steps, under +own, rc, gc and none. DefaultLib: `array_class_shared` and `array_class_string` compile and run under +own. + +## 52. A callback that assigns a captured number or string, 2026-10-27 + +`names.forEach((v) => { each += v; })`, with `forEach` handing the array a field holds on, was +`borrows a field but is used here after it may be released or overwritten`: an assignment through a +captured variable's cell, in the closure's body, was a drop of anything (`fieldDropsInBody`), and a +call given a closure was judged by none of its callees' place drops. + +- **A cell that owns no block** (`t += v.n`, a number): its `ts.ReleaseSlot` destroys nothing + (`releasesNothing`). Not a drop in `dropsInBody`, `fieldDropsInBody`, or the inference pass's. +- **A cell that holds a string** (`each += v`): its assignment frees that string only, and a string + owns nothing else. A place drop of its own (`OWN_CAPTURED_STRING_POSITION`): it drops a borrow that + may be a string, and no other - not an array, a class instance, a tuple, an object or an interface. +- **A call given a closure it does not keep** is judged by its callees' place drops too: the closure + is a member of the callback parameter's family, whose bodies those drops count (§33, §43). A call + that keeps the closure is not. + +`own_captured_string_callback` builds strings and counts through callbacks a class's methods give +on with the array a field holds, churning between steps, under own, rc, gc and none; own emits no +counting. Negative: `own_err_captured_string_drops`, a string read out of a field and borrowed +across a call that runs a callback assigning a captured string. + +## 53. `f && f(x)` of an optional function is `undefined | T`, 2026-10-27 + +`compare && compare(a, b)` with `compare?: (x, y) => number` failed in every model: the `&&` was the +union of the right side and the left one's type without its optional (`invalid cast from (p0, p1) => +s32 to s32`), and under own a `boolean | function` union holds the callback (§43). A function is +never falsy: an optional one is false only when it is not there, so the false branch is `undefined`, +as in TypeScript (`mlirGenAndOrLogic`). + +`own_callback_and_call` sorts class instances through `compare && compare(a[j], a[j + 1])`, with a +comparison given and with none, churning between steps, under own, rc, gc and none; own emits no +counting. + +## 54. A callback parameter captured by a closure that calls it, 2026-10-27 + +`a.forEach((v) => f(v))` captured `f`, which kept its family open (§43): a call through it was one +that may destroy anything, and so was every call that gave the closure on. + +- **The parameter's cell** (`onlyCalledParam`): a captured parameter has a cell declared from it, + never assigned. Each of its uses is a read that is only called or given on (as a slot's is), the + cell's own `ts.RetainCell` and `ts.ReleaseCell`, or a store of it into a field of a closure's + capture box. The closure (`closureFunctionOf`) is bound only to boxes holding that cell in that + field, or called by name with one (the built-in `forEach`, inlined, calls it so), and its body + reads the field only to call what the cell holds, or give it on. +- **The calls in the closure** (`resolveCallback`): a read of the cell out of the closure's box + (`ts.Load(ts.Load(ts.PropertyRef(box, i)))`) is the parameter's (`capturedCallbacks`), and so is a + read of the cell where it is declared. Both resolve through the parameter's family. +- With §52, `const g = f; g(v)` resolves as well: the local is a view of the parameter's slot. + +`own_callback_captured` sums, finds the largest and walks through callbacks given to a closure that +calls them and to a local that names them, from a class's methods over the array a field holds, +churning between steps, under own, rc, gc and none; own emits no counting. Negative: +`own_err_callback_captured_drops`, whose callback replaces a global array that the walk's element +may be part of. + +Not here: a closure held in a local and called (`const call = (v) => f(v); call(v)`), and a +callback stored into a field or returned, keep their families open. diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index b7329ce57..0426c791e 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5713,7 +5713,11 @@ class MLIRGenImpl resultFalse = leftExpressionValue; if (auto optType = dyn_cast(resultFalse.getType())) { - resultFalse = builder.create(location, optType.getElementType(), resultFalse); + // a function is never falsy: an optional one is false only when it is not there, so + // `compare && compare(a, b)` is `undefined | number`, as in TypeScript + resultFalse = mth.isAnyFunctionType(optType.getElementType()) + ? builder.create(location, getUndefinedType()).getResult() + : builder.create(location, optType.getElementType(), resultFalse).getResult(); } } else diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 26d33f127..c0c707e23 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -959,6 +959,14 @@ inline bool ownsHeap(mlir::Value value) return ownsHeap(value.getLoc(), value.getType()); } +// The release of a slot whose type owns no block - a captured number assigned (`t += v.n` in a +// callback) - destroys nothing (spec 52) +inline bool releasesNothing(mlir_ts::ReleaseSlotOp releaseSlotOp) +{ + auto refType = mlir::dyn_cast(releaseSlotOp.getSlot().getType()); + return refType && !ownsHeap(releaseSlotOp.getLoc(), refType.getElementType()); +} + // Holds no block: a number, `null` or `undefined` widened, an empty optional, a string literal. // Also a view of a value whose type owns none: a union's number payload made into another union // (`GetValueFromUnion`, then `CreateUnionInstance`) is a number all the way. A `value_ref` is not diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 599702f71..89919d227 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -239,7 +239,8 @@ namespace own_inference // an overwrite of a field or an element, or an assignment to a captured variable or // a global (spec 29: the global may hold what `this` or a parameter is) auto slot = releaseSlotOp.getSlot(); - if (isPlace(slot) || isCellVariable(slot) || isLoadedCell(slot) || slot.getDefiningOp()) + if (!releasesNothing(releaseSlotOp) && + (isPlace(slot) || isCellVariable(slot) || isLoadedCell(slot) || slot.getDefiningOp())) { drops.push_back(op); } diff --git a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp index f4b2c5b46..9c65a0f85 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp @@ -119,29 +119,168 @@ namespace own_signature return {}; } - bool OwnershipSignaturePass::onlyCalledParam(mlir::BlockArgument argument, llvm::SmallVectorImpl &forwards) + mlir_ts::FuncOp OwnershipSignaturePass::closureFunctionOf(mlir::Value box, mlir::Value cell, int32_t position) + { + auto boundOp = closureOfBox(box); + auto refOp = boundOp ? boundOp.getFunc().getDefiningOp() : mlir_ts::SymbolRefOp(); + auto funcOp = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); + if (!funcOp || funcOp.isDeclaration() || funcOp.getBody().front().getNumArguments() == 0) + { + return {}; + } + + // every box that comes in as its first argument is one of these + auto uses =mlir::SymbolTable::getSymbolUses(funcOp, funcOp->getParentOfType()); + if (!uses) + { + return {}; + } + + // each box it is bound to holds the same cell in the same field + auto holdsCell = [&](mlir::Value other) { + if (!closureOfBox(other)) + { + return false; + } + + auto stored = false; + for (auto *boxUser : other.getUsers()) + { + auto fieldOp = mlir::dyn_cast(boxUser); + if (!fieldOp || static_cast(fieldOp.getPosition()) != position) + { + continue; + } + + for (auto *fieldUser : fieldOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getValue() != cell) + { + return false; + } + + stored = true; + } + } + + return stored; + }; + + for (auto &use : *uses) + { + // called by name with the box (the built-in `forEach`, inlined, calls the closure so) + if (directCallee(use.getUser())) + { + auto args = callArgs(use.getUser()); + if (args.empty() || !holdsCell(args.front())) + { + return {}; + } + + continue; + } + + auto useRef = mlir::dyn_cast(use.getUser()); + if (!useRef || llvm::any_of(useRef->getUsers(), [&](mlir::Operation *refUser) { + auto bound = mlir::dyn_cast(refUser); + return !bound || !holdsCell(bound.getThisVal()); + })) + { + return {}; + } + } + + return funcOp; + } + + bool OwnershipSignaturePass::onlyCalledParam(mlir::BlockArgument argument, llvm::SmallVectorImpl &forwards, + llvm::SmallVectorImpl> &captured) { for (auto *user : argument.getUsers()) { auto varOp = mlir::dyn_cast(user); auto index = -1; - if (!varOp || !isParameterSlot(varOp, index)) + // a captured one: its cell, never assigned, read here or by the closures it is given to + auto cell = varOp && varOp.getCaptured().value_or(false) && !isOwningVariable(varOp) && + varOp.getInitializer() == argument; + if (!varOp || (!cell && !isParameterSlot(varOp, index))) { return false; } - for (auto *slotUser : varOp->getUsers()) + for (auto &slotUse : varOp->getUses()) { - if (mlir::isa(slotUser)) + auto *slotUser = slotUse.getOwner(); + if (mlir::isa(slotUser) || + (cell && mlir::isa(slotUser))) { continue; } - auto loadOp = mlir::dyn_cast(slotUser); - if (!loadOp || !onlyCalledValue(loadOp.getResult(), forwards)) + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + if (!onlyCalledValue(loadOp.getResult(), forwards)) + { + return false; + } + + continue; + } + + // the cell into a field of a closure's capture box: the closure's body reads it out of + // its box (`ts.Load(ts.Load(ts.PropertyRef(box, i)))`) only to call it + auto storeOp = mlir::dyn_cast(slotUser); + auto fieldOp = storeOp && cell && storeOp.getValue() == varOp.getResult() + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto position = fieldOp ? static_cast(fieldOp.getPosition()) : -1; + auto closure = fieldOp ? closureFunctionOf(fieldOp->getOperand(0), varOp.getResult(), position) : mlir_ts::FuncOp(); + if (!closure) { return false; } + + // the same closure met again through its other box + if (llvm::is_contained(captured, std::make_pair(closure.getOperation(), position))) + { + continue; + } + + auto box = closure.getBody().front().getArgument(0); + for (auto *boxUser : box.getUsers()) + { + auto readField = mlir::dyn_cast(boxUser); + if (!readField) + { + return false; + } + + if (readField.getPosition() != static_cast(position)) + { + continue; + } + + for (auto *fieldUser : readField->getUsers()) + { + auto cellRead = mlir::dyn_cast(fieldUser); + if (!cellRead) + { + return false; + } + + for (auto *cellUser : cellRead->getUsers()) + { + auto valueRead = mlir::dyn_cast(cellUser); + if (!valueRead || !onlyCalledValue(valueRead.getResult(), forwards)) + { + return false; + } + } + } + } + + captured.push_back({closure.getOperation(), position}); } } @@ -223,6 +362,7 @@ namespace own_signature void OwnershipSignaturePass::collectCallbackFamilies(mlir::ModuleOp module) { + capturedCallbacks.clear(); auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); if (!uses) { @@ -390,12 +530,18 @@ namespace own_signature for (auto argument : funcOp.getBody().front().getArguments()) { llvm::SmallVector forwards; - if (!isCallbackType(argument.getType()) || !onlyCalledParam(argument, forwards)) + llvm::SmallVector> captured; + if (!isCallbackType(argument.getType()) || !onlyCalledParam(argument, forwards, captured)) { continue; } auto index = static_cast(argument.getArgNumber()); + for (auto field : captured) + { + capturedCallbacks[field] = {funcOp.getOperation(), index}; + } + CallbackFamily family; for (auto *use : forwards) { @@ -575,14 +721,42 @@ namespace own_signature auto varOp = loadOp.getReference().getDefiningOp(); auto funcOp = loadOp->getParentOfType(); auto index = -1; - if (!varOp || !funcOp || !isParameterSlot(varOp, index)) + std::pair key{funcOp.getOperation(), -1}; + if (varOp && funcOp && isParameterSlot(varOp, index)) + { + key.second = index; + } + else if (auto argument = varOp && varOp.getCaptured().value_or(false) && varOp.getInitializer() + ? mlir::dyn_cast(varOp.getInitializer()) + : mlir::BlockArgument(); + argument && argument.getOwner()->isEntryBlock() && argument.getOwner()->getParentOp() == funcOp.getOperation()) + { + // the captured parameter's cell, read where it is declared (spec 54) + key.second = static_cast(argument.getArgNumber()); + } + else if (auto cellRead = loadOp.getReference().getDefiningOp()) + { + // read out of a closure's capture box: the parameter that field holds the cell of + auto fieldOp = cellRead.getReference().getDefiningOp(); + auto box = fieldOp ? mlir::dyn_cast(fieldOp->getOperand(0)) : mlir::BlockArgument(); + auto found = box && box.getArgNumber() == 0 && box.getOwner()->isEntryBlock() + ? capturedCallbacks.find({funcOp.getOperation(), static_cast(fieldOp.getPosition())}) + : capturedCallbacks.end(); + if (found == capturedCallbacks.end()) + { + return; + } + + key = found->second; + } + else { return; } // a closed family with no members: a method no one calls but through its vtable, never - the // call frees nothing - auto found = callbackFamilies.find({funcOp.getOperation(), index}); + auto found = callbackFamilies.find(key); if (found == callbackFamilies.end() || !found->second.closed) { return; diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index c818e792c..a3ba6424d 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1347,6 +1347,11 @@ namespace own_signature { auto slot = releaseSlotOp.getSlot(); auto slotType = mlir::dyn_cast(slot.getType()); + if (releasesNothing(releaseSlotOp)) + { + return; + } + if (isLoadedCell(slot) && slotType && isCopyableString(slotType.getElementType())) { // a captured string assigned (`each += v` in a callback): it frees that string only @@ -1612,7 +1617,7 @@ namespace own_signature // a captured variable assigned through its cell, by a closure's body: the variable // is its declaring function's auto slot = releaseSlotOp.getSlot(); - drops = slot.getDefiningOp() || (isPlace(slot) && reachesOutside(slot, funcOp)) || + drops = !releasesNothing(releaseSlotOp) &&slot.getDefiningOp() || (isPlace(slot) && reachesOutside(slot, funcOp)) || isLoadedCell(slot) || (!isPlace(slot) && touchesHandle(op) && !isCountedReadSlot(slot)); } else if (mlir::isa(op)) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index a5087e065..0992ba9c0 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -828,10 +828,21 @@ class OwnershipSignaturePass : public mlir::PassWrapper &forwards); + // Is every use of this parameter - through its slot, never assigned - the function a call goes + // through, a test of whether an optional one is given, or an argument of a call (`forwards`: it + // gives the parameter on to that call's)? A captured one may also be in one closure's capture + // box, whose body uses it as such (`a.forEach((v) => f(v))`, spec 54): `captured` takes the + // closure's (function, box position) for each. + bool onlyCalledParam(mlir::BlockArgument argument, llvm::SmallVectorImpl &forwards, + llvm::SmallVectorImpl> &captured); + + // (closure function, capture box position) -> the (function, parameter index) whose callback + // parameter that field holds the cell of (spec 54) + llvm::DenseMap, std::pair> capturedCallbacks; + + // The closure function a capture box is bound to, when every box it is bound to holds `cell` in + // field `position`. + mlir_ts::FuncOp closureFunctionOf(mlir::Value box, mlir::Value cell, int32_t position); // The function a caller passes as `value`: its symbol, seen as a function value through casts // between function types and made optional, or a closure over a capture box built of it. Null diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 225fa5748..313d4dd21 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3941,7 +3941,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_generator_this_param own_copy_within_returned_param own_swap_elements own_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array own_generator_take own_rest_array_borrows own_string_copies - own_captured_string_callback) + own_captured_string_callback own_callback_and_call own_callback_captured) 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. @@ -3977,7 +3977,7 @@ foreach(keeps_mm rc gc none) own_copy_within_returned_param own_swap_elements own_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array own_generator_take own_rest_array_borrows own_string_copies - own_captured_string_callback) + own_captured_string_callback own_callback_and_call own_callback_captured) 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() @@ -4674,7 +4674,8 @@ set(own_error_cases "own_err_callback_forwarded_keeps|:32:18: error: '[^']*' keeps '#0' for good, but -mm=own cannot make every caller make it immortal: the function is called through a value" "own_err_optional_record_field_kept|:15:12: error: '[^']*' borrows the result of 'findIn' and cannot be stored, returned or captured" "own_err_generator_take_used_after|:14:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" - "own_err_captured_string_drops|:20:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten") + "own_err_captured_string_drops|:20:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_callback_captured_drops|:14:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_callback_and_call.ts b/tslang/test/tester/own/own_callback_and_call.ts new file mode 100644 index 000000000..042dd10b1 --- /dev/null +++ b/tslang/test/tester/own/own_callback_and_call.ts @@ -0,0 +1,45 @@ +// `compare && compare(a, b)` of an optional callback: a function is never falsy, so the `&&` is +// `undefined | number` (spec 53), and no `boolean | function` union holds the callback. Under own the +// call is one through the parameter, known by the family of functions given for it (spec 43): it +// releases nothing of the two elements it is given. Each step churns; every model runs it. +class Item { + constructor(public n: number) {} +} + +function churn() { + const junk: Item[] = []; + for (let i = 0; i < 64; i++) junk.push(new Item(999)); +} + +function bubble(a: Item[], compare?: (x: Item, y: Item) => number) { + for (let i = 0; i < a.length; i++) { + for (let j = 0; j + 1 < a.length - i; j++) { + const r = compare && compare(a[j], a[j + 1]); + if (r !== undefined && r > 0) { + [a[j], a[j + 1]] = [a[j + 1], a[j]]; + } + } + } +} + +function byN(x: Item, y: Item) { + return x.n - y.n; +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + const a = [new Item(3), new Item(1), new Item(2)]; + bubble(a, byN); + churn(); + if (a[0].n == 1 && a[2].n == 3) ok++; + + const b = [new Item(3), new Item(1)]; + bubble(b); + churn(); + if (b[0].n == 3) ok++; + } + + assert(ok == 300 * 2, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_callback_captured.ts b/tslang/test/tester/own/own_callback_captured.ts new file mode 100644 index 000000000..771f4dd90 --- /dev/null +++ b/tslang/test/tester/own/own_callback_captured.ts @@ -0,0 +1,88 @@ +// A callback parameter captured by a closure that only calls it (`a.forEach((v) => f(v))`), or +// given a second name (`const g = f`), is still known by the family of functions its callers pass +// (spec 54); and a callback that assigns a captured number (`t += v.n`) frees nothing (spec 52). +// A call through either releases nothing of the elements it is given, and the array a field holds +// is not dropped by it. Each step churns, so an element freed too early reads garbage; every model +// runs it. +class Item { + constructor(public n: number) {} +} + +function churn() { + const junk: Item[] = []; + for (let i = 0; i < 64; i++) junk.push(new Item(999)); +} + +function each(a: Item[], f: (v: Item) => void) { + a.forEach((v) => f(v)); +} + +// reads the element again after the call: the call must release nothing of it +function walk(a: Item[], g: (v: Item) => void) { + let s = 0; + for (let i = 0; i < a.length; i++) { + const v = a[i]; + g(v); + s += v.n; + } + + return s; +} + +function eachWalked(a: Item[], f: (v: Item) => void) { + return walk(a, (v) => f(v)); +} + +function eachNamed(a: Item[], f: (v: Item) => void) { + const g = f; + for (const v of a) g(v); +} + +class Bag { + constructor(private data: Item[] = []) {} + + total() { + let t = 0; + each(this.data, (v) => { + t += v.n; + }); + return t; + } + + largest() { + let m = 0; + eachNamed(this.data, (v) => { + if (v.n > m) m = v.n; + }); + return m; + } + + first() { + return this.data[0]; + } + + walked() { + let seen = 0; + const s = eachWalked(this.data, (v) => { + seen++; + }); + return s * 10 + seen; + } +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + const b = new Bag([new Item(1), new Item(5), new Item(2)]); + if (b.total() == 8) ok++; + churn(); + if (b.largest() == 5) ok++; + churn(); + if (b.first().n == 1) ok++; + if (b.walked() == 83) ok++; + churn(); + } + + assert(ok == 300 * 4, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_callback_captured_drops.ts b/tslang/test/tester/own/own_err_callback_captured_drops.ts new file mode 100644 index 000000000..10908415e --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_captured_drops.ts @@ -0,0 +1,30 @@ +// -mm=own rejects: `each` gives its callback to a closure that calls it (spec 54), so the function +// passed to `each` is the one the closure calls - and it replaces a global array, releasing what +// it held. `walk` calls the closure with an element of an array that may be that global, and reads +// the element again after the call. +class Item { + constructor(public n: number) {} +} + +let pool: Item[] = []; + +function walk(a: Item[], g: (v: Item) => void) { + for (let i = 0; i < a.length; i++) { + const v = a[i]; + g(v); + print(v.n); + } +} + +function each(a: Item[], f: (v: Item) => void) { + walk(a, (v) => f(v)); +} + +function main() { + pool = [new Item(7), new Item(8)]; + const mine = [new Item(1)]; + each(mine, (v) => { + pool = []; + }); + print(pool.length); +} From 2b59037f7585a709585f2db34e13143dab874b94 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 28 Oct 2026 00:00:52 +0000 Subject: [PATCH 3/3] own_captured_string_callback: walk through a function of its own Without the default library the built-in forEach is inlined, and the test passed on the compiler without spec 52. Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/own/own_captured_string_callback.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tslang/test/tester/own/own_captured_string_callback.ts b/tslang/test/tester/own/own_captured_string_callback.ts index 62d66f3d2..bcd235c55 100644 --- a/tslang/test/tester/own/own_captured_string_callback.ts +++ b/tslang/test/tester/own/own_captured_string_callback.ts @@ -14,11 +14,16 @@ function label(i: number) { return "s" + i; } +// a function of its own, not the built-in `forEach`, which is inlined without the default library +function forAll(a: string[], func: (v: string) => void) { + for (let i = 0; i < a.length; i++) func(a[i]); +} + class Names { constructor(private data: string[] = []) {} forEach(func: (v: string) => void) { - this.data.forEach(func); + forAll(this.data, func); } count(func: (v: string) => boolean) {