Repository navigation
-mm=own phase 7a: generators and async; an interface is a view of its object - #439
Merged
Merged
Conversation
The maker of a generator makes its state object with `ts.New` and casts the `value_ref` to the
`!ts.object` it returns. The cast is the same block, so it is now a view, and the maker's result is
fresh: a generator whose locals own nothing compiles. An object literal with methods is the same
shape.
With that, `let raw = {...}; let i = <I>raw` gave `raw`'s block two owners, because the object
literal's interface cast is a `ts.NewInterface {__owned_result}` and `isFresh` took the mark at its
word. A `ts.NewInterface` is now fresh only when the object it wraps is fresh and goes nowhere
else.
Plan: docs/superpowers/plans/2026-10-02-own-phase-7.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate holds nothing The first commit's rule for `ts.NewInterface` (fresh only when its object is used nowhere else) was sound but lost twelve corpus files that compile on main, and main was not sound either: a folded object literal given twice as an interface (`use(o); use(o)`) is freed twice there. An interface over an object is now a view of it, as a union or an optional is, and a view is as fresh as its root: - `let ri = <I>raw` borrows `raw`. rc's `ts.Retain` of the interface is the reference of the `let` that consumes it, and a borrow erases it. - A class instance behind an interface is one block, so `own_err_interface` compiles (its two instances are released once each) and is gone, with the first commit's alias negative. - Two releases of one block, one reachable after the other, are two owners: the error `own_err_interface_two_views` pins. Checking the view's root showed that it had not been checked at all: `ownsHeapMemory` says a `value_ref` owns nothing, so a `ts.New`'d block passed every check. Inside the pass such a block seen as its object owns now, and the store that fills it is a borrow, not a taker. The initial state a generator's maker copies into its state object is a tuple loaded from a local a `ts.Constant` initializes. Where nothing stores into its owning fields it holds nulls, numbers and immortal literals, and rc's retain of it is erased: a generator whose locals own blocks compiles. Corpus: 325 of 591 on main, 353 now. Two files that compiled on main are errors now, both correctly: `00object` stores one object literal into two tuples (rc leaks it), and `00interface_object_array` reads an object after it moved into an array. Every newly compiling file runs under test-runner -mm=own, AOT and JIT, with gc's output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s are inferred
A `yield v` stores `v` into a `{value, done}` record and returns it, and the caller calls `next`
through the state object. Three pieces:
- A record built to be returned is a local a `ts.Constant` initializes, read once, the read only
returned. A fresh value stored into one of its fields moves in: the store is the value's taker,
and rc's retain of the record, after it is filled, is the acquisition, so the temporary's
release after it goes. The record's own retain goes with the initial-state rule.
- The signature pass resolves a function read out of an object's field when that field only ever
holds one function: every `ts.Constant` of the object's tuple type holds it there, nothing
stores into the field (a method sees its object as `object_storage`, whose fields count too),
and no other op makes a value of the type. A generator's `next` is such a field. The call gets
`__own_fresh_result`: `next` is open (its symbol sits in a constant), so its body may not return
a borrow.
- The inference runs on every function, including the `func.func` the async lowering outlines a
`for await` body or an `await`'s continuation into. Before, a retain there was neither erased
nor reported, and `00for_await_yield` failed in the lowering with "ownership inference left a
retain behind".
Also two tightenings of the previous commit: a store through a value is a borrow only for a
`value_ref`, and a borrow erases only the `ts.Retain` its `let` was found from.
`own_generator` yields fresh objects and strings, read past a churn() by for...of and by hand:
gc 6.1 MB, rc 11.2, none 1221.0, own 4.3. Found, not fixed (rc and own): a temporary made in front
of a `yield` in the generator's body is never released, since OwnedReturnConsumptionPass does not
release past a resume point (`yield "s" + i` leaks the number's string), and rc leaks yielded
objects.
Corpus: 355 of 591 (main 325).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ect; spec section 18 A generator's maker puts the cells of its parameters into a capture box and the box into the state object's `.captured` field. There is no closure value, but the box is an escaping closure's: the store into the state is its escape, and phase 5b's rules apply as they are. A cell holding a number moves in (`range(from, to)`); a parameter's object is the 5b error "its value is the caller's" (`own_err_generator_param_object`). rc's `ts.RetainCell` of the box is the state object's reference and moves in with it. An object literal whose methods capture keeps its box the same way. Tests: `own_generator` gains number parameters, and `own_record_return` (a plain function returning a record that holds a fresh object) and `own_async` (from the phase's probe, plus a `for await` body) are new. The no-counting check fails on `error:` now: `error` alone matched the async runtime's message "Awaited async operand is in error state". Teeth, each a temporary getenv switch: keeping the frame's ReleaseCell after a move into a generator's box, or a value's temporary release after it moves into a returned record, fails under AOT (0xC0000374) and JIT; skipping the released-twice check, or resolving a reassigned field, makes the matching negative compile. Corpus: 325 of 591 on main, 359 now. Spec section 18 has the results, the found-not-fixed list, and the input to 7b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The signature pass collected only the calls inside a `ts.Func`. A call in the `func.func` the async lowering outlines a `for await` body into got no facts, while findOpen counted it as a resolved call. So a callee that keeps its argument (`stash(c)` pushing into a global) was closed and moved the argument in its own body, while the body that called it kept its release: a use after free, on main as well. Calls, and callee-only references, are collected in any function now. Verified in the IR: the release after `stash(c)` is gone. A run cannot show it, under any model: `await f()` returns before `f`'s `for await` body has run (spec 18.5). Spec section 18: that, and the wording of 18.2 and 18.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 7a of
-mm=own: generators and async (spec §2.7, §7 phase 7). The plan isdocs/superpowers/plans/2026-10-02-own-phase-7.mdand the results are in spec §18. The PR also fixes two use-after-free holes that already exist on main.Pre-existing holes, fixed
ts.NewInterface__owned_result, andisFreshread that as a block of its own. On main,const o = {...}; use(o); use(o), withusetaking an interface, freesotwice and crashes.ts.NewInterfaceis now a view of its object, the way a union or an optional is, and a view is exactly as fresh as its root:let i = <I>rawborrowsraw. rc'sts.Retainof the interface is the reference of theletthat consumes it, and a borrow erases it.own_err_interface_two_views.for awaitbody into afunc.func, and neither own pass looked inside one. A retain there failed in the lowering (00for_await_yield). Worse, a call there to a callee that keeps its argument got no facts: the callee moved the argument while the body still released it. Both passes now run on every function. This one is verified in the IR only, because a run can't show it (see the last section).Generators
ts.Newand returns it cast to!ts.object, and that cast is a view. In the pass, ats.New'dvalue_refseen as its object owns a block, so it is checked:ownsHeapMemorysays no for everyvalue_ref. The store that fills it is a borrow.yield vbuilds a{value, done}record and returns it. A record built to be returned takes the fresh values stored into it: each store is that value's taker. The same rule covers a plainreturn { value: new C(), done: false }.next()goes through the state object's field. The signature pass now resolves a function read from a field when that field only ever holds one function, and marks the call's result fresh..capturedfield. That box is treated as an escaping closure's box, so the 5b rules apply: a number parameter moves in, and an object parameter is 5b's "its value is the caller's" error. An object literal whose methods capture keeps its box the same way.For review:
fieldFunctions(signature pass) is a closed-world rule over the module. It doesn't consultopenor privacy. It requires that everyts.Constantof the tuple type holds the same function in that field, that nothing stores into the field (from a method too, which seesobject_storage), and that no other op makes a value of the type. It is safe for a generator's state type because the type ofnextcarries a per-siteobject_storagesymbol that no other module can spell.own_err_field_function_reassignedpins the "nothing stores into it" condition.Verification
Release suite: 3383 of 3384 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/-mm=own phase 5b: a closure that escapes owns what it captures; a call in a try body is a call #437: the installed default library atI:\tslangis stale. All 381 own tests pass.WSL/GCC: builds, and 380 of 380 own tests pass (Windows has one more, the Windows-only shared-library check).
Corpus: 325 → 359 of 591.
-mm=own, AOT and JIT, and so does every-sharedpair whose two halves both compile. Their JIT output matches gc's, except the two async files, which only run under test-runner.00objectstores one object literal into two tuples, and rc leaks it there.00interface_object_arrayreads an object after moving it into an array.Memory (AOT,
measure.ps1, MB):own_generatorown_record_returnown_interface_viewown_asyncTeeth: each new rule was undone with a temporary switch:
ReleaseCellafter a cell moves into a generator's box breaksown_generator, AOT (0xC0000374) and JIT;own_record_return, AOT (0xC0000374) and JIT;own_err_interface_two_viewscompile and crash;own_err_field_function_reassignedcompile.Tests
own_generator: numbers, a class local, a reassigned local, a generator given up early, fresh objects and strings yielded throughfor...ofand by hand, number parameters, an object literal with a method.own_record_return.own_interface_view.own_async: a heap local acrossawait, a fresh result, a heap parameter, a string, afor awaitbody.own_err_interface_two_views,own_err_field_function_reassigned,own_err_generator_param_object.own_err_interface, a phase 0 placeholder for an interface over a class instance, which now compiles.error:now, becauseerroralone matched the async runtime's message string "Awaited async operand is in error state".Found, not fixed
yieldis never released, under rc as well.OwnedReturnConsumptionPassdoesn't release past a resume point, soyield "s" + ileaks the string made fromi.own_generator.await f()returns beforef'sfor awaitbody has run, under every model: afterfor await (const x of g()) keep.push(1),keepis empty. That's why the async-body facts fix can't get a runtime test..map/.filter; and yielded borrows.🤖 Generated with Claude Code