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 e82eab237..e9f6b31bb 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 @@ -5,7 +5,8 @@ amendments folded in. Phase 0 merged as #399 (results §11); phase 1 (moves by reachability) merged as #401 (results §12); phase 2 (borrows for locals) merged as #402 (results §13); phase 3 (containers and unions) merged as #403 (results §14); phase 4 (function signatures and `any`) merged as #406-#410 (results §15); phase 5a (closures that do not escape) -on branch `own-phase-5` (results §16). Plans in `docs/superpowers/plans/`. +merged as #436 (results §16); phase 5b (closures that escape) on branch `own-phase-5b` (results +§17). Plans in `docs/superpowers/plans/`. ## 1. Purpose @@ -1104,3 +1105,78 @@ are phase 7, and object literals whose methods capture), 4 stop at an earlier li - A non-escaping closure always borrows: one that could own its captures instead (called after the cell's block ends, the variable not used again) is rejected. - A call given any function value drops every chain when its callee may drop. + +## 17. Phase 5b results, 2026-10-02 + +Plan: `docs/superpowers/plans/2026-10-02-own-phase-5.md`, task 4. Changed: `OwnershipInferencePass.cpp`, +and, for the bug in §17.2, `OwnershipFacts.h` and `OwnershipSignaturePass.cpp`. + +### 17.1 What phase 5b accepts + +A closure over a cell that escapes (§16.1's classification) owns what its box holds. Its box keeps +today's routine (`tsrelcb_`), which destroys each cell with what is in it, and each copy. + +- **Copies** move in as in phase 4: the box's store is a taker, and the copy's owner gives it up there. +- **Each cell moves in** at the store that fills the box: rc's `ts.RetainCell` for it goes, and so + does each of the frame's `ts.ReleaseCell`s the move reaches. The move must dominate them: + `'a' is moved here on some paths only` otherwise (a closure pushed in one branch of an `if`). +- **Nothing in the frame may use the variable after the move.** That covers a read, an assignment + and another capture: `'a' is captured by a closure that escapes, and is still used here`. Two + closures that escape cannot share a variable. This is §2.5's "any use after the closure's + creation", measured from the box's store. +- **A move round a loop** of a cell made outside it is the loop rule (§2.6). +- **No move is possible** for a parameter's cell with a heap value, whose value is the caller's, or + for a cell an enclosing closure holds: `a closure that escapes owns what it captures, but -mm=own + cannot move this variable into it: ...`. A parameter's cell holding a number moves: it owns + nothing but itself (`makeScaler(k)`). +- **A borrowing closure over the same cell** must be done with it before the move: the move into an + escaping box is an end of the cell, beside its `ts.ReleaseCell`s. +- **A call through the box** (a folded `const f` called before `return f`) may not come after the + closure escapes or is released. + +### 17.2 A call inside a try body was not a call + +A call in a try body lowers to `ts.Invoke`, or to `ts.InvokeHybrid` through a hybrid function: a +terminator whose normal and unwind edges are its successors. Neither pass knew the op. So such a +call dropped no borrow: a read of `h.c` held across `reset(h)` inside a `try`, with `reset` +overwriting `h.c`, compiled and printed 999 (gc: 1). It also resolved to no callee and returned nothing +fresh. Found when `51exceptions.ts` reported its on-the-spot closure call in a `try` as an escape. +`directCallee`, `calleeValue` and `callArgs` now take every call form apart, and `isCall` and +`isFresh` know both ops. Test: `own_err_call_in_try_drops`. + +### 17.3 Measured + +AOT, `measure.ps1`, in MB: `own_closure_escape` reads gc 5.9, rc 4.3, none 234.6, own 4.3. + +### 17.4 Teeth + +With the frame's `ts.ReleaseCell`s kept after a move, `own_closure_escape` fails under AOT +(0xC0000005). The JIT folds the reads of freed memory, as in §16.3. + +### 17.5 Tests and the corpus + +- **Positive:** `own_closure_escape` (over an object local, over a number parameter, a counter, a + copy beside a cell, called through the box before it is returned, kept by a constructor, pushed + into a global). +- **Negatives:** `own_err_closure_escape_used`, `own_err_closure_escape_twice`, + `own_err_closure_escape_param`, `own_err_closure_escape_loop`, `own_err_closure_escape_inherited`, + `own_err_closure_escape_some_paths`, `own_err_call_in_try_drops`. `own_err_closure` (a counter that + escapes) compiles now and is gone. + +Corpus: 319 of 591 before, 325 after, none lost. `13actions` comes from the escapes. `51exceptions` +and four `using` files (`00disposable`, `00try_catch_return_dispose`, `00using_nested_scopes`, +`01try_catch_return_dispose`) come from §17.2: a call in a try body returns a fresh value now. All six +pass under test-runner `-mm=own`, AOT and JIT, and their JIT output matches gc's. + +Found, not fixed (every model, MLIRGen): pushing a closure that returns `s32` into a +`(() => number)[]` segfaults the compiler. For example, `let n = 0; h.push(() => n)`, since an +integer literal is `s32`. + +### 17.6 Known limits + +- The capture is the move, so a closure created on one path only, with the variable alive on the + other, is an error (no drop elaboration). +- A frame use between the capture and the escape is rejected, though the box is still alive there. +- A closure that escapes from a closure body with a cell it inherited, and a closure over a + parameter's object, are errors. `this` captured by an escaping callback is the common case. +- From §16.5: a non-escaping closure always borrows; a captured local is never `let`-borrowed. diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 9359cd276..bbf2e8944 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -45,6 +45,48 @@ namespace mlir_ts = mlir::typescript; // the caller's. #define OWN_ASSIGNS_CAPTURES_ATTR_NAME "__own_assigns_captures" +// A call inside a try body is a `ts.Invoke` (`ts.InvokeHybrid` through a hybrid function): a +// terminator whose normal and unwind edges are its successors. It names its callee as a symbol, or +// is given the function value as its first call operand. + +// The function a call names, or null when it goes through a value. +inline mlir::FlatSymbolRefAttr directCallee(mlir::Operation *op) +{ + if (auto callOp = mlir::dyn_cast(op)) + { + return callOp.getCalleeAttr(); + } + + if (auto callOp = mlir::dyn_cast(op)) + { + return callOp.getCalleeAttr(); + } + + if (auto invokeOp = mlir::dyn_cast(op)) + { + return invokeOp.getCalleeAttr(); + } + + return {}; +} + +// The function value a call goes through, or null when it names its callee. +inline mlir::Value calleeValue(mlir::Operation *op) +{ + if (mlir::isa(op)) + { + return op->getOperand(0); + } + + if (auto invokeOp = mlir::dyn_cast(op); invokeOp && !invokeOp.getCalleeAttr()) + { + return invokeOp.getCallOperands().front(); + } + + return {}; +} + // The arguments of a call, without the callee value of an indirect one. inline mlir::OperandRange callArgs(mlir::Operation *op) { @@ -53,6 +95,16 @@ inline mlir::OperandRange callArgs(mlir::Operation *op) return op->getOperands().drop_front(); } + if (auto invokeOp = mlir::dyn_cast(op)) + { + return invokeOp.getCalleeAttr() ? invokeOp.getCallOperands() : invokeOp.getCallOperands().drop_front(); + } + + if (auto invokeOp = mlir::dyn_cast(op)) + { + return invokeOp.getCallOperands(); + } + return op->getOperands(); } @@ -199,9 +251,9 @@ inline bool isFresh(mlir::Value value) return true; } - if (auto callOp = mlir::dyn_cast(def)) + if (mlir::isa(def) && directCallee(def)) { - auto callee = mlir::SymbolTable::lookupNearestSymbolFrom(def, callOp.getCalleeAttr()); + auto callee = mlir::SymbolTable::lookupNearestSymbolFrom(def, directCallee(def)); return callee && !callee.isDeclaration(); } @@ -210,7 +262,8 @@ inline bool isFresh(mlir::Value value) return true; } - if (mlir::isa(def)) + if (mlir::isa(def)) { return false; } @@ -221,7 +274,8 @@ inline bool isFresh(mlir::Value value) inline bool isCall(mlir::Operation *op) { return mlir::isa(op); + mlir_ts::CallInternalOp, mlir_ts::CallHybridInternalOp, mlir_ts::InvokeOp, + mlir_ts::InvokeHybridOp>(op); } inline bool isPlace(mlir::Value ref) diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 3dfa3b5bd..64fbe5770 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -69,6 +69,7 @@ class OwnershipInferencePass : public mlir::PassWrapper captureStores; // rc's references for boxes, which the closures decide llvm::DenseSet captureRetains; + // each closure over a cell, by its `ts.CreateBoundFunction` + llvm::DenseMap closureOf; // Finds the closures over cells and what they borrow, before anything else: whether a box's // stores are reads or takers depends on it. A closure with copies only keeps what phase 4 does @@ -418,13 +421,20 @@ class OwnershipInferencePass : public mlir::PassWrapperfills) { auto storeOp = fill.second; + auto isCell = mlir::isa(storeOp.getValue().getType()); + // an escaping closure's copies move into its box as in phase 4, rc's retain with them + if (closure->escape && !isCell) + { + continue; + } + if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) { closure->retains.push_back(retain); captureRetains.insert(retain); } - if (!closure->escape && !mlir::isa(storeOp.getValue().getType())) + if (!closure->escape && !isCell) { storeOp->setAttr(OWN_CAPTURE_BORROW_ATTR_NAME, mlir::UnitAttr::get(&getContext())); captureStores[storeOp] = closure.get(); @@ -436,6 +446,7 @@ class OwnershipInferencePass : public mlir::PassWrappersetAttr(OWN_BORROWS_CAPTURES_ATTR_NAME, mlir::UnitAttr::get(&getContext())); } + closureOf[boundOp.getOperation()] = closure.get(); closures.push_back(std::move(closure)); }); } @@ -636,7 +647,7 @@ class OwnershipInferencePass : public mlir::PassWrapperescape) { - reportClosureEscapes(closure->escape, boundOp); + decideEscaping(*closure, toErase); continue; } @@ -652,7 +663,7 @@ class OwnershipInferencePass : public mlir::PassWrapper()); for (auto *user : value.getUsers()) { - if (mlir::isa(user)) + if (mlir::isa(user) || movesCellAway(user)) { ends.push_back(user); } @@ -732,6 +743,117 @@ class OwnershipInferencePass : public mlir::PassWrapper(op); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + auto found = boundOp ? closureOf.find(boundOp.getOperation()) : closureOf.end(); + return found != closureOf.end() && found->second->escape && + mlir::isa(storeOp.getValue().getType()); + } + + // A closure that escapes owns what its box holds (spec 2.5, phase 5b). Its copies moved in as + // in phase 4. Each cell moves in where the box takes it: that store is the move, rc's + // `ts.RetainCell` for it goes, and so does each of the frame's `ts.ReleaseCell`s the move reaches, + // which it must dominate. Nothing in the frame may use the variable after the move - read it, + // assign it, capture it again - and the move may not come round a loop to a cell made outside + // it. A cell that borrows its value (a parameter's) or one an enclosing closure holds has + // nothing to move. And nothing may call through the box once the closure has gone. + void decideEscaping(Closure &closure, llvm::SetVector &toErase) + { + auto ok = true; + for (auto &fill : closure.fills) + { + auto value = fill.second.getValue(); + mlir::Operation *move = fill.second; + if (!mlir::isa(value.getType())) + { + continue; + } + + if (isLoadedCell(value)) + { + reportEscapingCellNotOwned(move, closure.escape, "it is captured from an enclosing function"); + ok = false; + continue; + } + + auto varOp = isCellVariable(value) ? value.getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp) + { + reportCaptureNotOwned(move, "this value"); + ok = false; + continue; + } + + auto name = varName(varOp); + MLIRTypeHelper mth(&getContext(), CompileOptions{}); + if (!isOwningVariable(varOp) && + mth.ownsHeapMemory(varOp.getLoc(), mlir::cast(value.getType()).getElementType())) + { + reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); + ok = false; + continue; + } + + if (takerLoops(move, varOp)) + { + reportMovedInLoop(move, name); + ok = false; + continue; + } + + llvm::SmallVector moved; + for (auto *user : value.getUsers()) + { + if (user == move || llvm::is_contained(closure.retains, user) || !reachableAfter(move, user, varOp)) + { + continue; + } + + if (!mlir::isa(user)) + { + reportUsedAfterEscapingCapture(user, move, name); + ok = false; + break; + } + + if (!dominance->properlyDominates(move, user)) + { + reportMovedOnSomePaths(move, user, name); + ok = false; + break; + } + + moved.push_back(user); + } + + toErase.insert(moved.begin(), moved.end()); + } + + // A call through the box itself after the closure was given away or given back. followClosure + // stops at the escape, so `ends` may miss releases past it; that is enough while the only + // calls through a box are those of a folded `const f`, whose release ends its block. + auto name = ownerName(closure.op.getResult()); + llvm::SmallVector gone(closure.ends.begin(), closure.ends.end()); + gone.push_back(closure.escape); + for (auto *end : gone) + { + for (auto *use : closure.boxUses) + { + if (ok && use != end && reachableAfter(end, use, closure.op.getOperation())) + { + reportUseAfterMove(use, end, name); + ok = false; + } + } + } + + toErase.insert(closure.retains.begin(), closure.retains.end()); + } + // A captured variable whose value this function does not own - a parameter, `this` - has a cell // that borrows the value: the cell's release frees the cell only (OWN_CELL_BORROWS_ATTR_NAME), // and nothing may assign the variable, here or in a closure over it, since the old value is the @@ -1263,7 +1385,8 @@ class OwnershipInferencePass : public mlir::PassWrapper(user)) + auto callee = isCall(user) ? calleeValue(user) : mlir::Value(); + if (!callee) { return false; } @@ -1274,7 +1397,7 @@ class OwnershipInferencePass : public mlir::PassWrapper(); - auto getMethodOp = user->getOperand(0).getDefiningOp(); + auto getMethodOp = callee.getDefiningOp(); auto methodField = getThisOp ? getThisOp.getOperand().getDefiningOp() : mlir_ts::LoadOp(); return use.getOperandNumber() == 1 && getMethodOp && methodField && getThisOp.getOperand() == getMethodOp.getBoundFunc() && @@ -1780,11 +1903,12 @@ class OwnershipInferencePass : public mlir::PassWrapper(call)) + auto value = calleeValue(call); + if (auto symbol = directCallee(call)) { - callee = callOp.getCalleeAttr().getAttr(); + callee = symbol.getAttr(); } - else if (auto calleeOp = call->getOperand(0).getDefiningOp()) + else if (auto calleeOp = value ? value.getDefiningOp() : nullptr) { if (auto getMethodOp = mlir::dyn_cast(calleeOp)) { @@ -2762,10 +2886,19 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("a closure that escapes owns what it captures, but -mm=own cannot move this " + "variable into it: ") + << why; + diag.attachNote(escape->getLoc()) << "the closure escapes here"; + signalPassFailure(); + } + + void reportUsedAfterEscapingCapture(mlir::Operation *use, mlir::Operation *capture, llvm::StringRef name) { - auto diag = escape->emitError("a closure that captures a variable and escapes here is not supported by -mm=own yet"); - diag.attachNote(closure->getLoc()) << "the closure is created here"; + auto diag = use->emitError("'") << name << "' is captured by a closure that escapes, and is still used here"; + diag.attachNote(capture->getLoc()) << "the closure takes it here"; signalPassFailure(); } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index b5d4993ae..bcb01f1c1 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -473,19 +473,13 @@ class OwnershipSignaturePass : public mlir::PassWrapper(op)) + if (auto callee = directCallee(op)) { - callees.known = addDefined(callOp.getCalleeAttr().getAttr(), callees) || - addImported(callOp.getCalleeAttr().getAttr(), callees); + callees.known = addDefined(callee.getAttr(), callees) || addImported(callee.getAttr(), callees); } - else if (auto callOp = mlir::dyn_cast(op)) + else if (mlir::isa(op)) { - callees.known = addDefined(callOp.getCalleeAttr().getAttr(), callees) || - addImported(callOp.getCalleeAttr().getAttr(), callees); - } - else if (mlir::isa(op)) - { - resolveValue(op->getOperand(0), callees); + resolveValue(calleeValue(op), callees); } if (!callees.known) @@ -599,7 +593,8 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user) && use.getOperandNumber() == 0) + if (mlir::isa(user) && + calleeValue(user) == use.get() && use.getOperandNumber() == 0) { continue; } @@ -657,7 +652,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user)) + if (mlir::isa(user)) { continue; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 3556f6378..7468cfa76 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2563,7 +2563,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_field_let_borrow own_element_let_borrow own_borrow_merge_after_call own_call_no_drops own_getter_borrow own_param_kept own_any_box own_string_length_borrow - own_closure_borrow) + own_closure_borrow own_closure_escape) 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. @@ -2626,7 +2626,13 @@ set(own_error_cases "own_err_const_alias_move|borrows .* and cannot be assigned" "own_err_const_alias_field|is used here after its value was moved" "own_err_field_alias|is used here after its value was moved" - "own_err_closure|a closure that captures a variable and escapes here is not supported by -mm=own yet" + "own_err_call_in_try_drops|borrows .* but is used here after it may be released or overwritten" + "own_err_closure_escape_used|is captured by a closure that escapes, and is still used here" + "own_err_closure_escape_twice|is captured by a closure that escapes, and is still used here" + "own_err_closure_escape_param|cannot move this variable into it: its value is the caller's" + "own_err_closure_escape_inherited|cannot move this variable into it: it is captured from an enclosing function" + "own_err_closure_escape_loop|is moved inside a loop" + "own_err_closure_escape_some_paths|is moved here on some paths only" "own_err_closure_outlives_cell|borrows .*, which it captures, but runs here after .* is released" "own_err_closure_copy_moved|borrows .*, which it captures, but runs here after .* is released" "own_err_closure_param_assigned|is a captured parameter and cannot be assigned under -mm=own" diff --git a/tslang/test/tester/own/own_closure_escape.ts b/tslang/test/tester/own/own_closure_escape.ts new file mode 100644 index 000000000..9c522219b --- /dev/null +++ b/tslang/test/tester/own/own_closure_escape.ts @@ -0,0 +1,97 @@ +// -mm=own, phase 5b: a closure that escapes owns what it captures. Each cell moves into its box +// where the box takes it, the frame's release of the cell goes, and the box's release destroys the +// cell and what is in it. Each case reads through a churn() after the frame that made the closure +// is gone. +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); + } +} + +// over an object local +function makeAdder(k: number) { + let bump = new C(k); + bump.v.push(k); + return (v: number) => v + bump.x + bump.v.length; +} + +// over a number parameter: a cell that owns nothing but itself +function makeScaler(k: number) { + return (v: number) => v * k; +} + +// over a number local the closure assigns +function makeCounter() { + let n = 0; + return () => ++n; +} + +// a copy beside a cell: the copy moves in as well +function makeWithCopy(k: number) { + const h = new C(k); + let m = 1; + return () => h.x * m; +} + +// called through its box before it is returned +function usedThenReturned(k: number) { + let a = new C(k); + const f = () => a.x; + churn(); + f(); + return f; +} + +// kept by a constructor +class Holder { + constructor(public f: () => number) {} +} + +function intoField(k: number) { + let a = new C(k); + return new Holder(() => a.x); +} + +// pushed into a global +let handlers: (() => number)[] = []; + +function register(k: number) { + let c = new C(k); + handlers.push(() => c.x); +} + +function main() { + let t = 0; + for (let i = 0; i < 20000; i++) { + const n = i % 10; + const add = makeAdder(n); + const scale = makeScaler(n); + const count = makeCounter(); + count(); + const copy = makeWithCopy(n); + const used = usedThenReturned(n); + const held = intoField(n); + if (i < 10) { + register(n); + } + + churn(); + t += add(1) + scale(2) + count() + copy() + used() + held.f(); + } + + churn(); + for (const h of handlers) { + t += h(); + } + + assert(t == 620045); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_call_in_try_drops.ts b/tslang/test/tester/own/own_err_call_in_try_drops.ts new file mode 100644 index 000000000..d500afb7b --- /dev/null +++ b/tslang/test/tester/own/own_err_call_in_try_drops.ts @@ -0,0 +1,35 @@ +// -mm=own rejects: `y` borrows `h.c`, and `reset`, called inside a try body (a `ts.Invoke`), +// overwrites it. A call there was not a call to the passes, so this compiled and read freed memory. +class C { + constructor(public x: number) {} +} + +class H { + c: C = new C(1); +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new C(999)); + } +} + +function reset(h: H) { + h.c = new C(9); +} + +function read(h: H) { + const y = h.c; + try { + reset(h); + } catch (e) { + } + + churn(); + print(y.x); +} + +function main() { + read(new H()); +} diff --git a/tslang/test/tester/own/own_err_closure.ts b/tslang/test/tester/own/own_err_closure.ts deleted file mode 100644 index 1080c8877..000000000 --- a/tslang/test/tester/own/own_err_closure.ts +++ /dev/null @@ -1,12 +0,0 @@ -// -mm=own, phase 5 rejects: a closure over a variable that escapes (returned). Its box would own -// the cell, which phase 5b does. -function makeCounter() { - let n = 0; - return () => ++n; -} - -function main() { - const f = makeCounter(); - print(f()); - print("done."); -} diff --git a/tslang/test/tester/own/own_err_closure_escape_inherited.ts b/tslang/test/tester/own/own_err_closure_escape_inherited.ts new file mode 100644 index 000000000..2ef8be71b --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_escape_inherited.ts @@ -0,0 +1,16 @@ +// -mm=own, phase 5b rejects: the inner closure escapes from `g` with a cell that `g`'s own box +// holds for `make`. +class C { + constructor(public x: number) {} +} + +function make() { + let a = new C(1); + const g = () => () => a.x; + const h = g(); + return h(); +} + +function main() { + print(make()); +} diff --git a/tslang/test/tester/own/own_err_closure_escape_loop.ts b/tslang/test/tester/own/own_err_closure_escape_loop.ts new file mode 100644 index 000000000..063a72905 --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_escape_loop.ts @@ -0,0 +1,18 @@ +// -mm=own, phase 5b rejects: each turn of the loop would move `a` into another closure. +class C { + constructor(public x: number) {} +} + +let handlers: (() => number)[] = []; + +function register() { + let a = new C(1); + for (let i = 0; i < 3; i++) { + handlers.push(() => a.x); + } +} + +function main() { + register(); + print(handlers.length); +} diff --git a/tslang/test/tester/own/own_err_closure_escape_param.ts b/tslang/test/tester/own/own_err_closure_escape_param.ts new file mode 100644 index 000000000..c99f65559 --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_escape_param.ts @@ -0,0 +1,13 @@ +// -mm=own, phase 5b rejects: `c` is the caller's, so a closure that escapes cannot own it. +class C { + constructor(public x: number) {} +} + +function make(c: C) { + return () => c.x; +} + +function main() { + const c = new C(1); + print(make(c)()); +} diff --git a/tslang/test/tester/own/own_err_closure_escape_some_paths.ts b/tslang/test/tester/own/own_err_closure_escape_some_paths.ts new file mode 100644 index 000000000..ecacec09c --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_escape_some_paths.ts @@ -0,0 +1,20 @@ +// -mm=own, phase 5b rejects: `a` moves into the closure on one path only, and the frame would +// have to release it on the other. +class C { + constructor(public x: number) {} +} + +let handlers: (() => number)[] = []; + +function register(keep: boolean) { + let a = new C(1); + if (keep) { + handlers.push(() => a.x); + } +} + +function main() { + register(true); + register(false); + print(handlers.length); +} diff --git a/tslang/test/tester/own/own_err_closure_escape_twice.ts b/tslang/test/tester/own/own_err_closure_escape_twice.ts new file mode 100644 index 000000000..fb0175a2d --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_escape_twice.ts @@ -0,0 +1,13 @@ +// -mm=own, phase 5b rejects: two closures that escape cannot both own `n`. +let handlers: (() => number)[] = []; + +function register() { + let n: number = 0; + handlers.push(() => ++n); + handlers.push(() => n); +} + +function main() { + register(); + print(handlers.length); +} diff --git a/tslang/test/tester/own/own_err_closure_escape_used.ts b/tslang/test/tester/own/own_err_closure_escape_used.ts new file mode 100644 index 000000000..275223d6d --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_escape_used.ts @@ -0,0 +1,15 @@ +// -mm=own, phase 5b rejects: the closure takes `a` with it, and the frame reads `a` afterwards. +class C { + constructor(public x: number) {} +} + +function make() { + let a = new C(1); + const f = () => a.x; + print(a.x); + return f; +} + +function main() { + print(make()()); +}