JakeChampion / JakeChampion/lang
architecture: irlower's RC/ownership facts are a scopeless, string-multiplexed side table keyed by variable NAME — the second half of the #5531 brittleness
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 1
- Forks
- 0
- Avg merge
- 1h 37m
- Merged PRs (30d)
- 977
Description
Companion to #5531 / #5986, covering the axis those two do not.
#5531 and #5986 diagnose one half of irlower.fern's brittleness precisely: type
facts are re-derived at lowering time instead of read from the checker, and the fix is
annotate-and-consume. That work is live and landing (docs/TYPED-IR-REWRITE.md,
carriers through ExprSlice.ty).
The other half is untracked: ownership facts — which slot is fresh, borrowed,
aliased, moved, reclaimable, a view, reusable — are carried in a hand-rolled untyped
map that is keyed by the variable's name and has no notion of scope.
The shape
LowerState.reclaimable_names: string[] is not a list of reclaimable names. It is a
multiplexed side table: entries are "<TAG>:<name>" or "<TAG>:<name>|<value>",
decoded by reclaim_has / tagged_value_of. Measured on origin/main today:
distinct "TAG:" namespaces multiplexed into that one field |
73 |
reclaim_has(s.reclaimable_names, …) probe sites |
38 |
sibling name-set fields on LowerState (aliased_names, borrowed_names, moved_names, own_params, grow_sole, grow_exempt, append_inplace, xblock_pending, optret_pending, defer_slots, move_sites, …) |
30-field struct, ~half of it |
name-keyed module registries on FnSigs |
27, all string[] |
LocalInfo flags encoding one slot's type as parallel booleans + comma-joined strings |
21 fields |
LocalInfo shows the cross-product problem directly: is_arr, is_strarr,
is_boolarr, is_f64arr, is_arrarr, is_closurearr, is_fnarr are seven flags
spelling one fact (Array[elem]) that a structured type spells once.
Why it is brittle, not merely ugly
The key has no scope. Fern has lexical scoping; a name in this table does not. So
every consumer that could be wrong about which binding a name refers to has to
hand-guard it. irlower.fern mentions shadowing 79 times, and carries
body_declares_name at 14 sites — a predicate that exists solely to ask "did anyone
rebind this name?", because the table cannot answer. docs/rc-log/2026-08-20-tostring-callee-local-recv.md
states it plainly: "The scan has no scopes. A nested block may shadow the name."
Each of those guards is correct where it was written and absent where it was not. That
is the defect generator:
- #6191 — the lift's name-keyed lookup resolved a fn-typed parameter to a module
function of the same name. Fixed at that site. - #6283 — the return-type registry did the same thing one level over: a user
function namedfreturningi64silently retyped the parameterfinside
std/array'smap/flat_map, producing an IR bail on x86-64 and a wasm module
that fails to load, blaming stdlib the program never called. Fixed at that site
(#6290). docs/RC-PERCEUS-SELF-HOST-PORT.mdrecords ~a dozen more shapes where a slice had to
add its own shadow refusal ("refusing a local that shadows the declaration",
"a frame that shadows the callee refuses the store", §9 lines 7145, 8347, 8398,
9078, 9117, 9169, 9406). One of those notes says outright: "No probe witnesses that
guard."
Two independent instances of the same root cause were each closed as their own bug.
Neither closed the class.
The encoding also costs. §4d.4 of docs/PERFORMANCE-AUDIT-2026-08.md is four PRs
(#7020, #7026, #7036, #7046) spent making the string decoding of these registries
cheap — self-compile 78.8 s → 42.4 s user, ≈ −46% — without changing the design. #7046
alone deleted ~83,000 string concatenations per module that existed only because the
facts are strings. A typed, slot-keyed table would not have generated any of that work.
And it is growing. docs/TYPED-IR-REWRITE.md sized irlower.fern at 39.8k lines
(2026-07-23); #5986 at 43,204 (2026-08-02); it is 60,339 today — +51% in under a
month, against checker.fern at 13.1k and parser.fern at 19.0k. The four largest
functions are 1,786 / 1,629 / 1,603 / 1,486 lines. Every goal-2 slice adds another
"TAG:" namespace and another set of hand-guards, so the eventual consolidation gets
larger at roughly the rate the feature work goes in — the same argument #5986 makes for
the type axis, on the axis with no issue behind it.
What the fix looks like
Not a rewrite, and deliberately not blocked on Phase B (SSA). The move is the same
"stop re-deriving what is already known" as #5531, applied to ownership:
- Key by slot, not by name. Every consumer already has the slot index (
LocalInfo
is indexed by it). Replacing thestringkey with the slot number makes shadowing
structurally impossible and deletes the 14body_declares_nameguards and the 79
shadow caveats along with it. This alone closes the #6191 / #6283 class. - Give the facts a type. Replace the 73 string prefixes with a
SlotFactsstruct
(or a small tagged enum per fact family) hanging offLocalInfo. The prefixes are
already a de-facto enum; the compiler should be checking it. Deletesreclaim_has,
tagged_value_of, and the concatenation traffic #7046 was fighting. - Collapse the
LocalInfocross-product into the structured type the typed-IR
carriers are already delivering — this is where the two issues meet, and why they
should be sequenced together rather than merged. - Split the file. 60k lines in one module is its own hazard: the four >1.4k-line
functions are where the sibling-arm bugs live (docs/TYPED-IR-REWRITE.md:
"the tuple DESTRUCTURE has its own copy of thatExprIndexarm … only one of them
was fixed").
Same migration discipline as #5986: one fact family at a time, byte-identical
self-compile fixpoint per step, delete the string form when no consumer reads it. Note
the #5986 caveat applies with more force here — these facts drive the exit dec-sweep,
so internal/e2eselfhost is the primary gate and the fixpoint is secondary
(docs/TEST-GATES.md).
Acceptance test for step 1
function f(): i64 { return 12i64; }
function main(): i32 { return f() as i32; }
with import "std/array"; — #6283's repro, which passes today only because #6290
patched one lookup. A slot-keyed table makes the whole family unreachable rather than
individually guarded.
Refs #5531, #5986, #6191, #6283, #4451.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with irlower.fern, especially LowerState, LocalInfo, reclaim_has, and tagged_value_of, then read docs/TYPED-IR-REWRITE.md and docs/TEST-GATES.md. Begin with the #6283 acceptance program and run internal/e2eselfhost. Done means one ownership fact family is slot-keyed, its string form and consumers are removed, and the self-host gate passes.
Written by the indexing model from the issue text.
Assessment
- Domain
- compilers
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100