Files
BFM-decomp/cookbook/C0214.md
T

4.1 KiB

§192 — THE PRE-GATE LADDER WAS MAIN-ONLY, AND NOBODY COULD SEE IT (P31 S54)

Three defects in one call path; the overlay slates that carry most of the wave work were being waved through

The symptom. Running pregate_check.py on an overlay slate printed:

slate 7 -> 7 after resolve_conflicts (0 dropped); checking 0 substituted file(s)
  clean — no textual defect found; the batch is worth a rebuild

Seven byte-perfect drafts, a green light, and zero files examined. Every wave since O has been overlay work; the check that §181 credits with catching 4 of the 5 rejection classes has never once run on any of it.

DEFECT 1 — gate_main.resolve_conflicts and gate_main.substitute both hardcoded corpus.stubs('main'). A non-main entry resolved to no stub, so resolve_conflicts compared it against a '<unknown>' pseudo-file (never clashing with anything) and substitute dropped it on the floor without a word. This is the R36 citizenship class again — a consumer structurally blind to a real binary — and the slate shape already carried the answer (gate_lane records have a binary field). Fixed with a memoized _stubs_for(binary); absent, the key defaults to main, so every historical main slate behaves byte-identically.

DEFECT 2 — sym_of returned a KEYWORD for any pointer-to-function declaration. The regex took the first identifier followed by [ or (, by position, so

extern void (*D_801923D0[])(void *);

reported its symbol as void — and then every such declaration in the TU "conflicted" with every other one under that name: 192 phantom CONFLICTING-EXTERN failures on one overlay TU. Project symbols are now matched by NAME first, C keywords are excluded from the generic branch, and the parenthesised declarator T (*NAME[])(...) is read explicitly. Negative control across the whole tree: 5,526,100 declarations, 189,301 changed verdicts, 0 regressions — every single change is a keyword becoming the real symbol (void → D_800A4F24, s32 → D_801274D0), which also means gate_main's own conflict table has been blind to function-pointer globals in main all along.

DEFECT 3 — void f() and void f(void) were normalized to the same thing. C89 6.5.4.3 makes an unspecified parameter list compatible with any prototype (this project leans on it — §37/§124), but an explicit (void) against f(s32) is a hard error. Collapsing both to () cost 40 more phantom failures against a TU that compiles today. Now: unspecified → a UNSPEC wildcard, explicit (void) → (), and the comparison is gate_main.sig_conflict, not !=. Seven synthetic controls, including both directions of the pair that matters.

The residue is the point. After the three fixes the same overlay slate reports 2 failures, and both are real: a D_80195AF6_t typedef defined twice, and memcpy declared (void*, const void*, u32) in the TU against a draft's (void*, void*, s32). Each of those is one lost clean rebuild — which is the whole reason the ladder exists.

§192b — A CHECKER THAT CHECKED NOTHING MUST NEVER READ AS A PASS. Two guards, both earned above: pregate_check now REFUSES (exit 2) when the slate is non-empty and zero files were substituted, and it prints one [DROP] line per draft resolve_conflicts rejected — with the clashing symbol, the side it clashed with, and both signatures. Those drops were already being computed and thrown away, so a slate that lost every draft to declaration conflicts still ended with "clean — the batch is worth a rebuild". The drop list is not a footnote: it is the §183 playbook's worklist, and it is now the tool's headline output.

The generalizable law. A tool written against one binary is a tool with an untested hypothesis about every other one. The main-vs-overlay split here is exactly the shape of R36's SC07 bug and R32's build_engine_types bug: the code did not fail, it silently narrowed its own domain and then reported success over the part it kept. When a checker's verdict is "clean", ask what it counted — and make the tool answer that question in its own output.