fix(phase-29): T60 — reconcile_def_sig name bug fixed (verdicts moved); 0 banked, three causes separated

Tool fix + a sharper diagnosis. NO BANKS — the three "header-conflict" families share a SYMPTOM, not
a cause.

THE FIX (cookbook §109): reconcile_def_sig now conforms the canonical TYPES and keeps the BODY's
parameter names, parsed with cdecl (base/params/pnames, R33 — not a regex). Two re-render traps
handled: `void*` + `a1` -> `void *a1` (cdecl glues stars to the type), and an EMPTY parameter list is
handed back verbatim because `(void)` and `()` both parse to params==[] and are DIFFERENT
declarations (§99 no-prototype). Unit-tested across 6 shapes incl. both void forms and an arity
mismatch; falls back to the wholesale canonical string for fn-ptr/array params.

THE FIX IS REAL, AND THE PROOF IS THAT THE VERDICTS MOVED:
  func_8016163C  `param_1 undeclared` -> DIFF                     (plumbing CLEARED; codegen left)
  func_8014D610  `param_1 undeclared` -> `void value not ignored` (the HEADER is wrong)
  func_80156044  unchanged -> `conflicting types for func_80155FF8` (WRONG LEVER — callee conflict)

TWO FINDINGS UNDER THAT:
 1. The §85 return-axis precondition applies to reconcile_def_sig and NOTHING CHECKS IT. Conforming a
    def's return to the canonical `void` is only safe when no caller consumes the return.
    func_8014D610's callers do, so engine_core.h's `void` contradicts the byte truth and conforming
    yields `void value not ignored`. The HEADER is the wrong artifact; correcting it is fleet-shared
    blast radius (§61/§63), not a sweep-time fix.
 2. func_80156044 was never the def-signature class — its conflict is on the CALLEE func_80155FF8
    (decl 2 lines above the splice). That is cast_call_sites / canon_sig_reconcile territory.

HONEST ACCOUNTING: re-swept all three with the fix -> 0/411, tree clean throughout. The lever is now
correct (it no longer manufactures a false compile failure) but it was ONE of three causes, not the
cause. My T59 write-up grouped them as a single ~30,000-instruction block; that grouping was WRONG,
and what disproved it was re-reading each verdict after the fix rather than re-running the batch and
reporting the total.

