Skip to content

-mm=own, phase 4: function signatures and any - #406

Merged
ASDAlexander77 merged 7 commits into
mainfrom
own-phase-4
Sep 29, 2026
Merged

ASDAlexander77 merged 7 commits into
mainfrom
own-phase-4

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Phase 4 of -mm=own (spec §7): function signatures and any. Plan: docs/superpowers/plans/2026-09-29-own-phase-4.md. Results: spec §15.

What changes

  • OwnershipSignaturePass (new module pass, own only, just before inference). It resolves each call it can: direct calls, virtual calls whose family is all private and defined here, and the .instanceOf slot. It computes three facts per function and pins on each call the facts its candidates agree on:
    • __own_no_drops: the callee destroys nothing its caller can reach.
    • __own_result_borrows = K: the result is a borrow of argument K. Getters, return this.c, a returned parameter, ___unbox.
    • __own_params: parameters the callee keeps, by storing or pushing them.
  • Closed world. A caller that doesn't know a borrowed result or a kept parameter would double-free. So only a private function whose every use is a call this pass resolves (or a class vtable entry), in families that agree, gets those two facts. Otherwise its body keeps rc's convention, and its error gets a note saying why.
  • Inference:
    • a call drops a borrow only if its callee may drop, so `${this.color} area=${this.area()}` compiles;
    • a call whose result borrows is a borrow read, with its temporary release and a consuming let's releases erased;
    • a kept argument moves into the call; the callee's move must reach every return.
  • Soundness fix: a call given a borrow now uses it for as long as the callee runs. Phase 3 compiled f(h.c) where f resets h.c through a global, then reads its parameter.
  • any (this touches MLIRGen, so it changes rc and gc too):
    • the boxing cast now carries its birth reference, as a printed number does. This is rc's §14.5 leak fix: a box held by a folded const, or passed straight to a call, was never released;
    • the catch copy thunk's store takes that reference over;
    • under own, a copy thunk's read out of the exception object is a move (nothing gives it back), and <C>a borrows the payload through ___unbox.

Verification

  • Release suite 3229/3229, after merging main (JIT cache: each .ts file is compiled into an object kept in __jit #405).
  • Debug own tests 327/330. All three failures are known Debug-only ones:
    • test-jit-rc-owned-async and test-jit-none-owned-async: the ~AsyncToken assert;
    • ownership-verifier-4: the linkname DictionaryAttr assert, which also fires under gc.
  • Memory, AOT, identical at -O3 and -O1: own is flat at 4.8 MB on all four new positives, where none reaches 559–1658 MB. rc reads 12.6 MB on own_getter_borrow (see below).
  • Teeth, with the erasures for each verdict kept:
    • owned arguments: own_param_kept fails;
    • borrowed results: own_getter_borrow and own_any_box fail.
  • Corpus under own: 272 → 303 of 564. None lost. "Takes a second reference" fell from 189 to 149 files.
  • 4 new positives and 14 new negatives. Every positive runs under JIT and AOT, reads through a churn(), and was checked under gc first.

Rulings

  • A returned parameter is a borrowed result, not an owned parameter (Rust's elision). §2.4 read it the other way, which would make every <C>u move u.
  • A parameter kept on some paths only is an error. The fix, drop elaboration, is not in scope.
  • Export and import of facts is deferred. Exported functions get no owned or borrowed facts, which is sound. The three export_class_abstract* files from §14.4 therefore stay rejected: their class is exported, so area may be overridden elsewhere.

Found, not fixed here

  • rc never releases a getter's result used as a temporary (h.cc.x): 12.6 MB against own's 4.8 MB.
  • Every model: <B>anyValue segfaults when B implements an interface. mlirGenInstanceOfOpaque calls vtable slot 0 as .instanceOf, but such a class keeps the interface's vtable there. The fix changes the vtable layout.

🤖 Generated with Claude Code

ASDAlexander77 and others added 7 commits September 29, 2026 17:23
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OwnershipSignaturePass (new, own only, before inference) resolves every call it
can - direct, a virtual call whose family is all private and defined here, the
.instanceOf slot - and pins __own_no_drops where no candidate may destroy what
its caller can reach (least fixpoint; a constructor filling its own object is
not a drop). The inference's call rule asks it first.

A call given a borrow now uses it for as long as the callee runs: phase 3
skipped a call's own arguments, so f(h.c) with f resetting h.c through a
global and reading its parameter compiled. The function value an indirect
call goes through, and the object a field-held method is bound to, are read as
the call starts.

The helpers both passes need move to OwnershipFacts.h.

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

OwnershipSignaturePass gives a function __own_result_borrows = K when every heap
value it returns is, through views, opaque casts, ts.Unbox, field and element
reads, the object a method is called on and results that borrow, parameter K
(never assigned) - and only in a closed world: the function is private, every
use of its symbol is a call this pass resolves or a class vtable entry, and
every class family it is in agrees. Otherwise it keeps rc's convention, and the
error in its body says why (__own_facts_lost). A fixpoint lets a method that
returns another's borrow borrow too.

The inference treats such a call as a borrow read (wildcard places, roots
through the argument): its temporary release and its consuming let's releases
go, and a function returning a borrow drops the retain rc made for its caller.

Found on the way: getSymbolUses(module) does not look inside the module (it is
a symbol table), so the closed world saw no uses at all.

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

Owned-by-callee parameters (__own_params): a parameter (never assigned) that
the body stores into a field, element or global, pushes, or passes to a kept
parameter, under the same closed world and family meet as borrowed results.
The callee's retain goes, and the move must reach every return, not loop, and
be the last use. The caller's call is a taker of each kept argument: out of a
fresh value, an owning let or a kept parameter; anything else is an error.
Found by own_param_kept: a move whose acquisition is the call erased the call.

any: the boxing cast carries its birth reference (__owned_result + retain), as
a printed number does. That fixes rc's leak of every box no let holds, and
under own makes the box a fresh value with one owner. The catch copy thunk's
store takes that reference over (the catch slot is runtime memory), and a
thunk's read out of the exception object is a move: nothing gives it back.
___unbox gets a borrowed result through ts.Unbox, and its .instanceOf call is
drop-free, so <C>a borrows the payload for as long as the box lives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec section 15: the signature pass and its closed world, what the inference
accepts, measured numbers (own flat at 4.8 MB where none climbs to 1.6 GB),
teeth, tests and the corpus (272 -> 303 of 564, none lost), and the known
limits: export/import of facts deferred, parameters kept on some paths only,
wildcard places for borrowed results, interface calls unresolved.

Found on the way and left for their own work: rc never releases a getter's
result used as a temporary (12.6 MB in own_getter_borrow), and under every
model <B>anyValue segfaults when B implements an interface, since
mlirGenInstanceOfOpaque calls vtable slot 0 as .instanceOf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GCC rejects a default argument that value-initializes a nested struct with
default member initializers inside the enclosing class (walkBorrowed's
Keeping). The struct is a plain aggregate now, and the default is spelled out.

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