From 50a108b5a8821ce07b6ecbb92fe7d989c2fef2f0 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:11:44 -0600 Subject: [PATCH] =?UTF-8?q?fix(phase-29):=20T60=20=E2=80=94=20reconcile=5F?= =?UTF-8?q?def=5Fsig=20name=20bug=20fixed=20(verdicts=20moved);=200=20bank?= =?UTF-8?q?ed,=20three=20causes=20separated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/matching-cookbook.md | 40 +++++++++++++++++++++++ phase-ends/CURRENT_PHASE.md | 50 ++++++++++++++++++++++++++++ tools/family_sweep.py | 65 ++++++++++++++++++++++++++++++++++--- 3 files changed, 150 insertions(+), 5 deletions(-) diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index 5af7662eb4..b40771cfc5 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -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. diff --git a/phase-ends/CURRENT_PHASE.md b/phase-ends/CURRENT_PHASE.md index cdff7b89be..ebe9984fff 100644 --- a/phase-ends/CURRENT_PHASE.md +++ b/phase-ends/CURRENT_PHASE.md @@ -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`). diff --git a/tools/family_sweep.py b/tools/family_sweep.py index c31445ca7e..b188baafe0 100644 --- a/tools/family_sweep.py +++ b/tools/family_sweep.py @@ -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):