From 7c14ebb4c5510eb6af1c7bc09e92be1028ef0c89 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 18:04:24 +0000 Subject: [PATCH 1/5] -mm=own: copy a string read out of the parameter the result borrows into a place A function that returns `this` has a result that borrows `this`, and spec 22.4 kept every string read out of it from being copied - so `this[t] = this[s]` (copyWithin) and a swap's `let v = this[i]` (reverse) were "borrows an element". The fact is about what is returned: `copyForRetain` now copies such a read stored into an element or a field, and starts a `let` from a copy when the local cannot be returned (`mayBeReturned`). A read returned, directly or through a local, stays the borrow. Spec section 41. Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 3 + .../2026-09-24-own-memory-model-design.md | 34 ++++++++ .../lib/TypeScript/OwnershipInferencePass.cpp | 77 ++++++++++++++++++- .../lib/TypeScript/OwnershipInferencePass.h | 3 + tslang/test/tester/CMakeLists.txt | 8 +- .../own/own_copy_within_returned_param.ts | 70 +++++++++++++++++ 6 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 tslang/test/tester/own/own_copy_within_returned_param.ts diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index 0c000bf3a..db16814b9 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -742,6 +742,9 @@ a reference gives it back on every path out of a function. - Array methods that keep elements of the source in a new array (`filter` over class instances) give each such element a second owner, which is an error by design. Use `Shared` elements, or build the result from copies. +- `sort`, `reverse` and `copyWithin` work on arrays of numbers and strings (a string is copied + where it moves). Over class instances they are an error: a swap reads an element into a local + while its slot is overwritten, which -mm=own cannot follow yet; use `Shared` elements. - Iterator helpers work, the lazy ones (`map`, `filter`, `take`, `drop`) taking their source over as in JavaScript: `const seq = fibonacci().map(f)`. Over an iterator that borrows its container (`s.values()` of a `Map` or a `Set`) a helper is bounded by the container too: 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 708355604..7e98fbe33 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 @@ -1808,6 +1808,9 @@ The error stays where a copy would be wrong: pass told this function's callers it keeps (`__own_params`) or returns a borrow of (`__own_result_borrows`). Facts lost (§15, an exported function) are not such facts. There the callers keep their own, so a copy is right. + A read of the parameter the result borrows that is stored into an element or a field, or that + starts a local which is not returned, is copied all the same (§41): the fact is about what is + returned. - **Not a string** in the sense of §22.1. ### 22.5 What it should accept @@ -4641,3 +4644,34 @@ the statement that made it, churning between steps, under own, rc, gc and none ( DefaultLib: `array_keys`, `array_values` and `array_iterator` pass under own; `iterator(this: T[])` (`[Symbol.iterator]`), which returns `this.values()`, says `@borrows("this")`. Not here: `entries()`, whose `yield [i, this[i]]` puts a borrowed element in a tuple the caller owns. + +## 41. Copies inside a function whose result borrows a parameter, 2026-10-27 + +DefaultLib `array_copyWithin`, `owning_copies`, `array_reverse` and `array_sort`, all over strings or +numbers. + +**`copyWithin` and `reverse`.** Both return `this`, so the signature pass records that the result +borrows `this` (`__own_result_borrows`), and §22.4 kept every string read out of `this` from being +copied: "a fact the callers rely on". A read stored back into an element (`this[t] = this[s]`) then had +no copy, and was "borrows an element and cannot be stored". The fact is about what is returned. So +`copyForRetain` now copies such a read when it goes into an element or a field (a `ts.Store` through +a `ts.ElementRef` or a `ts.PropertyRef`), and starts a `let` from a copy when that local cannot be +what the function returns (`mayBeReturned`: no load of it, nor of a local it is stored into, reaches a +`ts.ReturnInternal`). A read returned - directly, or through a local - stays the borrow. A parameter +the function keeps (`__own_params`) is unchanged. DefaultLib's `reverse` reads the element into a +`let`, as `sort`'s `temp` already did: a `const` borrows the slot the next line overwrites. + +**`sort`.** It assigned its optional `callbackfn` a default comparator. A callback parameter's facts - +what a call of it may release - come from the functions given for it (closed world), and a parameter +that is assigned is one nothing knows: `callbackfn(this[i], this[j])` was a use of the two elements +after a call that may release them. DefaultLib's `sort` now gives a `selectionSort` helper either the +comparator it was given or the default, and never assigns a parameter. No compiler change. + +Not here: `sort` and `reverse` over class instances. A swap holds one element in a local while its +slot is overwritten; for a string the local is a copy, for an object it would be a second owner. That +needs an exchange of two slots the analysis sees as moving nothing. + +`own_copy_within_returned_param`: `this[t] = this[s]` and a swap through a `let` in functions that +return `this`, and a read returned directly and through a local, over strings built at run time, +churning between steps, under own, rc, gc and none; AOT under own, 1000, 200000 and 400000 +iterations peak at 4.0, 4.0 and 4.4 MB (a returned read copied would leak). diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 635281b7a..013099ca5 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -585,6 +585,56 @@ namespace own_inference return param >= 0 && (llvm::is_contained(ownedParams(getFunction()), param) || param == returnsBorrowOf); } + bool OwnershipInferencePass::mayBeReturned(mlir_ts::VariableOp varOp) + { + llvm::SmallVector slots{varOp.getResult()}; + llvm::SmallPtrSet seen{varOp.getResult()}; + while (!slots.empty()) + { + auto slot = slots.pop_back_val(); + for (auto *user : slot.getUsers()) + { + auto loadOp = mlir::dyn_cast(user); + if (!loadOp) + { + continue; + } + + auto returned = false; + forEachUse(loadOp.getResult(), [&](mlir::Operation *loadUser, mlir::Value used) { + if (mlir::isa(loadUser)) + { + returned = true; + return; + } + + mlir::Value other; + if (auto otherVar = mlir::dyn_cast(loadUser)) + { + other = otherVar.getResult(); + } + else if (auto storeOp = mlir::dyn_cast(loadUser); + storeOp && storeOp.getValue() == used && storeOp.getReference().getDefiningOp()) + { + other = storeOp.getReference(); + } + + if (other && seen.insert(other).second) + { + slots.push_back(other); + } + }); + + if (returned) + { + return true; + } + } + } + + return false; + } + bool OwnershipInferencePass::copyForRetain(mlir::Operation *retain) { // `let x = v`: the local starts from a copy @@ -592,7 +642,16 @@ namespace own_inference { auto varOp = retainSlotOp.getSlot().getDefiningOp(); auto init = varOp ? varOp.getInitializer() : mlir::Value(); - if (!init || !isCopyableString(init.getType()) || borrowsKnownParam(init) || isCellVariable(varOp.getResult())) + if (!init || !isCopyableString(init.getType()) || isCellVariable(varOp.getResult())) + { + return false; + } + + // a read of the parameter the result borrows, into a local that is not returned (a swap's + // `let v = this[i]` in a function that returns `this`): a copy, as any read is + if (borrowsKnownParam(init) && + !(borrowedParam(init) == returnsBorrowOf && + !llvm::is_contained(ownedParams(getFunction()), returnsBorrowOf) && !mayBeReturned(varOp))) { return false; } @@ -611,7 +670,7 @@ namespace own_inference return true; } - if (!isCopyableString(value.getType()) || borrowsKnownParam(value)) + if (!isCopyableString(value.getType())) { return false; } @@ -640,6 +699,20 @@ namespace own_inference return false; } + // A read of a parameter this function owns, or of the one its result borrows, stays what it + // is: the borrow it returns, or the parameter's own. Stored into an element or a field + // (`this[t] = this[s]` in a function that returns `this`) it is a copy, as any read is; not + // into a local, which may be what is returned. + auto storeOp = mlir::dyn_cast(taking->getOwner()); + auto intoPlace = storeOp && storeOp.getValue() == taking->get() && + mlir::isa_and_nonnull(storeOp.getReference().getDefiningOp()); + if (borrowsKnownParam(value) && + !(intoPlace && borrowedParam(value) == returnsBorrowOf && + !llvm::is_contained(ownedParams(getFunction()), returnsBorrowOf))) + { + return false; + } + // only a retain in the use's block: a copy made elsewhere (rc's birth retain, after the // producer) has no owner on a path that does not reach the use; rc retains for the use // beside it too diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 80f43c43d..ec4fb2f27 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -172,6 +172,9 @@ class OwnershipInferencePass // without the twinReadAfter fallback: a retain whose value has no use of its own is not copied, // so its error stays. bool copyForRetain(mlir::Operation *retain); + // May what this local holds be what the function returns - read and returned, or handed to + // another local that may be? + bool mayBeReturned(mlir_ts::VariableOp varOp); // A retain of a tuple `ts.CreateTuple` made from borrowed strings and handles (`[s, shared]`): the // strings are copied into it instead, each handle is retained alone, and the retain goes. bool copyIntoTuple(mlir_ts::RetainOp retainOp); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 417995afc..51234eeeb 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3938,7 +3938,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_generator_method_this own_string_copy_call own_param_kept_some_paths own_method_callbacks own_loop_read_again own_length_drops_elements own_tuple_param_kept own_for_of_borrowed_iterator own_generator_keeps own_generator_keeps_bounded - own_generator_this_param) + own_generator_this_param own_copy_within_returned_param) 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. @@ -3966,9 +3966,11 @@ foreach(for_of_mm rc gc none) endforeach() # A generator whose `@keeps` makes its state own its arguments (spec 38), and one given a generator -# that borrows, and one over its `this` parameter; the same programs under every other model. +# that borrows, and one over its `this` parameter; strings copied within a parameter the result borrows; +# the same programs under every other model. foreach(keeps_mm rc gc none) - foreach(keeps_test own_generator_keeps own_generator_keeps_bounded own_generator_this_param) + foreach(keeps_test own_generator_keeps own_generator_keeps_bounded own_generator_this_param + own_copy_within_returned_param) 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() diff --git a/tslang/test/tester/own/own_copy_within_returned_param.ts b/tslang/test/tester/own/own_copy_within_returned_param.ts new file mode 100644 index 000000000..abe102aef --- /dev/null +++ b/tslang/test/tester/own/own_copy_within_returned_param.ts @@ -0,0 +1,70 @@ +// A function whose result borrows its parameter (`return this`) still copies a string it reads out +// of that parameter when it stores it into an element, as any function does (spec 41): +// `this[t] = this[s]`, and a swap's `let v = this[i]`. What it returns stays the borrow - read and +// returned, directly or through a local - so the caller does not free it twice, and a copy of it +// would leak. Every string is built at run time (a literal is immortal and would hide a missing copy), +// and each case churns between steps. +class C { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function made(n: int): string[] { + const r: string[] = []; + for (let i = 0; i < n; i++) r.push("s" + i); + return r; +} + +function copyWithin(this: string[], target: int, start: int, count: int) { + for (let i = 0; i < count; i++) this[target + i] = this[start + i]; + return this; +} + +function swapEnds(this: string[]) { + let v = this[0]; + this[0] = this[this.length - 1]; + this[this.length - 1] = v; + return this; +} + +function first(this: string[]) { + return this[0]; +} + +function firstOrSecond(this: string[]) { + let x = this[0]; + if (this.length > 9) x = this[1]; + return x; +} + +function main() { + let ok = 0; + for (let i = 0; i < 1000; i++) { + const a = made(4); + const same = a.copyWithin(0, 2, 2); + churn(); + if (same[0] == "s2" && a[1] == "s3" && a[2] == "s2" && a[3] == "s3") ok++; + + const b = made(3); + b.swapEnds(); + churn(); + if (b[0] == "s2" && b[2] == "s0") ok++; + + const f = b.first(); + const g = b.firstOrSecond(); + churn(); + if (f == "s2" && g == "s2") ok++; + } + + assert(ok == 1000 * (3), "ok"); + print("done."); +} From 6b12911eb51f106788a94a14cac71b1f2c76e3de Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 18:26:50 +0000 Subject: [PATCH 2/5] -mm=own: `[p, q] = [q, p]` of two elements or fields is an exchange A swap through a local, `let v = a[i]; a[i] = a[j]; a[j] = v`, gives an object element a second owner, so `sort` and `reverse` over class instances did not compile under own. An expression statement `[p, q] = [q, p]` whose two sides name the same two elements or fields of one type is now a `ts.Swap` of the two places: every value keeps one place, so it is emitted with no retain, release or drop in any model, and its lowering loads both before it stores either. The inference treats a swap as a drop of a borrow read out of either place (it is the other place's now, perhaps under another root), and the signature pass as an overwrite of both places for the callers. Spec section 42; docs limits bullet updated. Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 9 +- .../2026-09-24-own-memory-model-design.md | 42 +++++ tslang/include/TypeScript/TypeScriptOps.td | 16 +- tslang/lib/TypeScript/LowerToAffineLoops.cpp | 2 +- tslang/lib/TypeScript/LowerToLLVM.cpp | 22 ++- tslang/lib/TypeScript/MLIRGenImpl.h | 176 ++++++++++++++++++ tslang/lib/TypeScript/MLIRGenStatements.cpp | 13 ++ tslang/lib/TypeScript/OwnershipFacts.h | 13 ++ .../lib/TypeScript/OwnershipInferencePass.cpp | 5 + .../OwnershipInferencePlaceReads.cpp | 9 + .../lib/TypeScript/OwnershipSignaturePass.cpp | 16 ++ tslang/test/tester/CMakeLists.txt | 10 +- .../own/own_err_swap_borrow_used_after.ts | 12 ++ .../own_err_swap_call_borrow_used_after.ts | 16 ++ tslang/test/tester/own/own_swap_elements.ts | 130 +++++++++++++ 15 files changed, 481 insertions(+), 10 deletions(-) create mode 100644 tslang/test/tester/own/own_err_swap_borrow_used_after.ts create mode 100644 tslang/test/tester/own/own_err_swap_call_borrow_used_after.ts create mode 100644 tslang/test/tester/own/own_swap_elements.ts diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index db16814b9..3087f5c36 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -742,9 +742,12 @@ a reference gives it back on every path out of a function. - Array methods that keep elements of the source in a new array (`filter` over class instances) give each such element a second owner, which is an error by design. Use `Shared` elements, or build the result from copies. -- `sort`, `reverse` and `copyWithin` work on arrays of numbers and strings (a string is copied - where it moves). Over class instances they are an error: a swap reads an element into a local - while its slot is overwritten, which -mm=own cannot follow yet; use `Shared` elements. +- `sort` and `reverse` work on arrays of numbers, strings and class instances. They exchange + 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. `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 as in JavaScript: `const seq = fibonacci().map(f)`. Over an iterator that borrows its container (`s.values()` of a `Map` or a `Set`) a helper is bounded by the container too: 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 7e98fbe33..a4c34a9eb 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 @@ -4675,3 +4675,45 @@ needs an exchange of two slots the analysis sees as moving nothing. return `this`, and a read returned directly and through a local, over strings built at run time, churning between steps, under own, rc, gc and none; AOT under own, 1000, 200000 and 400000 iterations peak at 4.0, 4.0 and 4.4 MB (a returned read copied would leak). + +## 42. An exchange of two places (`[p, q] = [q, p]`), 2026-10-27 + +`sort` and `reverse` over class instances (§41, not there). A swap through a local, `let v = a[i]; +a[i] = a[j]; a[j] = v`, holds one element in `v` while its slot is overwritten: for a string `v` is +a copy, for an object a second owner, and the reads stored back were "borrows an element and cannot +be stored". Recognising the three stores in the inference would mean proving, after the affine +lowering and CSE, that nothing between them reads, writes or releases either place. JavaScript has +a statement for the exchange, and MLIRGen now gives it an op of its own. + +- **`ts.Swap %first, %second`.** Two references of one type; each place takes the other's value. + Every value keeps exactly one place, so it is emitted with no `ts.Retain`, `ts.ReleaseSlot` or + drop in any model, and rc's counts are what they were. Both operands read and write memory, so + nothing folds or hoists it. The LLVM lowering loads both values before it stores either: a + place swapped with itself (`i == j`) keeps its value. +- **MLIRGen.** An expression statement `[p, q] = [q, p]` (`discardedAssignments`: the array the + assignment would give as its value is never built) whose two sides name the same two places + (`isSamePlaceExpression`: names, `this`, numbers, members and elements of those, `+ - *` and + unary `-` of those - nothing that runs code, so evaluating each target once is evaluating it + twice) and whose targets are each an element or a field of one type (`swappablePlace`: a + `ts.Load` through a `ts.ElementRef` or a `ts.PropertyRef`) is a `ts.Swap`. Anything else - a + local, a global, a captured variable, an accessor, an interface's optional field, places of two + types, the assignment's value used - takes the path it took before. +- **The inference.** A swap is one of the function's drops: it frees nothing, but a borrow read + out of either place reads a block the other place holds now, under another root perhaps, which + may end first (`[a[0], b[0]] = [b[0], a[0]]`, then `b` released). `dropsChain` drops a chain + through either place as an overwrite of it would (`overwritesChain`, an element by its type, + a field by its position). +- **The signature pass.** A swap through a place that reaches outside the function is an + overwrite of both places for its callers: `dropsInBody`, and `fieldDropsInBody` names the two. + Without it a function that only swaps would be `__own_no_drops`, and a caller's borrow of an + element would outlive the call that moved it. + +`own_swap_elements` reverses and sorts a `C[]` with generic functions written as the default +library's, swaps an element with itself, two fields, and elements of two arrays, and reverses and +sorts strings built at run time, churning between steps, under own, rc, gc and none; own emits no +counting. Negatives: `own_err_swap_borrow_used_after` (an element read before a swap, used after +it) and `own_err_swap_call_borrow_used_after` (the same across a call that swaps). + +DefaultLib: `reverse` and `selectionSort` exchange elements with `[a[i], a[j]] = [a[j], a[i]]`; +under own a string element is no longer copied out and back. Not here: `copyWithin` over class +instances, which would leave one object in two elements - an error by design. diff --git a/tslang/include/TypeScript/TypeScriptOps.td b/tslang/include/TypeScript/TypeScriptOps.td index 5168f5176..37bb7874c 100644 --- a/tslang/include/TypeScript/TypeScriptOps.td +++ b/tslang/include/TypeScript/TypeScriptOps.td @@ -1349,7 +1349,21 @@ def TypeScript_StoreOp : TypeScript_Op<"Store", []> { }]; let arguments = (ins AnyType:$value, Arg:$reference); - + +} + +def TypeScript_SwapOp : TypeScript_Op<"Swap", [AllTypesMatch<["first", "second"]>]> { + let summary = "exchanges the values of two places"; + let description = [{ + `[p, q] = [q, p]` of two elements or fields: each place takes the other's value. Every value + keeps exactly one place, so nothing is retained, released or dropped, in any memory model + (own spec 42). + }]; + + let arguments = (ins + Arg:$first, + Arg:$second + ); } def TypeScript_LoadOp : TypeScript_Op<"Load", []> { diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 70c91d423..e14606be7 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -2932,7 +2932,7 @@ void AddTsAffineLegalOps(ConversionTarget &target) mlir_ts::AddressOfOp, mlir_ts::ArithmeticBinaryOp, mlir_ts::ArithmeticUnaryOp, mlir_ts::AssertOp, mlir_ts::CastOp, mlir_ts::ConstantOp, mlir_ts::ElementRefOp, mlir_ts::PointerOffsetRefOp, mlir_ts::FuncOp, mlir_ts::GlobalOp, mlir_ts::GlobalResultOp, mlir_ts::DefaultOp, mlir_ts::HasValueOp, mlir_ts::ValueOp, mlir_ts::ValueOrDefaultOp, mlir_ts::NullOp, mlir_ts::ParseFloatOp, mlir_ts::ParseIntOp, mlir_ts::IsNaNOp, - mlir_ts::PrintOp, mlir_ts::ConvertFOp, mlir_ts::SizeOfOp, mlir_ts::TypeDescriptorOp, mlir_ts::RetainOp, mlir_ts::ReleaseOp, mlir_ts::RetainSlotOp, mlir_ts::ReleaseSlotOp, mlir_ts::RetainCellOp, mlir_ts::ReleaseCellOp, mlir_ts::StoreOp, mlir_ts::SymbolRefOp, mlir_ts::LengthOfOp, mlir_ts::SetLengthOfOp, + mlir_ts::PrintOp, mlir_ts::ConvertFOp, mlir_ts::SizeOfOp, mlir_ts::TypeDescriptorOp, mlir_ts::RetainOp, mlir_ts::ReleaseOp, mlir_ts::RetainSlotOp, mlir_ts::ReleaseSlotOp, mlir_ts::RetainCellOp, mlir_ts::ReleaseCellOp, mlir_ts::StoreOp, mlir_ts::SwapOp, mlir_ts::SymbolRefOp, mlir_ts::LengthOfOp, mlir_ts::SetLengthOfOp, mlir_ts::StringLengthOp, mlir_ts::StringResizeOp, mlir_ts::StringConcatOp, mlir_ts::StringCopyOp, mlir_ts::SharedNewOp, mlir_ts::SharedValueRefOp, mlir_ts::SharedCountOp, mlir_ts::StringCompareOp, mlir_ts::AnyCompareOp, mlir_ts::LoadOp, mlir_ts::LoadSaveOp, mlir_ts::NewOp, mlir_ts::CreateTupleOp, mlir_ts::DeconstructTupleOp, mlir_ts::CreateArrayOp, mlir_ts::NewEmptyArrayOp, mlir_ts::NewArrayOp, mlir_ts::DeleteOp, mlir_ts::PropertyRefOp, mlir_ts::InsertPropertyOp, diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 3a6182d8f..4a40194db 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -5139,6 +5139,26 @@ struct StoreOpLowering : public TsLlvmPattern } }; +// Both values are read before either place is written, so a place swapped with itself keeps its +// value. The loads and stores are the dialect's own, lowered as any other. +struct SwapOpLowering : public TsLlvmPattern +{ + using TsLlvmPattern::TsLlvmPattern; + + LogicalResult matchAndRewrite(mlir_ts::SwapOp swapOp, Adaptor transformed, + ConversionPatternRewriter &rewriter) const final + { + auto loc = swapOp->getLoc(); + auto valueType = cast(swapOp.getFirst().getType()).getElementType(); + auto first = rewriter.create(loc, valueType, swapOp.getFirst()); + auto second = rewriter.create(loc, valueType, swapOp.getSecond()); + rewriter.create(loc, second, swapOp.getFirst()); + rewriter.create(loc, first, swapOp.getSecond()); + rewriter.eraseOp(swapOp); + return success(); + } +}; + struct ElementRefOpLowering : public TsLlvmPattern { using TsLlvmPattern::TsLlvmPattern; @@ -9151,7 +9171,7 @@ void TypeScriptToLLVMLoweringPass::runOnOperation() PointerOffsetRefOpLowering, LogicalBinaryOpLowering, NullOpLowering, NewOpLowering, CreateTupleOpLowering, DeconstructTupleOpLowering, CreateArrayOpLowering, NewEmptyArrayOpLowering, NewArrayOpLowering, ArrayPushOpLowering, ArrayPopOpLowering, ArrayUnshiftOpLowering, ArrayShiftOpLowering, ArraySpliceOpLowering, ArrayViewOpLowering, DeleteOpLowering, - ParseFloatOpLowering, ParseIntOpLowering, IsNaNOpLowering, PrintOpLowering, ConvertFOpLowering, StoreOpLowering, SizeOfOpLowering, TypeDescriptorOpLowering, RetainOpLowering, ReleaseOpLowering, RetainSlotOpLowering, ReleaseSlotOpLowering, RetainCellOpLowering, ReleaseCellOpLowering, ReleaseContentsOpLowering, MakeImmortalOpLowering, MarkImmortalCellOpLowering, + ParseFloatOpLowering, ParseIntOpLowering, IsNaNOpLowering, PrintOpLowering, ConvertFOpLowering, StoreOpLowering, SwapOpLowering, SizeOfOpLowering, TypeDescriptorOpLowering, RetainOpLowering, ReleaseOpLowering, RetainSlotOpLowering, ReleaseSlotOpLowering, RetainCellOpLowering, ReleaseCellOpLowering, ReleaseContentsOpLowering, MakeImmortalOpLowering, MarkImmortalCellOpLowering, InsertPropertyOpLowering, LengthOfOpLowering, SetLengthOfOpLowering, StringLengthOpLowering, StringResizeOpLowering, StringConcatOpLowering, StringCopyOpLowering, SharedNewOpLowering, SharedValueRefOpLowering, SharedCountOpLowering, StringCompareOpLowering, AnyCompareOpLowering, CharToStringOpLowering, UndefOpLowering, CopyStructOpLowering, MemoryCopyOpLowering, MemoryMoveOpLowering, diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index f703543d9..b7329ce57 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -7019,6 +7019,21 @@ class MLIRGenImpl if (leftExpression == SyntaxKind::ArrayLiteralExpression) { + if (discardedAssignments.contains(binaryExpressionAST.operator->())) + { + auto swapped = false; + if (mlir::failed(mlirGenSwap(location, leftExpression.as(), rightExpression, + swapped, genContext))) + { + return mlir::failure(); + } + + if (swapped) + { + return mlir::success(); + } + } + return mlirGenSaveLogicArray(location, leftExpression.as(), rightExpression, genContext); } @@ -7061,6 +7076,163 @@ class MLIRGenImpl return mlirGenSaveLogicOneItem(location, leftExpressionValue, rightExpressionValue, genContext); } + // The same place named twice: each part is a name, `this`, a number, a member or an element of + // one, or arithmetic of those - nothing that runs code or changes anything, so evaluating it once + // is evaluating it twice. + bool isSamePlaceExpression(Expression left, Expression right) + { + while (left == SyntaxKind::ParenthesizedExpression) + { + left = left.as()->expression; + } + + while (right == SyntaxKind::ParenthesizedExpression) + { + right = right.as()->expression; + } + + if (left == SyntaxKind::Identifier) + { + return right == SyntaxKind::Identifier && + left.as()->escapedText == right.as()->escapedText; + } + + if (left == SyntaxKind::ThisKeyword) + { + return right == SyntaxKind::ThisKeyword; + } + + if (left == SyntaxKind::NumericLiteral) + { + return right == SyntaxKind::NumericLiteral && left.as()->text == right.as()->text; + } + + if (left == SyntaxKind::PropertyAccessExpression) + { + if (right != SyntaxKind::PropertyAccessExpression) + { + return false; + } + + auto leftAccess = left.as(); + auto rightAccess = right.as(); + return !leftAccess->questionDotToken && !rightAccess->questionDotToken && + leftAccess->name == SyntaxKind::Identifier && rightAccess->name == SyntaxKind::Identifier && + leftAccess->name.as()->escapedText == rightAccess->name.as()->escapedText && + isSamePlaceExpression(leftAccess->expression, rightAccess->expression); + } + + if (left == SyntaxKind::ElementAccessExpression) + { + if (right != SyntaxKind::ElementAccessExpression) + { + return false; + } + + auto leftAccess = left.as(); + auto rightAccess = right.as(); + return !leftAccess->questionDotToken && !rightAccess->questionDotToken && + isSamePlaceExpression(leftAccess->expression, rightAccess->expression) && + isSamePlaceExpression(leftAccess->argumentExpression, rightAccess->argumentExpression); + } + + if (left == SyntaxKind::BinaryExpression) + { + if (right != SyntaxKind::BinaryExpression) + { + return false; + } + + auto leftBinary = left.as(); + auto rightBinary = right.as(); + auto op = (SyntaxKind)leftBinary->operatorToken; + return (op == SyntaxKind::PlusToken || op == SyntaxKind::MinusToken || op == SyntaxKind::AsteriskToken) && + op == (SyntaxKind)rightBinary->operatorToken && + isSamePlaceExpression(leftBinary->left, rightBinary->left) && + isSamePlaceExpression(leftBinary->right, rightBinary->right); + } + + if (left == SyntaxKind::PrefixUnaryExpression) + { + if (right != SyntaxKind::PrefixUnaryExpression) + { + return false; + } + + auto leftUnary = left.as(); + auto rightUnary = right.as(); + return leftUnary->_operator == SyntaxKind::MinusToken && rightUnary->_operator == SyntaxKind::MinusToken && + isSamePlaceExpression(leftUnary->operand, rightUnary->operand); + } + + return false; + } + + // The element or field a target names, when it is one a store writes as is: not a local, a + // global or a captured variable, whose slot the ownership inference follows by its stores, nor + // an interface's optional field, whose store sets a flag. + mlir::Value swappablePlace(mlir::Value targetValue) + { + auto loadOp = targetValue.getDefiningOp(); + if (!loadOp || loadOp->hasAttr(ATOMIC_ATTR_NAME) || loadOp->hasAttr(VOLATILE_ATTR_NAME)) + { + return mlir::Value(); + } + + auto reference = loadOp.getReference(); + if (!isa(reference.getType()) || + !isa_and_nonnull(reference.getDefiningOp())) + { + return mlir::Value(); + } + + return reference; + } + + // `[p, q] = [q, p]` of two elements or fields: an exchange, which moves nothing. Built through + // an array literal, each value would be read out of its place (under -mm=own a borrow, which + // may not be stored) and the literal would own a second reference to it. Only a statement's, + // whose value nothing reads: the swap builds no array to give. `swapped` stays false when the + // assignment is not such a swap. + mlir::LogicalResult mlirGenSwap(mlir::Location location, ArrayLiteralExpression arrayLiteralExpression, + Expression rightExpression, bool &swapped, const GenContext &genContext) + { + while (rightExpression == SyntaxKind::ParenthesizedExpression) + { + rightExpression = rightExpression.as()->expression; + } + + if (rightExpression != SyntaxKind::ArrayLiteralExpression) + { + return mlir::success(); + } + + auto &targets = arrayLiteralExpression->elements; + auto &sources = rightExpression.as()->elements; + if (targets.size() != 2 || sources.size() != 2 || !isSamePlaceExpression(targets[0], sources[1]) || + !isSamePlaceExpression(targets[1], sources[0])) + { + return mlir::success(); + } + + auto first = mlirGen(targets[0], genContext); + EXIT_IF_FAILED_OR_NO_VALUE(first) + auto second = mlirGen(targets[1], genContext); + EXIT_IF_FAILED_OR_NO_VALUE(second) + + auto firstPlace = swappablePlace(V(first)); + auto secondPlace = swappablePlace(V(second)); + if (!firstPlace || !secondPlace || firstPlace.getType() != secondPlace.getType()) + { + // the reads are dead: the assignment reads the places again + return mlir::success(); + } + + builder.create(location, firstPlace, secondPlace); + swapped = true; + return mlir::success(); + } + ValueOrLogicalResult mlirGenSaveLogicArray(mlir::Location location, ArrayLiteralExpression arrayLiteralExpression, Expression rightExpression, const GenContext &genContext) { @@ -16037,6 +16209,10 @@ class MLIRGenImpl // SupportedYieldScope. llvm::DenseMap supportedYields; + // The destructuring assignments a statement is made of, whose value nothing reads: such a + // `[p, q] = [q, p]` may be a swap (mlirGenSwap), which builds no array to give as its value. + llvm::DenseSet discardedAssignments; + private: std::string label; // `a: b: for (...)`: the outer labels of a chain that ends in a loop, each mapped to the innermost diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index a55bbb2a0..faaa8db1b 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -448,6 +448,7 @@ namespace mlirgen expression = expression.as()->expression; } + const void *discarded = nullptr; if (expression == SyntaxKind::BinaryExpression) { auto binaryExpression = expression.as(); @@ -456,9 +457,21 @@ namespace mlirgen { supportedYieldScope.mark(binaryExpression->right, true); } + + if (binaryExpression->operatorToken == SyntaxKind::EqualsToken + && binaryExpression->left == SyntaxKind::ArrayLiteralExpression) + { + discarded = expression.operator->(); + discardedAssignments.insert(discarded); + } } auto result = mlirGen(expressionStatementAST->expression, genContext); + if (discarded) + { + discardedAssignments.erase(discarded); + } + EXIT_IF_FAILED(result) return mlir::success(); } diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 6ba35f4d8..787f31d86 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -684,6 +684,19 @@ inline bool sameSlot(mlir::Operation *a, mlir::Operation *b) return globalA && globalB && globalA.getGlobalName() == globalB.getGlobalName(); } +// A place a `ts.Swap` exchanges, as an overwrite of it names it: a field's position, or -1 for an +// element (spec 42). +inline int64_t swappedPosition(mlir::Value place) +{ + int64_t position = -1; + if (auto propertyRefOp = place.getDefiningOp()) + { + position = propertyRefOp.getPosition(); + } + + return position; +} + // The storage an op gives a new value to or releases: a store, `ts.ReleaseSlot`. inline mlir::Operation *slotWritten(mlir::Operation *op) { diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 013099ca5..cb9edec2d 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -250,6 +250,11 @@ namespace own_inference drops.push_back(op); } } + else if (mlir::isa(op)) + { + // an exchange of two places, which moves their values (spec 42) + drops.push_back(op); + } else if (mlir::isa(op) || isCall(op)) diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index c01cd4519..bbe704af0 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -604,6 +604,15 @@ namespace own_inference return true; } + // An exchange frees nothing, but what a borrow of either place reads is the other place's + // now, under another root perhaps, which may end first (spec 42) + if (auto swapOp = mlir::dyn_cast(drop)) + { + return llvm::any_of(swapOp->getOperands(), [&](mlir::Value place) { + return overwritesChain(swappedPosition(place), place.getType(), chain); + }); + } + if (auto releaseSlotOp = mlir::dyn_cast(drop)) { // An assignment to a captured variable destroys its old value, which is reached only diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index d7c2a24c8..e180d1ad9 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1379,6 +1379,17 @@ namespace own_signature drops.add({position, slot.getType()}); } } + else if (auto swapOp = mlir::dyn_cast(op)) + { + // an exchange drops a borrow of either place as an overwrite of it would (spec 42) + for (auto place : swapOp->getOperands()) + { + if (reachesOutside(place, funcOp)) + { + drops.add({swappedPosition(place), place.getType()}); + } + } + } else if (mlir::isa(op)) { if (reachesOutside(op->getOperand(0), funcOp)) @@ -1579,6 +1590,11 @@ namespace own_signature { drops = reachesOutside(op->getOperand(0), funcOp); } + else if (auto swapOp = mlir::dyn_cast(op)) + { + // the caller's borrow of a place it exchanges reads another place's value (spec 42) + drops = reachesOutside(swapOp.getFirst(), funcOp) || reachesOutside(swapOp.getSecond(), funcOp); + } else if (mlir::isa(op)) { // through a handle, it may move the elements another handle's borrow reads (spec 23.3) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 51234eeeb..6ca3b6b6d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3938,7 +3938,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_generator_method_this own_string_copy_call own_param_kept_some_paths own_method_callbacks own_loop_read_again own_length_drops_elements own_tuple_param_kept own_for_of_borrowed_iterator own_generator_keeps own_generator_keeps_bounded - own_generator_this_param own_copy_within_returned_param) + own_generator_this_param own_copy_within_returned_param own_swap_elements) 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. @@ -3967,10 +3967,10 @@ endforeach() # A generator whose `@keeps` makes its state own its arguments (spec 38), and one given a generator # that borrows, and one over its `this` parameter; strings copied within a parameter the result borrows; -# the same programs under every other model. +# elements and fields swapped; the same programs under every other model. foreach(keeps_mm rc gc none) foreach(keeps_test own_generator_keeps own_generator_keeps_bounded own_generator_this_param - own_copy_within_returned_param) + own_copy_within_returned_param own_swap_elements) 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() @@ -4645,7 +4645,9 @@ set(own_error_cases "own_err_generator_keeps_used_after|:15:11: error: '[^']*' is used here after its value was moved" "own_err_generator_keeps_bounded|:14:17: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured" "own_err_generator_keeps_bounded_outlives|:20:11: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" - "own_err_generator_this_param_returned|:11:26: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured") + "own_err_generator_this_param_returned|:11:26: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured" + "own_err_swap_borrow_used_after|:11:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_swap_call_borrow_used_after|:15:11: 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_err_swap_borrow_used_after.ts b/tslang/test/tester/own/own_err_swap_borrow_used_after.ts new file mode 100644 index 000000000..160e7d983 --- /dev/null +++ b/tslang/test/tester/own/own_err_swap_borrow_used_after.ts @@ -0,0 +1,12 @@ +// -mm=own rejects: an element read before a swap of its array's elements, used after it. The swap +// frees nothing, but what was read is held by another place now, which may end first (spec 42). +class C { + constructor(public x: number) {} +} + +function main() { + const a = [new C(1), new C(2)]; + const first = a[0]; + [a[0], a[1]] = [a[1], a[0]]; + print(first.x); +} diff --git a/tslang/test/tester/own/own_err_swap_call_borrow_used_after.ts b/tslang/test/tester/own/own_err_swap_call_borrow_used_after.ts new file mode 100644 index 000000000..39f8811c4 --- /dev/null +++ b/tslang/test/tester/own/own_err_swap_call_borrow_used_after.ts @@ -0,0 +1,16 @@ +// -mm=own rejects: an element read before a call that swaps its array's elements, used after the +// call. The callee frees nothing, but it moves what was read to another place (spec 42). +class C { + constructor(public x: number) {} +} + +function swapFirst(a: C[]) { + [a[0], a[1]] = [a[1], a[0]]; +} + +function main() { + const a = [new C(1), new C(2)]; + const first = a[0]; + swapFirst(a); + print(first.x); +} diff --git a/tslang/test/tester/own/own_swap_elements.ts b/tslang/test/tester/own/own_swap_elements.ts new file mode 100644 index 000000000..88473ad79 --- /dev/null +++ b/tslang/test/tester/own/own_swap_elements.ts @@ -0,0 +1,130 @@ +// `[p, q] = [q, p]` of two elements or fields is an exchange (spec 42): each value keeps exactly one +// place, so nothing is retained, released or dropped, and under -mm=own an array of class instances +// can be reversed and sorted in place - through a local, `let v = a[i]`, the swap would make `v` a +// second owner. Reversed and sorted generically too, over strings built at run time. Each case churns +// between steps with blocks the size of the ones swapped, so a block freed by mistake is reused. +class C { + v: number[] = []; + constructor(public x: number) {} +} + +class Pair { + constructor(public a: C, public b: C) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function made(n: int): C[] { + const r: C[] = []; + for (let i = 0; i < n; i++) { + const c = new C(i); + c.v.push(i); + r.push(c); + } + + return r; +} + +function strings(n: int): string[] { + const r: string[] = []; + for (let i = 0; i < n; i++) r.push("s" + i); + return r; +} + +function reverseAll(this: T[]) { + const shift = this.length - 1; + const size = this.length >> 1; + for (let i = 0; i < size; i++) { + [this[i], this[shift - i]] = [this[shift - i], this[i]]; + } + + return this; +} + +function selectionSort(a: T[], compare: (value1: T, value2: T) => int) { + const len = a.length; + for (let i = 0; i < len - 1; ++i) { + for (let j = i + 1; j < len; ++j) { + if (compare(a[i], a[j]) > 0) { + [a[i], a[j]] = [a[j], a[i]]; + } + } + } +} + +function byX(p: C, q: C): int { + return p.x == q.x ? 0 : p.x > q.x ? 1 : -1; +} + +function byText(p: string, q: string): int { + return p == q ? 0 : p > q ? 1 : -1; +} + +function inOrder(a: C[], first: number, step: number) { + for (let i = 0; i < a.length; i++) { + if (a[i].x != first + i * step || a[i].v[0] != a[i].x) return false; + } + + return true; +} + +function main() { + let ok = 0; + for (let i = 0; i < 1000; i++) { + // reversed in place, an odd and an even length; the result is the array it was given + const a = made(5); + const same = a.reverseAll(); + churn(); + if (inOrder(same, 4, -1)) ok++; + + const b = made(4); + b.reverseAll(); + churn(); + if (inOrder(b, 3, -1)) ok++; + + // sorted with a comparison + const c = made(6); + c.reverseAll(); + [c[1], c[4]] = [c[4], c[1]]; + selectionSort(c, byX); + churn(); + if (inOrder(c, 0, 1)) ok++; + + // a place swapped with itself keeps its value + const k = 2; + [c[k], c[2]] = [c[2], c[k]]; + churn(); + if (inOrder(c, 0, 1)) ok++; + + // two fields + const p = new Pair(new C(1), new C(2)); + [p.a, p.b] = [p.b, p.a]; + churn(); + if (p.a.x == 2 && p.b.x == 1) ok++; + + // elements of two arrays, each of which then frees what it holds + const d = made(2); + const e = made(3); + [d[0], e[2]] = [e[2], d[0]]; + churn(); + if (d[0].x == 2 && e[2].x == 0 && d[1].x == 1) ok++; + + // strings, generically + const s = strings(5); + s.reverseAll(); + churn(); + selectionSort(s, byText); + churn(); + if (s[0] == "s0" && s[2] == "s2" && s[4] == "s4") ok++; + } + + assert(ok == 1000 * 7, "ok"); + print("done."); +} From 2287b57a82379ffdc5f753e5f5bec00a0f811729 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 19:00:12 +0000 Subject: [PATCH 3/5] -mm=own docs: sort over class instances still waits on forwarded callbacks `reverse` over class instances works with the exchange (spec 42); `sort` does not yet, because it gives its optional comparison on to a helper, and a callback given on through a parameter has no callback facts. The limits bullet and spec section 42 say so. Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 14 ++++++----- .../2026-09-24-own-memory-model-design.md | 25 +++++++++++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index 3087f5c36..0a7e3ecb0 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -742,12 +742,14 @@ a reference gives it back on every path out of a function. - Array methods that keep elements of the source in a new array (`filter` over class instances) give each such element a second owner, which is an error by design. Use `Shared` elements, or build the result from copies. -- `sort` and `reverse` work on arrays of numbers, strings and class instances. They exchange - 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. `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. +- `reverse` works on arrays of numbers, strings and class instances, and `sort` on numbers and + strings. They exchange 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. + `sort` over class instances does not compile yet: it hands its comparison on to a helper, and a + callback given on through a parameter is one -mm=own knows nothing about. `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 as in JavaScript: `const seq = fibonacci().map(f)`. Over an iterator that borrows its container (`s.values()` of a `Map` or a `Set`) a helper is bounded by the container too: 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 a4c34a9eb..ef28ec029 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 @@ -4678,12 +4678,13 @@ iterations peak at 4.0, 4.0 and 4.4 MB (a returned read copied would leak). ## 42. An exchange of two places (`[p, q] = [q, p]`), 2026-10-27 -`sort` and `reverse` over class instances (§41, not there). A swap through a local, `let v = a[i]; -a[i] = a[j]; a[j] = v`, holds one element in `v` while its slot is overwritten: for a string `v` is -a copy, for an object a second owner, and the reads stored back were "borrows an element and cannot -be stored". Recognising the three stores in the inference would mean proving, after the affine -lowering and CSE, that nothing between them reads, writes or releases either place. JavaScript has -a statement for the exchange, and MLIRGen now gives it an op of its own. +The swap that `sort` and `reverse` over class instances need (§41, not there). A swap through a +local, `let v = a[i]; a[i] = a[j]; a[j] = v`, holds one element in `v` while its slot is +overwritten: for a string `v` is a copy, for an object a second owner, and the reads stored back +were "borrows an element and cannot be stored". Recognising the three stores in the inference +would mean proving, after the affine lowering and CSE, that nothing between them reads, writes or +releases either place. JavaScript has a statement for the exchange, and MLIRGen now gives it an op +of its own. - **`ts.Swap %first, %second`.** Two references of one type; each place takes the other's value. Every value keeps exactly one place, so it is emitted with no `ts.Retain`, `ts.ReleaseSlot` or @@ -4715,5 +4716,13 @@ counting. Negatives: `own_err_swap_borrow_used_after` (an element read before a it) and `own_err_swap_call_borrow_used_after` (the same across a call that swaps). DefaultLib: `reverse` and `selectionSort` exchange elements with `[a[i], a[j]] = [a[j], a[i]]`; -under own a string element is no longer copied out and back. Not here: `copyWithin` over class -instances, which would leave one object in two elements - an error by design. +under own a string element is no longer copied out and back, and `reverse` over class instances +compiles and runs (`array_reverse_class`). + +Not here: `sort` over class instances. It gives its optional `callbackfn` on to `selectionSort`, +and a callback given on through a parameter is in no closed family (`collectCallbackFamilies` takes +only a function passed by name; `onlyCalledParam` refuses a parameter that is tested or passed on), +so `compare(a[i], a[j])` is a call that may release the two elements it is given - an error the +swap does not reach (`array_sort_class`, `// not own`). The same helper called with a function by +name compiles. And `copyWithin` over class instances, which would leave one object in two +elements - an error by design. From a03557012a2002aa43c1973dae88de3e333e15e5 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 19:30:32 +0000 Subject: [PATCH 4/5] -mm=own: a callback given on through a parameter keeps its facts A callback parameter (a function value, or an optional one) that is only called, tested for being given, unwrapped or passed to one call is in a family whose members are the functions passed for it - including those passed for the caller's own parameter it is given from. A family given on is closed only while the families it is given from and to are. A call of an optional callback once tested resolves through its family too. DefaultLib `sort` over class instances with a comparison compiles under own (spec 43). Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 12 +- .../2026-09-24-own-memory-model-design.md | 55 +++- .../OwnershipSignatureCallbacks.cpp | 263 +++++++++++++++--- .../lib/TypeScript/OwnershipSignaturePass.h | 14 +- .../TypeScript/OwnershipSignatureResolve.cpp | 7 + tslang/test/tester/CMakeLists.txt | 12 +- .../test/tester/own/own_callback_forwarded.ts | 123 ++++++++ .../own/own_err_callback_forwarded_drops.ts | 38 +++ .../own/own_err_callback_forwarded_keeps.ts | 37 +++ 9 files changed, 514 insertions(+), 47 deletions(-) create mode 100644 tslang/test/tester/own/own_callback_forwarded.ts create mode 100644 tslang/test/tester/own/own_err_callback_forwarded_drops.ts create mode 100644 tslang/test/tester/own/own_err_callback_forwarded_keeps.ts diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index 0a7e3ecb0..c04af4b8a 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -742,12 +742,12 @@ a reference gives it back on every path out of a function. - Array methods that keep elements of the source in a new array (`filter` over class instances) give each such element a second owner, which is an error by design. Use `Shared` elements, or build the result from copies. -- `reverse` works on arrays of numbers, strings and class instances, and `sort` on numbers and - strings. They exchange 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. - `sort` over class instances does not compile yet: it hands its comparison on to a helper, and a - callback given on through a parameter is one -mm=own knows nothing about. `copyWithin` works on +- `reverse` and `sort` work on arrays of numbers, strings and class instances. They exchange + 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 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 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 ef28ec029..ca4129cb9 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 @@ -4724,5 +4724,56 @@ and a callback given on through a parameter is in no closed family (`collectCall only a function passed by name; `onlyCalledParam` refuses a parameter that is tested or passed on), so `compare(a[i], a[j])` is a call that may release the two elements it is given - an error the swap does not reach (`array_sort_class`, `// not own`). The same helper called with a function by -name compiles. And `copyWithin` over class instances, which would leave one object in two -elements - an error by design. +name compiles. §43 adds it. And `copyWithin` over class instances, which would leave one object in +two elements - an error by design. + +## 43. A callback given on through a parameter, 2026-10-27 + +`sort` over class instances (§42, not there). DefaultLib's `sort(this: T[], callbackfn?)` tests its +optional comparison and gives it on to `selectionSort(a, compare)`, which calls `compare(a[i], +a[j])`. A call through a parameter knows its callee's facts only from the family of functions its +callers pass (§33), and `collectCallbackFamilies` took a member only from a function passed by +name: `selectionSort`'s `compare` was given `sort`'s parameter, its family was open, and the call +was one that may release the two elements it is given. `sort`'s own `callbackfn` had no family +either, since `onlyCalledParam` refused a parameter that is tested or passed on, and an optional +function was not a function type. + +- **A parameter given on.** A callback parameter's type is a function value or an optional one. + Each read of its slot may be the callee of a call, tested (`ts.HasValue`, a `ts.Cast` to + boolean), unwrapped (`ts.Value`, a cast between function types) and then any of these, or one + argument of a call (`forwards`; never the callee and an argument of one call, never two + arguments). A forward names the parameter of the one function its call reaches (`parameterOf`: + a direct callee, or a method no other overrides, as `passesToClosed` resolved it). +- **Its family.** For each call of the function, the argument for the parameter is a function by + name (`passedFunction`, now through `ts.OptionalValue` as well), nothing (`ts.Undef` or + `ts.OptionalUndef` of an optional type: an optional callback not given, which passes no function), + or the caller's own callback parameter given on (`givenParam`: its slot's read, unwrapped, made + optional or cast) - a source: the functions passed for that parameter are passed for this one. + Anything else opens the family, as before. +- **The fixpoint.** The loop that opens a family whose member escapes now also opens one with a + source or a forward that is not a closed family, and merges a closed source's members (with + their shifts; a member at two shifts opens it) and its passing symbols into it, until nothing + changes. A family given on is so closed only when every family it is given from and to is: the + calls it gives its members to must resolve to the same functions. A function given to itself + (`eachDown(a, i - 1, f)`) is its own source. `passedOnly` follows a member's symbol through + `ts.OptionalValue`, so the function passed to `sort` does not escape where it is made optional. +- **An optional callback called.** A call whose callee is a `ts.Value` of a read of a callback + parameter's slot (`if (compare) { if (compare(a, b) > 0) ... }`) is resolved through the + parameter's family, as one through the read itself (`resolveCallback`). +- The families then meet facts as §33's do: `selectionSort`'s `compare` reaches `sort`'s default + comparison and every function passed to `sort`, and a call through it knows what each releases. + +`own_callback_forwarded` sorts an `Item[]` through an optional comparison given on (and with none +given), gives a callback on twice (`eachTwice` to `each`) and to the function itself, and calls +an optional comparison once tested, churning between steps, under own, rc, gc and none; own emits no counting. Negatives: +`own_err_callback_forwarded_drops` (the function passed to `sortIt` replaces a global array, so +`selectionSort`'s call with two elements is a use of them after they may be released) and +`own_err_callback_forwarded_keeps` (it keeps its argument in a global array, for good, which a call +through a value cannot make immortal). + +DefaultLib: `array_sort_class` compiles and runs under own, and so does `const_array_mutation`, +which sorts numbers with a comparison `sort` gives on. + +Not here: a callback stored, captured or returned is not given on, and keeps its family open. So +is one in `compare && compare(a, b)`: the `&&` is a `boolean | function` union that holds the +callback on the path that does not call it. diff --git a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp index 72b3c2687..f4b2c5b46 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp @@ -8,12 +8,118 @@ namespace own_signature { - bool OwnershipSignaturePass::isFunctionValueType(mlir::Type type) + static bool isFunctionValue(mlir::Type type) { return mlir::isa(type); } - bool OwnershipSignaturePass::onlyCalledParam(mlir::BlockArgument argument) + bool OwnershipSignaturePass::isFunctionValueType(mlir::Type type) + { + return isFunctionValue(type); + } + + // A callback parameter's type: a function value, or an optional one + static bool isCallbackType(mlir::Type type) + { + if (auto optionalType = mlir::dyn_cast(type)) + { + type = optionalType.getElementType(); + } + + return isFunctionValue(type); + } + + // A read of a callback parameter: the function a call goes through, tested for being given + // (`if (f)`), unwrapped, or an argument of a call that gives it on (`forwards`) + static bool onlyCalledValue(mlir::Value value, llvm::SmallVectorImpl &forwards, int depth = 0) + { + if (depth > 8) + { + return false; + } + + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + if (auto castOp = mlir::dyn_cast(user)) + { + if (mlir::isa(castOp.getType())) + { + continue; + } + + if (!isFunctionValue(castOp.getType()) || + !onlyCalledValue(castOp.getResult(), forwards, depth + 1)) + { + return false; + } + + continue; + } + + if (auto valueOp = mlir::dyn_cast(user)) + { + if (!onlyCalledValue(valueOp.getResult(), forwards, depth + 1)) + { + return false; + } + + continue; + } + + // the callee, or one argument: never both, never twice + auto called = calleeValue(user) == use.get(); + if (!isCall(user) || llvm::count(callArgs(user), use.get()) + (called ? 1 : 0) != 1) + { + return false; + } + + if (!called) + { + forwards.push_back(&use); + } + } + + return true; + } + + // The caller's parameter `value` gives on: a read of its slot, unwrapped, made optional or cast + // between function types. Its function, or null. + static mlir_ts::FuncOp givenParam(mlir::Value value, int &index) + { + for (auto depth = 0; value && depth < 8; ++depth) + { + auto *def = value.getDefiningOp(); + if (auto valueOp = mlir::dyn_cast_or_null(def)) + { + value = valueOp.getIn(); + } + else if (auto optionalOp = mlir::dyn_cast_or_null(def)) + { + value = optionalOp.getIn(); + } + else if (auto castOp = mlir::dyn_cast_or_null(def); + castOp && isFunctionValue(castOp.getType())) + { + value = castOp.getIn(); + } + else + { + auto loadOp = mlir::dyn_cast_or_null(def); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + return varOp && isParameterSlot(varOp, index) ? varOp->getParentOfType() : mlir_ts::FuncOp(); + } + } + + return {}; + } + + bool OwnershipSignaturePass::onlyCalledParam(mlir::BlockArgument argument, llvm::SmallVectorImpl &forwards) { for (auto *user : argument.getUsers()) { @@ -32,20 +138,10 @@ namespace own_signature } auto loadOp = mlir::dyn_cast(slotUser); - if (!loadOp) + if (!loadOp || !onlyCalledValue(loadOp.getResult(), forwards)) { return false; } - - for (auto &use : loadOp->getUses()) - { - auto *call = use.getOwner(); - if (!isCall(call) || calleeValue(call) != use.get() || - llvm::is_contained(callArgs(call), use.get())) - { - return false; - } - } } } @@ -54,6 +150,11 @@ namespace own_signature mlir_ts::SymbolRefOp OwnershipSignaturePass::passedFunction(mlir::Value value, int32_t &shift) { + if (auto optionalOp = value.getDefiningOp()) + { + value = optionalOp.getIn(); + } + while (auto castOp = value.getDefiningOp()) { if (!isFunctionValueType(castOp.getIn().getType()) || !isFunctionValueType(castOp.getType())) @@ -96,12 +197,18 @@ namespace own_signature } auto castOp = mlir::dyn_cast(user); + auto optionalOp = mlir::dyn_cast(user); auto boundOp = mlir::dyn_cast(user); if (castOp && isFunctionValueType(castOp.getType()) && passedOnly(castOp.getResult(), passes, depth + 1)) { continue; } + if (optionalOp && passedOnly(optionalOp.getResult(), passes, depth + 1)) + { + continue; + } + if (boundOp && boundOp.getFunc() == use.get() && closureOfBox(boundOp.getThisVal()) == boundOp && passedOnly(boundOp.getResult(), passes, depth + 1)) { @@ -250,6 +357,27 @@ namespace own_signature llvm::all_of(users, [&](mlir::Operation *user) { return callsOf(funcOp, user, calls); }); }; + // The parameter of the one function a call reaches that `use` is the argument for, by its + // operand's position; null when the call reaches no one function or `use` is not an argument. + auto parameterOf = [&](mlir::OpOperand &use) -> std::pair { + auto *call = use.getOwner(); + if (!isCall(call)) + { + return {nullptr, -1}; + } + + auto callee = directCallee(call); + auto calleeOp = callee ? functions.lookup(callee.getValue()) : methodCalled(call); + auto args = callArgs(call); + if (!calleeOp || args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || + use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) + { + return {nullptr, -1}; + } + + return {calleeOp, static_cast(use.getOperandNumber() - args.getBeginOperandIndex())}; + }; + for (auto &entry : functions) { auto funcOp = entry.second; @@ -261,19 +389,50 @@ namespace own_signature for (auto argument : funcOp.getBody().front().getArguments()) { - if (!isFunctionValueType(argument.getType()) || !onlyCalledParam(argument)) + llvm::SmallVector forwards; + if (!isCallbackType(argument.getType()) || !onlyCalledParam(argument, forwards)) { continue; } auto index = static_cast(argument.getArgNumber()); CallbackFamily family; + for (auto *use : forwards) + { + auto target = parameterOf(*use); + family.closed = family.closed && target.first; + family.forwards.push_back(target); + } + for (auto *user : calls) { + if (!family.closed) + { + break; + } + auto args = callArgs(user); + auto arg = static_cast(index) < args.size() ? args[index] : mlir::Value(); auto shift = 0; - auto refOp = static_cast(index) < args.size() ? passedFunction(args[index], shift) - : mlir_ts::SymbolRefOp(); + auto refOp = arg ? passedFunction(arg, shift) : mlir_ts::SymbolRefOp(); + if (!refOp && arg) + { + // an optional callback not given: no function + if (mlir::isa_and_nonnull(arg.getDefiningOp()) && + mlir::isa(arg.getType())) + { + continue; + } + + // the caller's own callback parameter, given on: the functions passed for it + auto position = -1; + if (auto source = givenParam(arg, position); source && source->isAncestor(user)) + { + family.sources.push_back({source, position}); + continue; + } + } + auto member = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) { @@ -303,27 +462,61 @@ namespace own_signature // A member's symbol used anywhere else - a call through another value, a field, a vtable, a // global - reaches callers this cannot see; so does one passed to a family that is not closed. + // A parameter given on is closed only while the parameters it is given from and on to are: + // the functions passed for those are its own, and the calls it gives them to must resolve. // Each family that has such a member is open, until nothing changes. + auto closedFamily = [&](std::pair key) -> CallbackFamily * { + auto found = callbackFamilies.find(key); + return found != callbackFamilies.end() && found->second.closed ? &found->second : nullptr; + }; + auto passesToClosed = [&](mlir::OpOperand &use) { - auto *call = use.getOwner(); - auto callee = directCallee(call); - auto calleeOp = callee ? functions.lookup(callee.getValue()) : (isCall(call) ? methodCalled(call) : mlir_ts::FuncOp()); - if (!calleeOp) - { - return false; - } + auto key = parameterOf(use); + return key.first && closedFamily(key); + }; - // the argument this use is, by its operand's position - auto args = callArgs(call); - if (args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || - use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) + // Takes in the members of the parameters given here: false when one is not closed, or gives + // a member with another shift. + auto mergeSources = [&](CallbackFamily &family, bool &changed) { + for (auto source : family.sources) { - return false; + auto *from = closedFamily(source); + if (!from) + { + return false; + } + + if (from == &family) + { + continue; + } + + for (auto &member : from->members) + { + auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == member.symbol; }); + if (same != family.members.end() && same->shift != member.shift) + { + return false; + } + + if (same == family.members.end()) + { + family.members.push_back(member); + changed = true; + } + } + + for (auto *refOp : from->passing) + { + if (!llvm::is_contained(family.passing, refOp)) + { + family.passing.push_back(refOp); + changed = true; + } + } } - auto index = static_cast(use.getOperandNumber() - args.getBeginOperandIndex()); - auto found = callbackFamilies.find({calleeOp, index}); - return found != callbackFamilies.end() && found->second.closed; + return true; }; for (auto changed = true; changed;) @@ -336,6 +529,14 @@ namespace own_signature continue; } + if (!mergeSources(family, changed) || + llvm::any_of(family.forwards, [&](auto target) { return !closedFamily(target); })) + { + family.closed = false; + changed = true; + continue; + } + for (auto &member : family.members) { auto memberOp = functions.lookup(member.symbol.getValue()); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index bd11d8a03..0a25bd580 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -812,6 +812,10 @@ class OwnershipSignaturePass : public mlir::PassWrapper members; // the ops that name a member's symbol to pass it here llvm::SmallVector passing; + // the parameters of its callers they give on here (their members are this one's too), and the + // parameters of the calls it gives its own on to: each must be closed for this one to be + llvm::SmallVector, 1> sources; + llvm::SmallVector, 1> forwards; bool closed = true; }; @@ -825,15 +829,17 @@ class OwnershipSignaturePass : public mlir::PassWrapper &forwards); // The function a caller passes as `value`: its symbol, seen as a function value through casts - // between function types, or a closure over a capture box built of it. Null for anything else. + // between function types and made optional, or a closure over a capture box built of it. Null + // for anything else. static mlir_ts::SymbolRefOp passedFunction(mlir::Value value, int32_t &shift); // Does the function value `value` go only where `passes` allows - as an argument of a call - and - // through casts between function types, a closure built of it, and that closure's own retains + // through casts between function types, made optional, a closure built of it, and that closure's own retains // and releases? static bool passedOnly(mlir::Value value, llvm::function_ref passes, int depth = 0); diff --git a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp index 6eb51c1fe..5f36e14d8 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp @@ -319,6 +319,13 @@ namespace own_signature resolveCallback(loadOp, callees); return; } + else if (auto valueOp = mlir::dyn_cast_or_null(def); + valueOp && valueOp.getIn().getDefiningOp()) + { + // an optional callback, called once tested + resolveCallback(valueOp.getIn().getDefiningOp(), callees); + return; + } else if (auto boundOp = mlir::dyn_cast_or_null(def)) { // a function-typed field of an interface, called: `ts.CreateBoundFunction( diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 6ca3b6b6d..193160d9d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3938,7 +3938,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_generator_method_this own_string_copy_call own_param_kept_some_paths own_method_callbacks own_loop_read_again own_length_drops_elements own_tuple_param_kept own_for_of_borrowed_iterator own_generator_keeps own_generator_keeps_bounded - own_generator_this_param own_copy_within_returned_param own_swap_elements) + own_generator_this_param own_copy_within_returned_param own_swap_elements + own_callback_forwarded) 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. @@ -3967,10 +3968,11 @@ endforeach() # A generator whose `@keeps` makes its state own its arguments (spec 38), and one given a generator # that borrows, and one over its `this` parameter; strings copied within a parameter the result borrows; -# elements and fields swapped; the same programs under every other model. +# elements and fields swapped; a callback given on through a parameter; the same programs under every +# other model. foreach(keeps_mm rc gc none) foreach(keeps_test own_generator_keeps own_generator_keeps_bounded own_generator_this_param - own_copy_within_returned_param own_swap_elements) + own_copy_within_returned_param own_swap_elements own_callback_forwarded) 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() @@ -4647,7 +4649,9 @@ set(own_error_cases "own_err_generator_keeps_bounded_outlives|:20:11: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_generator_this_param_returned|:11:26: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured" "own_err_swap_borrow_used_after|:11:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" - "own_err_swap_call_borrow_used_after|:15:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten") + "own_err_swap_call_borrow_used_after|:15:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "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") 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_forwarded.ts b/tslang/test/tester/own/own_callback_forwarded.ts new file mode 100644 index 000000000..d9bfe5f0a --- /dev/null +++ b/tslang/test/tester/own/own_callback_forwarded.ts @@ -0,0 +1,123 @@ +// A callback given on through a parameter (spec 43): the functions passed for the parameter it is +// given from are passed for the one it is given to, so a call through it - with two elements of an +// array the callback only reads - knows it releases nothing. An optional callback is tested for +// being given, and one not given passes nothing. Given on once, twice, and to the function itself; +// an optional one called itself. +// Each case churns between steps with blocks the size of the ones read, so a block freed by mistake +// is reused. +class Item { + v: number[] = []; + constructor(public key: int) {} +} + +function churn() { + const junk: Item[] = []; + for (let i = 0; i < 64; i++) { + const c = new Item(999); + c.v.push(99); + junk.push(c); + } +} + +function made(n: int): Item[] { + const r: Item[] = []; + for (let i = n - 1; i >= 0; i--) { + const c = new Item(i); + c.v.push(i); + r.push(c); + } + + return r; +} + +function selectionSort(a: T[], compare: (x: T, y: T) => int) { + const len = a.length; + for (let i = 0; i < len - 1; ++i) { + for (let j = i + 1; j < len; ++j) { + if (compare(a[i], a[j]) > 0) { + [a[i], a[j]] = [a[j], a[i]]; + } + } + } +} + +// as DefaultLib's sort: an optional comparison, given on when given +function sortIt(this: T[], callbackfn?: (x: T, y: T) => int) { + if (callbackfn) { + selectionSort(this, callbackfn); + } else { + selectionSort(this, (a: T, b: T) => 0); + } +} + +function each(a: T[], f: (v: T) => void) { + for (let i = 0; i < a.length; i++) { + f(a[i]); + } +} + +function eachTwice(a: T[], f: (v: T) => void) { + each(a, f); + each(a, f); +} + +function eachDown(a: T[], i: int, f: (v: T) => void) { + if (i < 0) { + return; + } + + f(a[i]); + eachDown(a, i - 1, f); +} + +// an optional callback called itself, once tested +function countDescents(a: T[], compare?: (x: T, y: T) => int) { + let n = 0; + for (let i = 0; i < a.length - 1; ++i) { + if (compare) { + if (compare(a[i], a[i + 1]) > 0) n++; + } + } + + return n; +} + +let total: number = 0; + +function add(c: Item) { + total += c.key + c.v[0]; +} + +function main() { + let ok = 0; + for (let round = 0; round < 1000; round++) { + const a = made(5); + a.sortIt((p: Item, q: Item) => p.key - q.key); + churn(); + if (a[0].key == 0 && a[2].key == 2 && a[4].key == 4 && a[4].v[0] == 4) ok++; + + const b = made(3); + b.sortIt(); + churn(); + if (b[0].key == 2 && b[2].key == 0) ok++; + + total = 0; + const c = made(4); + eachTwice(c, add); + churn(); + if (total == 24 && c[3].v[0] == 0) ok++; + + total = 0; + eachDown(c, 3, add); + churn(); + if (total == 12 && c[0].key == 3) ok++; + + const d = made(4); + const descents = countDescents(d, (p: Item, q: Item) => p.key - q.key) + countDescents(d); + churn(); + if (descents == 3 && d[3].v[0] == 0) ok++; + } + + assert(ok == 1000 * 5, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_callback_forwarded_drops.ts b/tslang/test/tester/own/own_err_callback_forwarded_drops.ts new file mode 100644 index 000000000..50e0bf6a4 --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_forwarded_drops.ts @@ -0,0 +1,38 @@ +// -mm=own rejects: `sortIt` gives its comparison on to `selectionSort` (spec 43), so the function +// passed to `sortIt` is the one `selectionSort` calls - and it replaces a global array, releasing +// what it held. `selectionSort` calls it with two elements of an array that may be that global, and +// reads them again after the call. +class Item { + constructor(public key: int) {} +} + +let pool: Item[] = []; + +function selectionSort(a: T[], compare: (x: T, y: T) => int) { + const len = a.length; + for (let i = 0; i < len - 1; ++i) { + for (let j = i + 1; j < len; ++j) { + if (compare(a[i], a[j]) > 0) { + [a[i], a[j]] = [a[j], a[i]]; + } + } + } +} + +function sortIt(this: T[], callbackfn?: (x: T, y: T) => int) { + if (callbackfn) { + selectionSort(this, callbackfn); + } else { + selectionSort(this, (a: T, b: T) => 0); + } +} + +function main() { + const items = [new Item(3), new Item(1), new Item(2)]; + pool = [new Item(7)]; + items.sortIt((p: Item, q: Item) => { + pool = []; + return p.key - q.key; + }); + print(items[0].key, pool.length); +} diff --git a/tslang/test/tester/own/own_err_callback_forwarded_keeps.ts b/tslang/test/tester/own/own_err_callback_forwarded_keeps.ts new file mode 100644 index 000000000..c1e126467 --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_forwarded_keeps.ts @@ -0,0 +1,37 @@ +// -mm=own rejects: `sortIt` gives its comparison on to `selectionSort` (spec 43), so the function +// passed to `sortIt` is the one `selectionSort` calls - and it keeps its first argument for good, in +// a global array. Its callers must make that argument immortal, and `selectionSort` calls it through +// a value, which cannot. +class Item { + constructor(public key: int) {} +} + +let kept: Item[] = []; + +function selectionSort(a: T[], compare: (x: T, y: T) => int) { + const len = a.length; + for (let i = 0; i < len - 1; ++i) { + for (let j = i + 1; j < len; ++j) { + if (compare(a[i], a[j]) > 0) { + [a[i], a[j]] = [a[j], a[i]]; + } + } + } +} + +function sortIt(this: T[], callbackfn?: (x: T, y: T) => int) { + if (callbackfn) { + selectionSort(this, callbackfn); + } else { + selectionSort(this, (a: T, b: T) => 0); + } +} + +function main() { + const items = [new Item(3), new Item(1), new Item(2)]; + items.sortIt((p: Item, q: Item) => { + kept.push(p); + return p.key - q.key; + }); + print(items[0].key, kept.length); +} From b55fb4869998c303eda6f901b949dd6637bc230b Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 21:10:55 +0000 Subject: [PATCH 5/5] -mm=own: tuple field reads, caught values, defaulted parameters, literals into arrays, --di locals Five borrows or moves that own refused although they were safe: nothing could free what was read, or nothing else held what was moved. - A field read out of a borrowed tuple value borrows what the tuple borrows. console.log of the T | undefined that find returns reads each field with ts.ExtractProperty. A number field keeps nothing (spec 44). - A caught value belongs to the exception until the catch ends. A throw takes what it throws, since own refuses to throw a borrow. So a call frees what is read out of a catch variable only when it is given something derived from it (spec 44). - A parameter with a default (constructor(private d: T[] = [])) is kept as any parameter is. Its slot merges the argument with the default made here, and it counts as the parameter's slot. The argument left out is a ts.Undef of the optional type, which holds no block (spec 45). - An object literal that owns what it holds moves into an array literal's element or a pushed item, as it already moved into a local, the result or a box (spec 46). - Under --di, a borrowing local's ts.DebugVariable keeps nothing (spec 47). DefaultLib now compiles these under own: array_find2/3, catch_library_error, new Array(), regexp_parse_examples, and concat and owning_copies in the debug pass. Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 12 ++- .../2026-09-24-own-memory-model-design.md | 72 +++++++++++++ tslang/lib/TypeScript/OwnershipFacts.h | 102 +++++++++++++++++- .../OwnershipInferenceBorrowers.cpp | 19 ++++ .../OwnershipInferenceKeptParams.cpp | 13 +-- .../TypeScript/OwnershipInferenceMoves.cpp | 8 ++ .../OwnershipInferencePlaceReads.cpp | 7 +- .../lib/TypeScript/OwnershipSignaturePass.cpp | 18 +--- tslang/test/tester/CMakeLists.txt | 23 +++- .../test/tester/own/own_caught_value_call.ts | 27 +++++ .../own/own_err_optional_record_field_kept.ts | 17 +++ .../test/tester/own/own_for_of_nested_di.ts | 19 ++++ .../test/tester/own/own_literal_into_array.ts | 28 +++++ .../tester/own/own_optional_record_read.ts | 58 ++++++++++ .../test/tester/own/own_param_default_kept.ts | 43 ++++++++ 15 files changed, 431 insertions(+), 35 deletions(-) create mode 100644 tslang/test/tester/own/own_caught_value_call.ts create mode 100644 tslang/test/tester/own/own_err_optional_record_field_kept.ts create mode 100644 tslang/test/tester/own/own_for_of_nested_di.ts create mode 100644 tslang/test/tester/own/own_literal_into_array.ts create mode 100644 tslang/test/tester/own/own_optional_record_read.ts create mode 100644 tslang/test/tester/own/own_param_default_kept.ts diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index c04af4b8a..ecb353362 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -754,9 +754,15 @@ a reference gives it back on every path out of a function. as in JavaScript: `const seq = fibonacci().map(f)`. Over an iterator that borrows its container (`s.values()` of a `Map` or a `Set`) a helper is bounded by the container too: `for (const v of s.values().take(2))` works, but the result cannot be returned or kept past `s`. - An array's `values()`, `keys()` and `[Symbol.iterator]()` borrow it in the same way; - `entries()` does not compile yet, its `[i, a[i]]` holding a borrowed element. - A closure that captures, given to two of them, is moved into the first. + An array's `values()`, `keys()`, `entries()` and `[Symbol.iterator]()` borrow it in the same + way. A closure that captures, given to two of them, is moved into the first. A `for...of` over a + generator, or a spread of one (`[...s.matchAll(re)]`), only borrows what each step yields: keeping + a yielded object or array (pushing it into another array) is an error for now. +- 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. +- 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. - There are no lifetime annotations: a borrow that the compiler cannot follow is an error, even when the program would be correct. The fix is usually a move, a fresh value or `Shared`. 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 ca4129cb9..b70c097cc 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 @@ -4777,3 +4777,75 @@ which sorts numbers with a comparison `sort` gives on. Not here: a callback stored, captured or returned is not given on, and keeps its family open. So is one in `compare && compare(a, b)`: the `&&` is a `boolean | function` union that holds the callback on the path that does not call it. + +## 44. A field read out of a borrowed tuple value, and a caught value, 2026-10-27 + +Two borrows that own refused although nothing could free what they read. + +- **A field of a tuple value.** A function returning `T | undefined` of a record (`find`) gives + the caller an optional whose payload is the tuple itself, and printing it reads each field with + `ts.ExtractProperty` out of the `ts.Value` (an in-place read goes through `ts.PropertyRef` + instead). `walkBorrowed` took the extraction for a keep. A field read this way borrows what the + tuple borrows: a number or a boolean keeps nothing, and a field that may point into a block is + walked on as a borrow of the same chain, so storing it is still refused. +- **A caught value.** A catch clause's variable is filled by `ts.SaveCatchVar` with what was thrown + and assigned nowhere else (`isCaughtValue`). A throw takes what it throws, since a borrow cannot + be thrown, so the value is the exception's until the catch ends: no global and no other local + holds it. A chain rooted at a read of it is treated as one this function owns when `dropsChain` + asks whether a call may free it, so only a call given something derived from it may. Before, + `const r = e; r instanceof Error` was refused: the virtual `instanceOf` call, whose + facts own cannot know, was taken to free the exception. + +`own_optional_record_read` prints records `find` returns (found and not), and calls a method and +`instanceof` on a caught value cast to its class, under own, rc, gc and none; own emits no +counting. `own_caught_value_call` (compiled only) gives the cast caught value to a declared +function, which has no facts, and reads it after the call. Negative: `own_err_optional_record_field_kept` pushes a class-typed field of a found +record into another array. A throw of an element, a field or a reassigned global stays refused, so +nothing but the exception holds what a catch reads. DefaultLib: `array_find2`, `array_find3` and `catch_library_error` compile and run under +own. + +## 45. A parameter with a default is kept as any parameter is, 2026-10-27 + +`constructor(private data: T[] = [])` (DefaultLib's `class Array`) was `takes a second +reference`: MLIRGen makes a parameter with a default `HasValue(p) ? Value(p) : `, merged +into the local the body reads, not a local made of the argument itself, so the signature pass found +no slot of the parameter and the store into the field kept a merge it could not follow. + +- **Its slot.** `isParameterSlot` now also takes a local whose initializer is a merge of the + parameter itself (seen through `ts.Value`) and values that are fresh or hold no block - the + default made here. `parameterSlotsOf` finds such a local from the parameter (through the + `ts.Value`'s branch), and `localOwnedParams`, `takenOnEveryPath` and the inference's + `parameterSlot` use it. Everything §33 does for a kept parameter then holds: the callers move the + argument in, a path that stores the slot's value moves it out, and a path that does not releases + the slot, which holds the argument given or the default, both this function's. +- **The argument left out** is a `ts.Undef` of the optional type: `isLiteral` takes it as the empty + optional `ts.OptionalUndef` is, so a caller "moves" nothing. + +`own_param_default_kept` constructs with and without the argument, keeping it on some paths only, +and a defaulted string beside a defaulted array, churning between steps, under own, rc, gc and none; +own emits no counting. A defaulted parameter that is not kept is unchanged: its default is made and +not released, as before. + +## 46. An object literal moved into an array, 2026-10-27 + +`[{ regex: /a/, order: [1, 2, 3] }, ...]` and `a.push({ name: "f" + i, quantity: i })` were `an +object literal that holds a value it owns is moved only into a local or returned as it is`: §27 +lets `recordRetainOf` take the literal's read into the result, an owning local or a box. An element +of an array literal (`ts.CreateArray`) and an item pushed (`ts.ArrayPush`) now take it too: the +array owns the tuple and what it holds, and rc's retain of the read is where the fresh values stored +into its fields move in, each value's own check erasing its release. The read still goes to one +place only. + +`own_literal_into_array` builds arrays of literals holding a fresh string, a new object and an array +literal, by an array literal and by push, churning between steps, under own, rc, gc and none; own +emits no counting, and the array's release is the only one of what the literals hold. DefaultLib: +`regexp_parse_examples` compiles and runs under own. + +## 47. A local's record for the debugger keeps nothing, 2026-10-27 + +Under `--di`, a borrowing local (the array an inner `for...of` walks, read out of the outer one's +element) has a `ts.DebugVariable` of its slot. `collectBorrowerUses` took any use of the slot that is +not a read for a keep: `'.a' borrows an element and cannot be stored`. It now skips it, as +`isBorrow` already skips a `ts.DebugVariable` of a value. `own_for_of_nested_di` (debug build, JIT) +sums the lengths of the strings of a `string[][]`. DefaultLib: `array_concat` and `owning_copies`, +which failed under own in the debug pass only, compile and run. diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 787f31d86..f1a971d8c 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -476,7 +476,9 @@ inline bool isOwningVariable(mlir_ts::VariableOp varOp) // (`u: C | undefined = undefined`), hold no block at all. inline bool isLiteral(mlir::Operation *def) { - if (mlir::isa(def)) + // ...and so does `ts.Undef` of an optional: the argument left out for a parameter with a default + if (mlir::isa(def) || + (mlir::isa(def) && mlir::isa(def->getResult(0).getType()))) { return true; } @@ -1275,6 +1277,33 @@ inline bool isLoadedCell(mlir::Value value) return loadOp && mlir::isa(loadOp.getType()); } +// A read of a catch clause's variable, which ts.SaveCatchVar fills with what was thrown and +// nothing else assigns: the exception's value, not this function's nor any global's. +inline bool isCaughtValue(mlir::Value value) +{ + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || varOp.getInitializer() || varOp.getCaptured().value_or(false)) + { + return false; + } + + auto saved = false; + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + saved = true; + } + else if (!mlir::isa(user)) + { + return false; + } + } + + return saved; +} + // A captured variable's cell, in the function that declared it. inline bool isCellVariable(mlir::Value value) { @@ -1309,6 +1338,35 @@ inline bool isParameterSlot(mlir_ts::VariableOp varOp, int &index) { auto init = varOp.getInitializer(); auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); + // a parameter with a default (`d: T[] = []`): its slot holds the argument given, or the default + // made here when none was - fresh, or holding no block - so it is the parameter's as an + // undefaulted one's is + if (argument && !argument.getOwner()->isEntryBlock()) + { + llvm::SmallVector merged; + mlir::BlockArgument given; + if (!mergedInto(argument, merged)) + { + return false; + } + + // each seen through its views: the argument given is the optional parameter itself + for (auto incoming : merged) + { + auto passed = mlir::dyn_cast(incoming); + if (passed && passed.getOwner()->isEntryBlock() && (!given || given == passed)) + { + given = passed; + } + else if (!isFresh(incoming) && !holdsNoBlock(incoming)) + { + return false; + } + } + + argument = given; + } + if (!argument || !argument.getOwner()->isEntryBlock() || !mlir::isa(argument.getOwner()->getParentOp()) || isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) @@ -1329,6 +1387,48 @@ inline bool isParameterSlot(mlir_ts::VariableOp varOp, int &index) return true; } +// The slots of a function's parameter (isParameterSlot): the local made of it, or, for a parameter +// with a default, the local made of the argument given or the default. +inline llvm::SmallVector parameterSlotsOf(mlir::BlockArgument argument) +{ + llvm::SmallVector slots; + auto add = [&](mlir::Operation *user) { + auto varOp = mlir::dyn_cast(user); + auto index = -1; + if (varOp && isParameterSlot(varOp, index) && index == static_cast(argument.getArgNumber()) && + !llvm::is_contained(slots, varOp)) + { + slots.push_back(varOp); + } + }; + + for (auto *user : argument.getUsers()) + { + add(user); + if (!mlir::isa(user)) + { + continue; + } + + for (auto &use : user->getResult(0).getUses()) + { + auto branchOp = mlir::dyn_cast(use.getOwner()); + auto merged = branchOp ? branchOp.getSuccessorBlockArgument(use.getOperandNumber()) : std::nullopt; + if (!merged) + { + continue; + } + + for (auto *mergedUser : merged->getUsers()) + { + add(mergedUser); + } + } + } + + return slots; +} + // The object a method call is made on: `ts.GetThis` of a method bound to it - by its class, or by an // interface value whose method it is (`i.m`: the object the interface value is a view of, spec 31). // Null otherwise. diff --git a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp index 0b2d8324b..711afaa31 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp @@ -235,6 +235,12 @@ namespace own_inference continue; } + // `--di`'s record of the local for the debugger names it and keeps nothing + if (mlir::isa(user)) + { + continue; + } + auto readOp = mlir::dyn_cast(user); if (!readOp) { @@ -391,6 +397,19 @@ namespace own_inference continue; } + // a field read out of a tuple value (`console.log(found)` of an optional record): + // a number keeps nothing, and a field that holds a block borrows it as the tuple does + if (mlir::isa(user)) + { + auto field = user->getResult(0); + if (mayPointInto(field) && seen.insert(field).second) + { + values.push_back({field, true, pending.kills}); + } + + continue; + } + llvm::SmallVector same; llvm::SmallVector renewedAt; if (passesOn(use, same, &renewedAt)) diff --git a/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp b/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp index 2d7c6c060..96b627877 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp @@ -16,17 +16,8 @@ namespace own_inference return {}; } - for (auto *user : body.front().getArgument(index).getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - if (varOp && isParameterSlot(varOp, position) && position == index) - { - return varOp; - } - } - - return {}; + auto slots = parameterSlotsOf(body.front().getArgument(index)); + return slots.empty() ? mlir_ts::VariableOp() : slots.front(); } bool OwnershipInferencePass::isReleasedParam(mlir::Value slot) diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp index e0f5ae309..4047003c7 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -156,6 +156,14 @@ namespace own_inference return; } + // an element of an array literal (`[{ re: /a/, order: [1, 2] }]`), or an item pushed: the + // array owns what the record held, and rc's retain of the read is where its values move in + auto pushOp = mlir::dyn_cast(user); + if (mlir::isa(user) || (pushOp && llvm::is_contained(pushOp.getItems(), used))) + { + return; + } + // `let a = { item: new C() }`: the read is what an owning local starts with, and rc's // `ts.RetainSlot` of that local is the record's retain if (auto ownerOp = mlir::dyn_cast(user)) diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index bbe704af0..0aa5dc191 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -662,7 +662,12 @@ namespace own_inference // a call given a closure may run it, and its box may hold anything auto givenClosure = llvm::any_of(drop->getOperands(), holdsCaptures); auto mayDrop = true; - if (chain.owned() && !givenClosure) + // A caught value is the exception's until the catch ends: a throw takes what it throws + // (a borrow cannot be thrown), so nothing a call reaches holds it but what it is given + auto caught = llvm::all_of(chain.roots, [&](auto &root) { + return root.second != Chain::NotOwned || isCaughtValue(root.first); + }); + if (caught && !givenClosure) { auto &fromRead = derivedFrom(borrowed ? borrowed : read->getResult(0)); auto sources = rootSources(chain); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index e180d1ad9..c7607bed4 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -828,15 +828,8 @@ namespace own_signature } auto kept = false; - for (auto *user : argument.getUsers()) + for (auto varOp : parameterSlotsOf(argument)) { - auto varOp = mlir::dyn_cast(user); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index)) - { - continue; - } - for (auto *slotUser : varOp.getResult().getUsers()) { if (auto loadOp = mlir::dyn_cast(slotUser)) @@ -892,15 +885,8 @@ namespace own_signature { // the statements that keep the parameter, as decideParam sees a move llvm::SmallVector takers; - for (auto *user : body.front().getArgument(index).getUsers()) + for (auto varOp : parameterSlotsOf(body.front().getArgument(index))) { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - if (!varOp || !isParameterSlot(varOp, position)) - { - continue; - } - for (auto *slotUser : varOp.getResult().getUsers()) { if (auto loadOp = mlir::dyn_cast(slotUser)) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 193160d9d..29e10747c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3939,7 +3939,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_loop_read_again own_length_drops_elements own_tuple_param_kept own_for_of_borrowed_iterator own_generator_keeps own_generator_keeps_bounded own_generator_this_param own_copy_within_returned_param own_swap_elements - own_callback_forwarded) + own_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array) 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. @@ -3972,7 +3972,8 @@ endforeach() # other model. foreach(keeps_mm rc gc none) foreach(keeps_test own_generator_keeps own_generator_keeps_bounded own_generator_this_param - own_copy_within_returned_param own_swap_elements own_callback_forwarded) + own_copy_within_returned_param own_swap_elements own_callback_forwarded + own_optional_record_read own_param_default_kept own_literal_into_array) 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() @@ -4023,6 +4024,21 @@ add_test(NAME test-own-di-null-narrowing set_tests_properties(test-own-di-null-narrowing PROPERTIES PASS_REGULAR_EXPRESSION "ts.Func" FAIL_REGULAR_EXPRESSION "error|Stack dump") +# ...and a for...of over an element of the array an outer for...of walks, whose local the debugger +# records too +add_test(NAME test-own-di-for-of-nested + COMMAND $ --emit=jit --di --opt_level=0 --no-default-lib -mm=own + "${PROJECT_SOURCE_DIR}/test/tester/own/own_for_of_nested_di.ts") +set_tests_properties(test-own-di-for-of-nested PROPERTIES PASS_REGULAR_EXPRESSION "done[.]" + FAIL_REGULAR_EXPRESSION "error|Stack dump") + +# A caught value given to a call own has no facts for, and read after it (spec 44) +add_test(NAME test-own-caught-value-call + COMMAND $ --emit=mlir-affine --no-default-lib -mm=own + "${PROJECT_SOURCE_DIR}/test/tester/own/own_caught_value_call.ts") +set_tests_properties(test-own-caught-value-call PROPERTIES PASS_REGULAR_EXPRESSION "ts.Func" + FAIL_REGULAR_EXPRESSION "error|Stack dump") + # A field read through an interface is a place read under own (spec 28); the same program under # every other model. foreach(field_chain_mm rc gc none) @@ -4651,7 +4667,8 @@ set(own_error_cases "own_err_swap_borrow_used_after|:11:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_swap_call_borrow_used_after|:15:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "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_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") 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_caught_value_call.ts b/tslang/test/tester/own/own_caught_value_call.ts new file mode 100644 index 000000000..a5db2789a --- /dev/null +++ b/tslang/test/tester/own/own_caught_value_call.ts @@ -0,0 +1,27 @@ +// A caught value cast to its class, given to a function own has no facts for (declared here, as a +// default library's virtual `instanceOf` is to its callers), and read after the call: the call is +// given the value, which it borrows, and nothing else holds the exception, so it cannot free it. +// Before, the call was taken to free the catch variable's value. Compiled only: nothing defines +// `checkCode`. +class Failure { + constructor(public code: int) {} +} +function fail(c: int) { + if (c > 0) throw new Failure(c); +} +declare function checkCode(f: Failure, c: int): boolean; +function caught(c: int) { + let matched = false; + try { + fail(c); + } catch (e) { + const failure = e; + matched = checkCode(failure, c); + matched = matched && failure.code == c; + } + return matched; +} +function main() { + print(caught(0)); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_optional_record_field_kept.ts b/tslang/test/tester/own/own_err_optional_record_field_kept.ts new file mode 100644 index 000000000..533c9cf4c --- /dev/null +++ b/tslang/test/tester/own/own_err_optional_record_field_kept.ts @@ -0,0 +1,17 @@ +// A field read out of the record a borrowing `T | undefined` result holds borrows what the record +// borrows: pushing a class-typed field into another array is still a second reference. +class Tag { constructor(public v: string) {} } +type Fruit = { name: string, tag: Tag }; + +function findIn(this: Fruit[], test: (f: Fruit) => boolean): Fruit | undefined { + for (const v of this) if (test(v)) return v; + return undefined; +} + +function main() { + const inventory: Fruit[] = []; + const kept: Tag[] = []; + const r = inventory.findIn((f) => f.name == "f1"); + if (r) kept.push(r.tag); + print(kept.length); +} diff --git a/tslang/test/tester/own/own_for_of_nested_di.ts b/tslang/test/tester/own/own_for_of_nested_di.ts new file mode 100644 index 000000000..a5f48d01f --- /dev/null +++ b/tslang/test/tester/own/own_for_of_nested_di.ts @@ -0,0 +1,19 @@ +// A for...of over an element read out of the array an outer for...of walks: the inner loop's array +// is a local that borrows the element. Built with debug info, the local has a record for the +// debugger of its own, which keeps nothing; it was taken for a keep ("'.a' borrows an element and +// cannot be stored"), so DefaultLib's concat failed under own with --di only. +function total(other: string[][]) { + let n = 0; + for (const item of other) { + for (const v of item) n += v.length; + } + + return n; +} + +function main() { + const y = ["a" + 1, "bc"]; + const z = ["d" + 2]; + assert(total([y, z]) == 6, "total"); + print("done."); +} diff --git a/tslang/test/tester/own/own_literal_into_array.ts b/tslang/test/tester/own/own_literal_into_array.ts new file mode 100644 index 000000000..5901b749d --- /dev/null +++ b/tslang/test/tester/own/own_literal_into_array.ts @@ -0,0 +1,28 @@ +// An object literal that owns what it holds (a fresh string, a new object, an array literal) given +// as an element of an array literal, or pushed, moves into the array as the array's element: the +// array's release frees what the literal held, and nothing else releases it. Before, only an owning +// local, the result or a box could take such a literal ("is moved only into a local or returned as +// it is"). Each step churns, so a value freed too early reads garbage; every model runs it. +class C { constructor(public x: number) {} } +function label(i: number) { return "s" + i; } +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) junk.push(new C(999)); +} +function main() { + let ok = 0; + for (let i = 0; i < 1000; i++) { + const patterns = [ + { name: label(i), order: [1, 2, 3], c: new C(i) }, + { name: "b", order: [3, 1, 2], c: new C(2) }, + ]; + churn(); + const more: { name: string, order: int[] }[] = []; + for (let j = 0; j < 3; j++) more.push({ name: label(j), order: [j] }); + churn(); + if (patterns[0].name == label(i) && patterns[0].order[1] == 2 && patterns[0].c.x == i && patterns[1].c.x == 2) ok++; + if (more.length == 3 && more[2].name == "s2" && more[2].order[0] == 2) ok++; + } + assert(ok == 2000, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_optional_record_read.ts b/tslang/test/tester/own/own_optional_record_read.ts new file mode 100644 index 000000000..103caab15 --- /dev/null +++ b/tslang/test/tester/own/own_optional_record_read.ts @@ -0,0 +1,58 @@ +// A record found in an array is returned as `T | undefined`, a borrow of the array: printing it +// reads its fields out of the optional's tuple value, and a field that is a number keeps nothing. +// A caught value cast to its class is the exception's until the catch ends, so a call on it - a +// virtual one own has no facts for - cannot free it. +type Fruit = { name: string, quantity: int }; + +function find(this: Fruit[], test: (f: Fruit) => boolean): Fruit | undefined { + for (const v of this) if (test(v)) return v; + return undefined; +} + +function made(n: int): Fruit[] { + const r: Fruit[] = []; + for (let i = 0; i < n; i++) { + const f = { name: "f" + i, quantity: i }; + r.push(f); + } + + return r; +} + +class Failure { + constructor(public code: int) {} + + isCode(c: int) { + return this.code == c; + } +} + +function fail(c: int) { + if (c > 0) throw new Failure(c); +} + +function caughtCast(c: int) { + let matched = false; + try { + fail(c); + } catch (e) { + const failure = e; + matched = failure.isCode(c) && failure instanceof Failure; + } + + return matched; +} + +function main() { + const inventory = made(4); + const found = inventory.find((f) => f.name == "f2"); + assert(found !== undefined && found.quantity == 2, "found"); + print(found); + print(inventory.find((f) => f.quantity == 3)); + print(inventory.find((f) => f.quantity == 9)); + + assert(caughtCast(5), "caught cast"); + assert(!caughtCast(0), "nothing thrown"); + + print("done."); +} diff --git a/tslang/test/tester/own/own_param_default_kept.ts b/tslang/test/tester/own/own_param_default_kept.ts new file mode 100644 index 000000000..53533ee25 --- /dev/null +++ b/tslang/test/tester/own/own_param_default_kept.ts @@ -0,0 +1,43 @@ +// A parameter with a default (`d: C[] = []`) is kept as any parameter is: its slot holds the +// argument given, moved in by the caller, or the default made here when none was given, and a store +// of it into a field moves it out on either path. A path that does not keep it releases what the +// slot holds, whichever it is. The argument left out is `undefined`, which holds no block. Before, +// the merge of the argument and the default was "takes a second reference". Each step churns, so a +// value freed too early reads garbage; the same program runs under every model. +class C { constructor(public x: number) {} } +function label(i: number) { return "s" + i; } +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) junk.push(new C(999)); +} +class A { + d: C[] = []; + constructor(d: C[] = [new C(7)], keep = true) { + if (keep) this.d = d; + } +} +class B { + constructor(private items: C[] = [], public name = label(3)) {} + first() { return this.items.length > 0 ? this.items[0].x : -1; } +} +function main() { + let ok = 0; + for (let i = 0; i < 1000; i++) { + const a = new A(); + churn(); + if (a.d[0].x == 7) ok++; + const b = new A([new C(i)]); + churn(); + if (b.d[0].x == i) ok++; + const c = new A([new C(i)], false); + const e = new A(undefined, false); + churn(); + if (c.d.length == 0 && e.d.length == 0) ok++; + const f = new B(); + const g = new B([new C(5)], label(i)); + churn(); + if (f.first() == -1 && g.first() == 5 && f.name == "s3" && g.name == label(i)) ok++; + } + assert(ok == 1000 * 4, "ok"); + print("done."); +}