No src/ or config/ change: no bank, no metric move.
This commit is contained in:
Drew T
2026-07-28 21:11:44 -06:00
parent 28ef237c35
commit 50a108b5a8
3 changed files with 150 additions and 5 deletions
+40
View File
@@ -7587,3 +7587,43 @@ both are already in hand at the call site.
> (§107), off by default, or subtly broken — never the compiler. **Before diagnosing a family as
> hard, enumerate the levers the invocation actually enabled, then read one member's real cc1
> output.** A sweep's `0/N` is a statement about the harness.
---
## §109 — Conforming a definition to a shared header: fix the NAMES, then check the RETURN precondition (Phase 29 T60)
`reconcile_def_sig` substituted the canonical header decl **wholesale — types AND parameter names** —
while the body kept the exemplar's names. Its docstring called this a "rare name mismatch" the gate
would reject; both halves were wrong. An exemplar drafted with the `param_N` convention hits it
**every time**, and the outcome is not a rejected match but the **whole family booking as a compile
failure**, indistinguishable from a compiler wall:
```
canonical : void func_8014D610(s32 a0, void *a1, void *a2)
draft body: ... param_1 ... -> `param_1' undeclared (first use this function)
```
**Fix: conform the TYPES, keep the BODY's names** — both are in hand at the call site. Parse with
`cdecl` (`base` = return type, `params` = types, `pnames` = names), not a regex. Two traps in the
re-render: `void*` + `a1` must become `void *a1` (cdecl glues the stars to the type), and an **empty
parameter list must be handed back verbatim** — `(void)` and `()` both parse to `params == []`, and
they are different declarations (§99: `()` is the no-prototype form).
**The fix is real but it is not sufficient, and the proof is that the verdicts MOVED:**
| family | before | after | what is left |
|---|---|---|---|
| `func_8016163C` | `param_1 undeclared` | **DIFF** | plumbing fully cleared; genuine codegen |
| `func_8014D610` | `param_1 undeclared` | `void value not ignored as it ought to be` | **the header is wrong** |
| `func_80156044` | *unchanged* | `conflicting types for func_80155FF8` | **wrong lever** — a CALLEE conflict |
> **The §85 return-axis precondition applies here too, and nothing checks it.** Conforming a
> definition's return type to the canonical `void` is only safe when **no caller consumes the
> return**. `func_8014D610`'s callers do — so the header's `void` contradicts the byte truth, and
> conforming to it produces `void value not ignored`. The header is the thing that is wrong; changing
> it is fleet-shared blast radius. `reconcile_def_sig` should test that precondition before promoting
> or demoting a return type, exactly as §85 requires of `conform_decls`.
> **And a verdict that changes is the signal to re-route, not to push harder.** One of these three
> is now a codegen question, one is a header-correctness question, and one was never the def
> signature at all. They shared a symptom, not a cause — three families, three levers.
+50
View File
@@ -8130,3 +8130,53 @@ T56 a lever unreachable from the sweep path · T57 a lever off by default · T59
4. **Probe the distinct-code +0 anomaly** (4 data points, no identified variable).
5. **~50 more eligible families** — regenerate `family_hseq.py` first (the map predates T56–T58), and
select exemplars with **`corpus.stubs`**, not a name-grep.
## 🔧 T60 — `reconcile_def_sig`'s name bug FIXED (verified by moved verdicts); **0 banked**, three causes separated
Tool fix + a sharper diagnosis. **No banks** — the three "header-conflict" families turned out to
share a *symptom*, not a cause.
### THE FIX (§109)
`reconcile_def_sig` now conforms the canonical **TYPES** and keeps the **BODY's parameter names**,
parsed with `cdecl` (`base`/`params`/`pnames`, R33 — not a regex). Two re-render traps handled:
`void*` + `a1` → `void *a1` (cdecl glues stars to the type), and an **empty parameter list is handed
back verbatim** because `(void)` and `()` both parse to `params == []` and are *different*
declarations (§99 no-prototype). Unit-tested across 6 shapes incl. both void forms and an arity
mismatch; falls back to the wholesale canonical string for fn-ptr/array params it cannot re-render.
### THE FIX IS REAL — AND THE PROOF IS THAT THE VERDICTS MOVED
| family | before | after | what is actually left |
|---|---|---|---|
| `func_8016163C` | `param_1 undeclared` | **DIFF** | plumbing fully cleared — genuine codegen |
| `func_8014D610` | `param_1 undeclared` | `void value not ignored as it ought to be` | **the header is wrong** |
| `func_80156044` | *unchanged* | `conflicting types for func_80155FF8` | **wrong lever** — a CALLEE conflict |
### THE TWO FINDINGS UNDER THAT
1. **The §85 return-axis precondition applies to `reconcile_def_sig`, and nothing checks it.**
Conforming a def's return type to the canonical `void` is only safe when **no caller consumes the
return**. `func_8014D610`'s callers do — so `engine_core.h`'s `void` contradicts the byte truth,
and conforming to it yields `void value not ignored`. **The header is the wrong artifact here**,
and correcting it is fleet-shared blast radius (§61/§63) — not a sweep-time fix.
2. **`func_80156044` was never the def-signature class.** Its conflict is on the CALLEE
`func_80155FF8` (decl 2 lines above the splice). `--fix-def-sig` cannot touch it; that is
`cast_call_sites` / `canon_sig_reconcile` territory.
### HONEST ACCOUNTING
Re-swept all three with the fix: **0/411**, tree clean throughout. The lever is now correct — it no
longer manufactures a false compile failure — but it was **one of three causes, not the cause**. My
T59 write-up grouped them as a single 30,000-instruction block; that grouping was **wrong**, and the
thing that disproved it was re-reading each verdict after the fix rather than re-running the batch
and reporting the total.
## ▶ NEXT (ranked, all measured)
1. **`func_80156044`** (137) — a CALLEE decl conflict (`func_80155FF8`); route to `cast_call_sites` /
`canon_sig_reconcile`, not `--fix-def-sig`. Cheapest of the three.
2. **`func_8016163C`** (137) — now a clean **DIFF**: measure closeness and route per §31. Likely the
`s32`→`void` return demotion costing an instruction (§85) — if so, it needs the header corrected,
which merges it with item 3.
3. **`func_8014D610`** (137) — `engine_core.h` declares it `void` but callers consume the return.
Correcting the header is fleet-shared (§61/§63, R22-mandatory); size the blast radius first.
4. **Add the §85 return precondition to `reconcile_def_sig`** — it should refuse to demote a return
type when a caller consumes it, instead of emitting code that cannot compile.
5. `0x80143d28` (136) `ApplyMatrixSV`; `0x801457a4` (137) the original DIFF; the distinct-code +0
probe; ~50 more families (regenerate the map; select with `corpus.stubs`).
+60 -5
View File
@@ -30,6 +30,7 @@ import scope_tu_externs as STU # §103 — the TU-side decl-scope lever
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO, 'tools'))
import corpus # the derived corpus oracle (Phase 26-A)
import cdecl # the coverage-asserting C-declaration parser (Phase 26-A) — used by _merge_sig
PY = ".venv/bin/python"
_OV_LOCKS = __import__("collections").defaultdict(_th.Lock) # two splits of ONE overlay build the same binary
SWEEP = ".run/sweep"
@@ -80,10 +81,25 @@ def header_sig_map():
def reconcile_def_sig(draft, to_func, smap):
"""If `to_func` is forward-declared in a shared header, rewrite the draft's DEFINITION signature
(return type + param list) to that canonical decl, so the member TU stops seeing conflicting types.
Body param names come from the header decl (auto-generated a0/a1/... on both sides, so they align in
the common case; a rare name mismatch makes the body reference an undeclared name → the gate rejects
it, never a false bank). Returns the draft unchanged if the fn isn't in a header or no def is found."""
to the canonical TYPES while KEEPING THE BODY'S PARAMETER NAMES, so the member TU stops seeing
conflicting types without the body losing the identifiers it references.
THE NAME BUG THIS FIXES (Phase 29 T59/T60). The first cut substituted the canonical decl string
WHOLESALE — types AND names. Its docstring called a name mismatch "rare", and said the gate would
reject it. Both were wrong in the way that matters: an exemplar drafted with the `param_N`
convention hits it EVERY time, and the result is not a rejected match but a whole family booking
as a compile failure — indistinguishable from a compiler wall:
canonical : void func_8014D610(s32 a0, void *a1, void *a2)
draft body: ... param_1 ... -> `param_1' undeclared (first use this function)
Three families / 411 members sat at 0/N on exactly this. The types are what cc1 compares; the
names are the body's own business, and both are in hand right here.
Parsed with `cdecl`, not a regex (R33): `base` is the return type, `params` the types, `pnames`
the names. Falls back to the wholesale canonical string when a canonical param type is one this
cannot safely re-render (a function-pointer or array parameter) — honest, and no worse than before.
Returns the draft unchanged if the fn isn't in a header or no def is found."""
canon = smap.get(to_func)
if not canon:
return draft
@@ -92,7 +108,46 @@ def reconcile_def_sig(draft, to_func, smap):
m = pat.search(draft)
if not m:
return draft
return draft[:m.start()] + m.group(1) + canon + " {" + draft[m.end():]
return draft[:m.start()] + m.group(1) + _merge_sig(m.group(0), canon, to_func) + " {" + draft[m.end():]
def _render_param(ctype, name):
"""`void*` + `a1` -> `void *a1`; `s32` + `a0` -> `s32 a0`. cdecl renders a param type with its
stars glued on, which is not how a declarator is written."""
stars = len(ctype) - len(ctype.rstrip('*'))
return f"{ctype.rstrip('*')} {'*' * stars}{name}" if name else ctype
def _merge_sig(def_line, canon, to_func):
"""Canonical TYPES + the definition's own parameter NAMES -> a signature string (no trailing brace).
Positional: name[i] comes from the draft where it has one, else from the canonical decl. An arity
mismatch is not smoothed over — the canonical arity governs the declaration (it is what callers
see), and if the draft had MORE parameters its extra names simply are not declared, so the body
references an undeclared identifier and the byte-gate rejects it. That is the correct outcome for
a genuinely disagreeing signature; it must not be papered into a false bank (G3/P9)."""
try:
cd = cdecl.parse(canon.rstrip('; ') + ';')[0]
dd = cdecl.parse(def_line.strip().lstrip('\n').rstrip('{ \t\n') + ';')[0]
except Exception:
return canon
ctypes = cd.params or []
if any(('(' in t or '[' in t) for t in ctypes): # fn-ptr / array param: cannot re-render safely
return canon
if not ctypes:
# `(void)` and `()` both parse to params==[] — and they are NOT the same declaration
# (§99: `()` is the no-prototype form, which changes argument promotion). There are no names
# to preserve here anyway, so hand back the canonical string and keep whichever form the
# header actually wrote.
return canon
dnames = list(dd.pnames or [])
cnames = list(cd.pnames or [])
out = []
for i, t in enumerate(ctypes):
nm = (dnames[i] if i < len(dnames) and dnames[i] else
(cnames[i] if i < len(cnames) and cnames[i] else f"a{i}"))
out.append(_render_param(t, nm))
return f"{cd.base} {to_func}({', '.join(out)})"
def draft_def_ref(draft, to_func):