Skip to content

-mm=own, phase 1: moves decided by reachability - #401

Merged
ASDAlexander77 merged 5 commits into
mainfrom
own-phase-1
Sep 29, 2026
Merged

ASDAlexander77 merged 5 commits into
mainfrom
own-phase-1

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Phase 1 of -mm=own (spec §2.2 verdict 1, §2.6, §4.2 step 3, §5). The plan is tslang/docs/superpowers/plans/2026-09-29-own-phase-1.md and the results are in spec §12. Only OwnershipInferencePass.cpp changed. MLIRGen and the lowering are untouched, and gc, rc and none are unaffected.

What changes

Phase 0 allowed a move only into one owner, in the value's own block, with no later use. Phase 1 decides a move by reachability and dominance on the affine CFG:

  • Moves into a branch are allowed. Each release of the source that the move can reach must be dominated by it, and is erased. A release the move cannot reach stays: it is the owner on the paths that didn't move. For example: const a = new C(); if (x) { let b = a; return ...; } use(a).
  • Moves out of a let: into another let, a field (h.c = a), or a return. The move is measured from rc's retain, because at this level a return is a store into the return slot that comes after the source's scope-exit release.
  • Use after move is an error, with a note at the move. This covers a read, an assignment, and any use of an earlier read such as const b = a (reported as 'a' is used here after its value was moved).
  • Loops (§2.6): moving a value made outside the loop is rejected. Moving out of a let declared in the loop body is fine.
  • Moved on some paths only (a move reaches a release it doesn't dominate) is an error. Fixing it needs drop elaboration, which is later.
  • Other changes:
    • c: C | null = null now compiles (a cast of null or undefined is a literal).
    • --di errors name the variable.
    • Only heap-owning values are move candidates, and string literals (immortal) may be held by any number of places. Under --opt and JIT, CSE had merged literals, which gave false loop errors.

Tests

  • New positives: own_move_const_branch, own_move_let, own_move_field, own_move_return, own_move_loop_body, own_move_control_flow, own_literal_reuse. Each runs under JIT and AOT, with a churn() before its reads, plus the no-counting check.
  • New negatives: own_err_move_some_paths, own_err_assign_after_move, own_err_let_twice, own_err_let_loop, own_err_const_alias_move, own_err_const_alias_field, and a --di naming test.
  • Changed negatives: six phase-0 negatives now say "is used here after its value was moved" instead of "takes a second reference". own_err_alias gained a use after its move, because phase 1 accepts a bare let b = a.
  • Teeth: with the moved releases kept, every move test fails.

Verification

  • Release suite (Windows): 3130/3130.
  • Debug own tests: all pass except the known linkname DictionaryAttr assert, which fails on main too. owned-async was excluded; its Debug async-runtime assert also fails on main.
  • 1M iterations: own 4.8 MB, the same as rc (gc 6.6, none 547) for let→let, →field and →return, at both -O3 and -O1.
  • A fresh-context review found one Critical (a const alias of a moved let read freed memory) and two Important findings (CSE-merged literals; test gaps). All three are fixed in the last commit, each with a test that failed first.
  • Linux not run.

Still rejected (spec §12.5, input to phase 2)

  • borrows;
  • moves on some paths only;
  • assigning to a moved let;
  • moves out of fields or parameters, or through boxing casts.

const b = a; a = new C(); b.x isn't caught: an assignment isn't a move. rc has the same bug today and prints garbage.

🤖 Generated with Claude Code

ASDAlexander77 and others added 5 commits September 29, 2026 11:32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 0 let a fresh value move only into one taker in its own block with no use after.
Now a taker may be in any block: every other use must be unreachable from it, a taker
that control returns to without the value being made again is a move inside a loop
(spec 2.6), and each release the move reaches must be dominated by it and is erased.
A release the move cannot reach stays, the owner on the paths that did not move; one
it reaches but does not dominate is "moved here on some paths only".

Diagnostics follow spec 5: "is used here after its value was moved" with a note at the
move. Four phase-0 negatives now report that instead of "takes a second reference".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc reads an owning local with ts.Load (through casts that keep the block) and retains what
it read for the receiver. That retain is now a move out of the slot: it is erased with
every ReleaseSlot of the slot the move reaches, which must be dominated by it. Any other
use of the slot the move reaches - a read, an assignment - is a use after the move, and a
move whose own read comes round again without the slot being declared again is a move
inside a loop.

Also: a cast of null or undefined is a literal (`c: C | null = null` was rejected), and
under --di the diagnostics name the variable, read from MLIRGen's NameLoc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec section 12: the rule as built, 1M-iteration numbers (own 4.8 MB like rc, none 547),
the teeth run, the negative tests whose text changed, and phase 2's inputs.

own_move_loop_body passed with the moved releases kept - it read the value before either
release and allocated nothing over it. It now keeps every value past the loop and reads
them after churn(), and fails without the move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…literals

`const b = a` for a `let a` is folded into a second ts.Load of a's slot, and the slot
move only looked at the moving load's users, so `const b = a; let c = a; <c destroyed>;
b.x` compiled and read freed memory. Every use of every read of the slot now counts as a
use of `a` (own_err_const_alias_move, own_err_const_alias_field).

Under --opt, and so under the JIT, CSE merges identical literal casts before inference,
and a merged `0` was treated as a fresh heap value moved on every loop iteration. Only a
value whose type owns heap memory is a candidate now, and a string literal - the
immortal global - may be held by any number of places (own_literal_reuse).

own_move_control_flow covers break/continue out of a moving loop body and returns from
inside a loop; it fails with the moved releases kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 13b390e into main Sep 29, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-phase-1 branch September 29, 2026 11:29
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