From 7c14ebb4c5510eb6af1c7bc09e92be1028ef0c89 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 18:04:24 +0000 Subject: [PATCH 1/4] -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/4] -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/4] -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/4] -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); +}