Skip to content

-mm=own, phase 2: borrows for locals - #402

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

ASDAlexander77 merged 5 commits into
mainfrom
own-phase-2

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Phase 2 of -mm=own (spec §2.2 verdict 2). The plan is tslang/docs/superpowers/plans/2026-09-29-own-phase-2.md and the results are in spec §13. Only OwnershipInferencePass.cpp changed, and gc, rc and none are unaffected.

What changes

The verdicts now run in the spec's order: move, then borrow, then error.

  • Borrowing. When a value can't be moved, a let declared from it borrows it: an owning let that took its own reference, not captured. The borrower's retain and all its releases are erased, and the owner keeps its own.
  • When a borrow is sound. No use of the borrower may be reachable from any release or assignment of the owner. "Use" covers anything derived from reading it that can still point into the block: field references, values loaded through them, bound methods, and catch/finally locals.
  • Escapes. The borrower must never escape: stored, pushed, returned, retained, boxed, chained into another let, or assigned.
  • Several receivers. When one owner has several receivers, all of them must borrow, because a borrow pins its owner. A field store next to a borrowing let is 'a' is moved here while 'b' borrows it.
  • Errors now accepted. These phase-1 errors are now legal programs:
    • an inner let of a value used again;
    • let b = a; let c = a;;
    • a let in a loop body of a value made outside the loop;
    • a let on one branch only.
  • Errors. They follow §5 and name both variables under --di:
    • 'b' borrows 'a' but is used here after 'a' is released or overwritten;
    • ... cannot be stored, returned or captured;
    • ... cannot be assigned.

Tests

  • New positives: own_borrow_inner_let, own_borrow_loop, own_borrow_branch, own_borrow_two_lets, own_borrow_nullable. Each runs under JIT and AOT with a churn() read, plus the no-counting check.
  • New negatives: own_err_borrow_escape, own_err_borrow_and_move, own_err_move_some_paths_field, and four from the review: own_err_borrow_bound_method, _catch_local, _finally_local, _derived_ref.
  • Removed negatives: six phase-1 negatives that phase 2 accepts, replaced by the positives above.
  • Teeth: with the borrower's releases kept, 10/10 borrow checks fail.

Verification

  • Release suite (Windows): 3146/3146.
  • Debug own tests: 240/240. owned-async and ownership-verifier-4 were excluded; they fail on main too.
  • 1M iterations: own is 4.7–4.8 MB, the same as rc, while none reaches 113 MB–1.1 GB. Measured at both -O3 and -O1.
  • A fresh-context review found one Critical and one Important, both fixed in the last commit, each with a test that failed first:
    • Critical: derived values weren't tracked. const f = b.m, a for…of over b.v, and a catch local of b compiled to reads of freed memory.
    • Important: if (b) on a nullable borrower was a false escape.
  • Linux not run.

Still rejected (spec §13.5, input to phase 3)

  • borrows out of containers (arr[i], h.c), bounded by pop/splice/overwrite;
  • borrow chains, and borrowers that get assigned;
  • borrows of parameters and of this (phase 4);
  • a move after every borrower's last use.

An alias of the owner itself held across an assignment of it compiles to a use-after-free under rc as well (§12.5).

🤖 Generated with Claude Code

ASDAlexander77 and others added 5 commits September 29, 2026 12:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verdicts run in the spec's order: move, then borrow, then error. The phase-1 move check
now runs quietly first; when it fails and the receiver is an owning `let` that took its
own reference, it borrows: its retain and every release of its slot go, and the owner
keeps its own. That holds while no use of the borrower can run after a release or an
assignment of the owner, and while nothing it holds is stored, returned, retained or
assigned - each broken rule has its own error, naming both variables under --di.

Five phase-1 negatives are now legal and are replaced by positives: an inner `let` of a
value used again, a `let` in a loop of a value made outside it, a `let` on one branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where an owning local has more than one receiver, each must be a `let` that borrows it:
a borrow pins its owner, so none of them may move it. `let b = a; let c = a;` is now
legal; a field store beside a borrow reports "'a' is moved here while 'b' borrows it".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec section 13: the borrow rule as built, the several-receivers rule, 1M-iteration
numbers (own 4.7-4.8 MB like rc; none up to 1.1 GB), the teeth run (8/8 borrow checks
fail with the borrower's releases kept), the tests that changed, and phase 3's inputs.

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

A borrow checked the borrower's reads and the casts of them, but not what those reads
produce: a bound method (`const f = b.m`), a field reference that a for...of reads on
every iteration, a catch or finally clause's non-owning local. Each of those still points
into the borrowed block, and four programs compiled into reads of freed memory when the
owner was overwritten. Every use of anything derived from a read that can point into the
block is now a use of the borrower; the walk stops at numbers and booleans, which also
ends a false escape on `if (b)` for a nullable borrower.

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