Skip to content

-mm=own phase 6: the corpus report; globals' initializers, data nothing owns, record literals - #445

Merged
ASDAlexander77 merged 6 commits into
mainfrom
own-phase-6
Oct 2, 2026
Merged

ASDAlexander77 merged 6 commits into
mainfrom
own-phase-6

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Phase 6 of -mm=own: the corpus report, and the shapes it says matter. The plan is docs/superpowers/plans/2026-10-02-own-phase-6.md; the results are in spec §20.

Rebased on main after #443 and #444 merged: 5 commits. On the rebased branch, the Release suite passes 3415 of 3416 (the same stale default-library test), and the own corpus is unchanged at 402 compiling files. The corpus now has 592 files; #443's new 00iterator_methods is rejected under own, as #443's comment documents.

The report

After 7b, 227 of the 591 corpus files fail under own. 137 of those stop at the catch-all "takes a second reference", which names no rule. A temporary dump at the catch-all's four report sites (kept out of the tree) split the 137 into shapes. Spec §20.1 has the tables, including how many files have each shape as their only blocker.

What this PR accepts

  • A global's initializer. It runs in the global's region, which no function's inference sees, so rc's retain for its ts.GlobalResult reached the lowering's guard ("left a retain behind", 8 files). The signature pass now erases that retain when three things hold:

    • the value is the only reference to its block;
    • it is fresh or constant data;
    • every use ends in the ts.GlobalResult: directly, through a view, as an array-literal element, or copied into an object literal's box.

    Any other retain there is reported by the global's name.

  • Data nothing owns (isConstantData, OwnershipFacts.h): a number or null in a union, a ts.Constant or a field of one, a function named by a ts.SymbolRef, and a tuple of these.

    • Its retain is erased.
    • A ts.RetainSlot only qualifies while nothing is stored into the slot or its fields.
    • It also has no use after a move: under --opt, CSE merges equal literals.
  • A record literal made for an owning local, let a = { item: new C() }, and a = {...}. This is 7a's "record built to be returned", with an owning local in place of the result slot.

For review

  • The two ownsHeaps disagree by design. isConstantData lives in OwnershipFacts.h and both passes read it. The header's ownsHeap looks at the type only; the inference pass overrides it for a ts.New seen as its object. That disagreement produced a regression while this PR was being built: 00object, one object literal stored into two records, which 7a rightly rejects, compiled again. Hence:
    • an explicit ts.New exclusion, pinned by the new negative own_err_literal_two_records;
    • an exclusion for values a ts.NewInterface view boxes. For own_err_interface_two_views, each exclusion alone keeps the test rejected.
  • The corpus script compiles without --opt, but the test runner uses --opt --opt_level=3. The gate (test-runner AOT and JIT on every newly compiling file) caught 01optional, which only failed under --opt. The final count is the same either way.

Verification

Tests

  • Positive: own_global_init, own_constant_data, own_record_local.
  • Negative: own_err_literal_two_records.

Not in this PR (spec §20.4-§20.6)

  • Next PR: named messages for the shapes the dump recognises (slot reads, conditional results, nested records), and a check of the use-after-move and borrows-a-field buckets for false positives.
  • Known limits:
    • a borrow of data nothing owns, past its owner's overwrite;
    • nested record literals;
    • arrays of record literals;
    • a conditional's result.
  • Found, not fixed (every model):
    • A global initialized from another global reads garbage (let b = a at module level). With a class, the program faults.
    • A module with a top-level statement, imported as source, fails with invalid LLVM IR.
    • With -shared, an exported const from such a module can't be resolved.

🤖 Generated with Claude Code

ASDAlexander77 and others added 5 commits October 2, 2026 17:36
…global

A global's initializer runs in the global's own region, which no
function's inference sees, so rc's retain for its `ts.GlobalResult` was
left behind and the lowering failed the compile ("ownership inference
left a retain behind", with no location). A global is a root: what its
initializer makes moves into it. The signature pass erases a retain in a
global's region when the value is the only reference to its block
(retained once, released nowhere) and is fresh, constant data, or a
record of constants, and every use of it ends in the `ts.GlobalResult`:
directly, through views, as an element of an array literal, or copied
into an object literal's box. Any other retain there is reported by the
global's name.

isConstantData (OwnershipFacts.h): what holds no block, a `ts.Constant`
or a field of one, and a tuple of such.

The phase 6 plan: docs/superpowers/plans/2026-10-02-own-phase-6.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc retains a value wherever its type could hold a block. A number or
null in a union, a function named by a `ts.SymbolRef`, a string literal,
and a tuple of such hold none, so the retain takes nothing: it is
erased instead of reported as a second reference. A `ts.RetainSlot` is
decided on the slot's initializer, so only while nothing else is ever
stored into the slot and no closure captures it.

Such data has no use after a move either: under --opt, CSE merges equal
literals (the left-out optional field of two tuple literals), and one
value then goes into several places (checkValueMoves).

isConstantData excludes two things a type-only reading takes for data:
a `ts.New`, whose `value_ref` type owns nothing but which is the object
its view shows (`00object` stores one into two records), and a value a
`ts.NewInterface` view boxes into a block of its own each time
(own_err_interface_two_views).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The slot guard for data nothing owns took any use of the slot but a load
for a write, and a tuple local's `a[0]` reaches it through a
`ts.PropertyRef`. A field ref whose uses only read is a read.

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

`let a = { item: new C() }` builds the literal in a local a `ts.Constant`
starts, stores the fresh value into its field, and reads the record once
into `a`. That is the record built to be returned (recordRetainOf) with
an owning local in place of the result slot: the fresh value moves into
the record, and the record into `a`, whose `ts.RetainSlot` is rc's
reference for the values moved in. `a`'s scope exit releases it. The
assignment form stores the read into an owning local, and is accepted
the same way.

Measured (AOT, measure.ps1, a loop building and dropping such records):
own 4.2 MB at both 20000 and 200000 iterations, as rc; none 7.7 -> 35.2.

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 cd7c284 into main Oct 2, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-phase-6 branch October 2, 2026 17:16
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