4.3 KiB
§64 — The §20 type-lift's three laws: fold the tagged typedef, check VISIBILITY, and strip only what is TEXTUALLY IDENTICAL (lift_types.py, Phase 29 SESSION-14, 2026-07-23)
The setup. The broad §20 lift (154 fleet-local types → engine_types.h, 2,958 files stripped) had been
carried three sessions as "needs collision-vetting + -O0 strip precision." Both framings were wrong, and the
one real collision class was misdiagnosed. Fixing the tool first (R35) changed all three answers.
Law 1 — a tagged typedef is ONE entity, not two. typedef struct Tag {...} Alias; is matched by BOTH
find_defs (as Tag) and find_typedefs (as Alias), with the struct span CONTAINED in the typedef span.
Treating them as independent does three wrong things at once: it lifts the inner span, which starts at
struct, so the header gets struct Tag {...} Alias; — a variable definition of Alias in every TU; it
then lifts the typedef too (Alias redeclared as different kind of symbol); and it strips both spans
highest-first, so the outer span's end offset is stale by the length of the inner one and the second
delete removes that many EXTRA characters past its intended end. MEASURED: 13 tag/alias pairs, 6,142
occurrences, and 0 of those tags is ever defined standalone — folding is always the correct read.
build_engine_types.resolve_type_defs() is now the ONE model both tools call (R33), and it guarantees
defs/tdefs spans are pairwise disjoint; assert_disjoint() enforces that at every mutation (R32).
The Phase-29 "case-variant name collision" (
actor4cvsActor4C) was this, not a naming problem. A single-def filter accidentally hid 12 of the 13 pairs and leaked exactly the one whose tag and alias differ in case, which made a case-insensitive exclude look like the fix. It is not: it would also wrongly drop the legitimate, non-collidingObj/objandVec/vecpairs. Key by (kind, name) — the C namespace.
Law 2 — never strip a definition out of a TU that cannot SEE the replacement. MEASURED: exactly 1 TU
of 3,226 (ov_SC01_077_o0.c, the -O0 split) deliberately omits engine_core.h — for the documented reason
that its -O0 functions cannot propagate through a header other overlays compile at -O2. Stripping its types
does not consolidate them, it DELETES them, and the symptom surfaces three steps downstream: type undeclared
→ parse error on the next declaration → "data definition has no type or storage class" → gcc falls back
to implicit int → that TENTATIVE definition collides at LINK as multiple definition of D_801DAA08 — a
link error naming a data symbol nobody touched. That is the whole of the "-O0 strip precision" mystery.
bet.type_visible(path) derives the visible-header set from the src/shared include graph (R33) and the
lift keeps such defs local, naming them.
Law 3 (the expensive one) — strip a local def ONLY if what becomes visible is TEXTUALLY IDENTICAL to it.
--candidates classifies per ENTITY and defers VARIANTs, but --types takes NAMES: struct Prim was
LIFTABLE while typedef Prim was a deferred VARIANT (header ≠ the 103 overlay copies), and passing the
shared name dragged the deferred entity back in. Result: 103 overlays silently repointed at a different
Prim layout. It compiled clean, the per-binary pre-filter passed, and R22 failed 37/140 — the 103
failures were EXACTLY the 103 Prim-stripped overlays (set equality, byte-verified). The guard now lives at
the mutation, not in the selector, so a selector bug cannot reach the source; divergent copies are reported
as the per-camp reconcile work they are.
Sequencing that paid for itself. make build BINARY=ov_SC01_077 reproduced the -O0 failure in 0.26 s
with a compile error naming the five affected types, where the fleet cycle had produced a link error naming
an unrelated symbol. But a pre-filter is only evidence about what it filtered: ov_SC01_077 passed the
Prim-broken run too. Pre-filter on a binary that FAILED, not one that passed (§61 one level down).
Result. 154 types lifted, 2,958 files stripped, R22 140/140 byte-identical, engine_types.h +510
lines. Deferred and named, not silently dropped: 8 VARIANT entities (MATRIX/Buf/Vec8/Prim/Handler/…) awaiting
the per-camp field-access reconcile, 14 carried tags, and 5 types kept local in the -O0 TU.