Skip to content

-mm=own phase 5b: a closure that escapes owns what it captures; a call in a try body is a call - #437

Merged
ASDAlexander77 merged 4 commits into
mainfrom
own-phase-5b
Oct 2, 2026
Merged

ASDAlexander77 merged 4 commits into
mainfrom
own-phase-5b

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Phase 5b of -mm=own (spec §2.5; plan docs/superpowers/plans/2026-10-02-own-phase-5.md task 4; results in spec §17). It also fixes a soundness hole that predates phase 5.

A call inside a try body was not a call (pre-existing, fixed)

Calls in a try body lower to ts.Invoke / ts.InvokeHybrid, and neither own pass knew those ops. Three consequences:

  • Drops were missed. Such a call dropped no borrow. A read of h.c held across try { reset(h) }, where reset overwrites h.c, compiled and printed 999 (gc: 1).
  • No callee. The call resolved to no callee.
  • Nothing fresh. Its result was never treated as freshly made.

directCallee / calleeValue / callArgs now take every call form apart, and isCall / isFresh know both ops. Regression test: own_err_call_in_try_drops. Because a call in a try now returns a fresh value, four using files and 51exceptions compile under own now (all checked at runtime, below).

Closures that escape

A closure over a variable that escapes (returned, stored, pushed, kept) owns what it captures. Its box keeps the owning routine, which destroys cells and copies.

  • The move. Each cell moves into the box at the store that fills it. rc's ts.RetainCell goes, and so do the frame's ts.ReleaseCells that store reaches; it must dominate them, or the error is moved here on some paths only.
  • No later use. Any use of the variable in the frame after the move is an error: a read, an assignment, a second capture (so two escaping closures can't share a variable). So is a capture in a loop of a variable declared outside it.
  • Can't move: a parameter's cell holding an object (the value is the caller's), and a cell inherited from an enclosing closure. A number parameter moves fine (makeScaler(k)).
  • Copies move in as in phase 4. A borrowing closure over the same variable must be done before the move. A call through the box may not come after the closure escapes.

The limit users will hit first: a closure that escapes cannot capture this or an object parameter. An event-handler style register(() => this.x) is rejected.

Verification

  • Release suite: 3368 of 3369 pass. The one failure, test-compile-gc-defaultlib-collector, is the same as on -mm=own phase 5a: a closure that does not escape borrows what it captures #436: the installed default library at I:\tslang is stale. All 367 own tests pass, including the new ones.
  • WSL/GCC: builds, and 366 of 366 own tests pass.
  • Corpus: 319 → 325 of 591, none lost. The 6 new ones (13actions, 51exceptions, 00disposable, 00try_catch_return_dispose, 00using_nested_scopes, 01try_catch_return_dispose) pass under test-runner -mm=own, AOT and JIT, and their JIT output matches gc.
  • Memory: measure.ps1 on own_closure_escape gives gc 5.9, rc 4.3, none 234.6, own 4.3 MB.
  • Teeth: with the frame's ReleaseCells kept after a move, own_closure_escape crashes under AOT (0xC0000005).

Tests

  • Positive: own_closure_escape (object local, number parameter, counter, copy beside a cell, called before being returned, kept by a constructor, pushed into a global).
  • Negatives: own_err_closure_escape_used, _twice, _param, _loop, _inherited, _some_paths, and own_err_call_in_try_drops. own_err_closure (an escaping counter) compiles now and is removed.

Found, not fixed (every model)

MLIRGen segfaults on pushing a closure that returns s32 into a (() => number)[], e.g. let h: (() => number)[] = []; function r() { let n = 0; h.push(() => n); }. Since 0 is s32, that is the natural way to write it. It is unrelated to own; I can file an issue.

🤖 Generated with Claude Code

ASDAlexander77 and others added 4 commits October 2, 2026 10:33
Each cell moves into the box where the box takes it: rc's RetainCell goes, and so
does every frame ReleaseCell the move reaches, which it must dominate. A use of the
variable in the frame after the move, a second capture, a capture in a loop of a
cell made outside it, a parameter's cell with a heap value and a cell inherited
from an enclosing closure are errors. Copies move in as in phase 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calls in a try body lower to ts.Invoke / ts.InvokeHybrid, which neither ownership
pass recognised: such a call dropped no borrow (a field read across reset(h) in a
try compiled and read freed memory), resolved to no callee, and returned nothing
fresh. directCallee/calleeValue/callArgs now take every call form apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 8cfb0f1 into main Oct 2, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-phase-5b branch October 2, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant