mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
phase-36: argcheck (the missing-argument census), a10's verdict downgraded on Drew's challenge, and R14's documented premise corrected on bytes
- tools/argcheck.py: every call site whose in-scope declaration is narrower than the callee's real definition. 15,626 definitions read; 110,478 narrower declarations over 1,912 callees (86,701 K&R-empty, 23,777 positively narrow); 471 of them sit in a body that still holds a NEEDED $4-$7 pin — 323 bodies, the targeted draw, written to .run/P36/engine/argcheck_draw.tsv. Checked against the six agent cracks it was built from: it finds a7, a12 and a25, and it CANNOT find a13, whose function is declared (void) in 131 of 138 sites AND defined (void) — a declaration comparison is blind to a definition that is uniformly wrong. The census is a lower bound and the residual stays the oracle; the docstring says so. Corrected before use: the scope column says which FIX is available, not whether the body can be banked — the cast route is body-only at either scope (a7 closed a file-scope case that way). - Drew challenged a10's 'needed by construction' verdict: nobody wrote the pin, so a plain-C spelling must exist. He is right and the verdict is downgraded to its real scope — no spelling reaches the bytes in that TU's CURRENT declaration environment. That environment is demonstrably wrong there: the file declares extern s32 func_801789AC(s32 arg0) while the body calls it through a cast that drops the argument, the same class six agents cracked today. Restoring it was tested at once and scores 3, not 0, so it is not the whole answer — but a verdict taken inside a distorted environment is not a property of the function. func_80178970 moves to the structs/types phase list rather than being marked permanent, with its reading attached. - R14's documented premise corrected on bytes (agent a15): MIPS defines only PROMOTE_PROTOTYPES, not PROMOTE_MODE (config/mips/mips.h:1153), so a narrowed parameter stays HImode and the extension still happens at the use. R14's banked closes stand on their bytes, not on that rationale.
This commit is contained in:
+1436216
File diff suppressed because it is too large
Load Diff
@@ -1156,6 +1156,40 @@ accumulate here as the phase produces them.**
|
||||
Its answer to the `$0` question is a correction worth keeping: `zr` is `register int __asm__("$0")` with NO initializer,
|
||||
an opaque operand and NOT R16's constant holder — **R16 must refuse an uninitialised `register … __asm__` declaration.**
|
||||
|
||||
- **S102 — DREW'S CHALLENGE TO a10's "NEEDED BY CONSTRUCTION", and the verdict DOWNGRADED (R65/R69).** He asked the right
|
||||
question: *"does this mean the dev in 1998 wrote in that register pin in his C code? because if not, then it should be
|
||||
possible to remove it."* **He is right, and the verdict was too strong.** Nobody wrote `register int r __asm__("$2")` —
|
||||
every pin in this tree is OUR scaffold, inserted by this project to force a byte match, so a plain-C spelling exists for
|
||||
every one of them by construction. What agent a10 actually proved is the SCOPED claim: within `func_80178970`'s CURRENT
|
||||
declaration environment, no spelling it tried reaches the bytes, because combine deletes the call-result copy
|
||||
(`combine.c:914-917`, `use_crosses_set_p` at `:10127-10130`; the `SMALL_REGISTER_CLASSES` arm at `:944-957` is not
|
||||
defined for MIPS) and only a call or a return writes `$v0` in plain C. **The environment is demonstrably wrong in that
|
||||
very function:** the TU declares `extern s32 func_801789AC(s32 arg0);` at line 3884 and the body calls it as
|
||||
`((s32 (*)(void))func_801789AC)()` — the SAME dropped-argument class six other agents cracked today. Restoring the
|
||||
argument was tested on the bytes at once: `func_801789AC(0)` scores **3**, both as a statement and folded into the `if`,
|
||||
so it is not the whole answer here — but it proves the body is being read in a distorted environment, and a verdict
|
||||
taken inside a distorted environment is not a property of the function. **Drew's own next hypothesis is the right one to
|
||||
record:** structs, once built out and propagated, change the RTL a body generates — this phase already has a byte-proof
|
||||
of exactly that lever, agent a18's `func_80141874`, where declaring a global `extern u16 X[];` instead of a scalar set
|
||||
`MEM_IN_STRUCT_P` on its store, restored a `true_dependence` edge the exception clause at `sched.c:837-839` had
|
||||
discarded, and closed the body. **So `func_80178970` is NOT permanent: it moves to the structs/types phase's list**
|
||||
alongside `func_80136824`, `func_80168828` and `func_80157D20`, with its reading attached.
|
||||
- **S102 — T7 agent a15: `func_80166F58` NOT closed (score 4 of a start of 4; ~350 hand-tested candidates on top of the
|
||||
search's 2,625) — and it CORRECTS one of my own documented premises.** The SETUP row for R14 says a `short` parameter
|
||||
"arrives in its SImode register and gcc 2.7.2 sign-extends it IN PLACE". **That is wrong for this port:** MIPS defines
|
||||
only `PROMOTE_PROTOTYPES`, not `PROMOTE_MODE` (`config/mips/mips.h:1153`), so a narrowed parameter stays a HImode pseudo
|
||||
and the extension still happens at the USE — the agent compiled it both ways and got the same 67-instruction merged
|
||||
form, and the K&R form too. R14 was doubly dead on that body (the TU also declares the function twice at file scope
|
||||
after its definition, so narrowing is a hard `conflicting types` error). Its real reading: the missing pair is a reg-reg
|
||||
copy that combine deletes because the extension's destination carries `REG_DEAD` at the copy, and the obvious defence —
|
||||
a second use — is undone by `cse.c:canon_reg`, which rewrites the later use onto the COPY's destination; only a second
|
||||
use placed BEFORE the copy survives, and it then rotates the callee-saved seats. Its sweep measures the trap exactly:
|
||||
second use after both copies 67 ins, between them 68, before both 69. It also disassembled all 4,284 objects under
|
||||
`build/src` looking for the target's shape and found it in exactly ONE function in the whole game — itself.
|
||||
**And a scoring finding that explains several stalls: the score is not monotone in structure.** Its 69-instruction
|
||||
candidate (the CORRECT shape) scores 21 while the 2-instruction-short one scores 4, so a hill-climb cannot reach the
|
||||
answer from the start; a secondary sort on |count difference| would have let the mechanical search find it.
|
||||
|
||||
## 🛑 SESSION CHECKPOINT — S101 (2026-09-09) / LIVE, refreshed S102 (2026-09-10): T0–T6 ☑, **T7 RUNNING — agent a1 BANKED + HARVESTED: `func_80156044` (130 bodies) and its move toolified as generator **R15, the sink**, which then closed 6 more exemplars (267 bodies) in 4 compiles each with no tokens; agent a2 banked 125 more; 30,358 → 29,572 sites, R22 218/218**; **lane B DELIVERED + 4 claims verified on bytes; lane A = rung G, `tools/delever_search.py`, BUILT, CONTROLLED, MEASURED over six runs (g1–g6b: 33,427 → 30,358 sites, 12,048 → 9,747 bodies, every bank R22 218/218, no drafting tokens); the head is where the number is (57 classes ≥100 copies = 7,318 of 9,796 residue bodies) and the wide search is spent on it; T7 APPROVED by Drew as ONE AGENT AT A TIME — the packs, the brief and the agent's scorer are built; NEXT = §2: start the serial agent loop IN THIS FRESH SESSION** | the number at this commit: **27,984 sites** in 8,249 bodies · marked 27,984 · UNMARKED 0 · orphans 0 — `lever_census --check` OK · `lever_progress --check` OK (20 milestones). **S102's loop state: the burst of 20 is landing; a4/a7/a8/a12/a18/a19 banked (756 bodies); the per-file scratch-object collision is FIXED (per-function tag); the biggest class found is a truncated `(void)` DECLARATION, three cases of which need the types phase.**
|
||||
|
||||
### 0. How to use this block
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""argcheck — every call site whose in-scope declaration is NARROWER than the callee's real definition.
|
||||
|
||||
WHY THIS EXISTS (P36 S102, the burst of 20). Six T7 agents, working independently on different functions and never
|
||||
seeing each other's results, each reached score 0 by restoring an argument the source had dropped: a7 (a cast at the
|
||||
call), a8 (a widened function-pointer-array prototype), a11 (a dispatch-table call given its object pointer), a12 (a
|
||||
call given the value it loads), a13 (a definition given its two parameters), a25 (a call re-issued at the callee's real
|
||||
four-argument arity). In every one the register pin was hired to fake the instruction the missing argument would have
|
||||
emitted, and the mechanisms were different each time — combine's `added_sets_2` gate (`combine.c:1458`), a copy
|
||||
preference applied ahead of first-fit (`global.c:1535/1589` → `:997-1030`) whose argument copy degenerates into a
|
||||
self-move deleted at `toplev.c:3142`, reorg's liveness half (`reorg.c:3374`, the use added to
|
||||
`CALL_INSN_FUNCTION_USAGE` at `:428`) — but the CLASS is one, and no candidate generator can reach it: every family in
|
||||
`delever.recipe_candidates` rewrites statements and declarations, none edits a call's ARGUMENT LIST.
|
||||
|
||||
So this is a static filter, not a search. It answers, for the whole tree at once, the question each of those agents had
|
||||
to answer by hand with one grep: does this call site's declaration agree with the callee's real definition?
|
||||
|
||||
WHAT IT REPORTS, and what the scope column does and does not mean. The scope says which FIX is available, not whether
|
||||
the body can be banked:
|
||||
* ANY scope — the CAST route is bankable body-only, because the cast lives inside the definition: agent a7 closed
|
||||
`func_8013BC7C` with `((void (*)(void *))f)(p)` against a FILE-scope `(void)` declaration, and a25 re-issued a call
|
||||
at the callee's real four-argument arity the same way.
|
||||
* BLOCK scope — the declaration itself can also be widened in the same edit as the body (a8's
|
||||
`extern void (*D_80192FD0[])(u8 *);`), which is the more readable spelling.
|
||||
* FILE scope — widening the DECLARATION belongs to the types phase, because gcc 2.7.2 rejects a conflicting
|
||||
block-scope redeclaration outright; three such are already recorded by name (`func_80136824`, `func_80168828`,
|
||||
`func_80157D20`).
|
||||
|
||||
KNOWN BLIND SPOT, measured against the six agent cracks this was built from. It finds a7, a12 and a25 and it CANNOT find
|
||||
a13: `func_80157D20` is declared `(void)` in 131 of its 138 defining sites AND defined `(void)`, so no declaration
|
||||
disagrees with any definition — the missing parameters exist only in the target's bytes, where the prologue copies two
|
||||
argument registers. A declaration comparison is blind to a definition that is uniformly wrong; only the residual sees
|
||||
those. So this census is a lower bound on the class, and the residual remains the oracle.
|
||||
|
||||
The real arity is read from the DEFINITION, wherever it lives: a `.c` under `src/`, or a shared body header under
|
||||
`src/shared/` (P35's one-source-per-function shape). A definition is the ground truth; a declaration is a claim.
|
||||
"""
|
||||
import argparse, collections, json, pathlib, re, sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||
import lever_census as lc
|
||||
|
||||
REPO = pathlib.Path(__file__).resolve().parent.parent
|
||||
NAME = r"[A-Za-z_]\w*"
|
||||
DEF = re.compile(r"^[A-Za-z_][\w \t*]*?\b(" + NAME + r")\s*\(([^;{]*)\)\s*\{", re.M)
|
||||
DECL = re.compile(r"\bextern\s+[A-Za-z_][\w \t*]*?\b(" + NAME + r")\s*\(([^;)]*)\)\s*;")
|
||||
|
||||
|
||||
def arity(params):
|
||||
"""The number of declared parameters. `void` and an empty list are both 0, but they are NOT the same claim: an empty
|
||||
list is K&R "unspecified", which gcc accepts against any call, while `(void)` is a positive assertion of none."""
|
||||
p = params.strip()
|
||||
if p == "" or p == "void":
|
||||
return 0
|
||||
depth, n = 0, 1
|
||||
for ch in p:
|
||||
if ch in "([":
|
||||
depth += 1
|
||||
elif ch in ")]":
|
||||
depth -= 1
|
||||
elif ch == "," and depth == 0:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def definitions():
|
||||
"""{name: (arity, params, where)} from every DEFINITION in the tree."""
|
||||
out = {}
|
||||
for f in sorted(list((REPO / "src").glob("**/*.c")) + list((REPO / "src" / "shared").glob("**/*.h"))):
|
||||
rel = str(f.relative_to(REPO))
|
||||
try:
|
||||
txt = f.read_text(errors="surrogateescape")
|
||||
except OSError:
|
||||
continue
|
||||
masked = lc.sc.mask_text(txt)
|
||||
for m in DEF.finditer(masked):
|
||||
name, params = m.group(1), m.group(2)
|
||||
if name in ("if", "for", "while", "switch", "return", "sizeof"):
|
||||
continue
|
||||
out.setdefault(name, (arity(params), params.strip(), rel))
|
||||
return out
|
||||
|
||||
|
||||
def scan(defs, only_needed_argpins=True):
|
||||
"""[row] — one row per narrower-than-real declaration, with the scope that decides whether it is bankable."""
|
||||
rows = []
|
||||
for f in sorted(list((REPO / "src").glob("**/*.c")) + list((REPO / "src" / "shared").glob("**/*.h"))):
|
||||
rel = str(f.relative_to(REPO))
|
||||
try:
|
||||
txt = f.read_text(errors="surrogateescape")
|
||||
except OSError:
|
||||
continue
|
||||
if "extern" not in txt:
|
||||
continue
|
||||
masked = lc.sc.mask_text(txt)
|
||||
lines = masked.split("\n")
|
||||
# the bodies of this TU, and which of them hold a NEEDED pin on an argument register
|
||||
argpin_fns = set()
|
||||
if "!FAKE" in txt:
|
||||
for s in lc.walk_file(txt, rel, rel.endswith(".h"))["sites"]:
|
||||
if s["cls"] in "AB" and s["kind"] == "pin" and s.get("detail") in ("$4", "$5", "$6", "$7"):
|
||||
argpin_fns.add(s.get("fn"))
|
||||
spans = [(m.group(1), masked[:m.start()].count("\n") + 1,
|
||||
masked[:m.end()].count("\n") + 1) for m in DEF.finditer(masked)]
|
||||
def fn_at(ln):
|
||||
for name, a, _ in spans:
|
||||
if a <= ln:
|
||||
last = name
|
||||
else:
|
||||
break
|
||||
return last if spans and spans[0][1] <= ln else None
|
||||
depth = 0
|
||||
for i, l in enumerate(lines):
|
||||
for m in DECL.finditer(l):
|
||||
callee, params = m.group(1), m.group(2)
|
||||
real = defs.get(callee)
|
||||
if real is None:
|
||||
continue
|
||||
dec = arity(params)
|
||||
if dec >= real[0]:
|
||||
continue
|
||||
if params.strip() == "" and real[0] > 0:
|
||||
kind = "K&R-empty" # accepts any call; not itself an error, but m2c then calls it with none
|
||||
else:
|
||||
kind = "narrow"
|
||||
holder = fn_at(i + 1)
|
||||
rows.append(dict(tu=rel, line=i + 1, callee=callee, declared=dec, real=real[0],
|
||||
real_params=real[1], defined_in=real[2], scope="block" if depth > 0 else "file",
|
||||
kind=kind, in_fn=holder,
|
||||
argpin=bool(holder and holder in argpin_fns)))
|
||||
depth += l.count("{") - l.count("}")
|
||||
depth = max(depth, 0)
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--json", help="write the rows here")
|
||||
ap.add_argument("--argpin-only", action="store_true", help="only bodies holding a NEEDED $4-$7 pin")
|
||||
a = ap.parse_args()
|
||||
defs = definitions()
|
||||
rows = scan(defs)
|
||||
if a.argpin_only:
|
||||
rows = [r for r in rows if r["argpin"]]
|
||||
by_callee = collections.Counter(r["callee"] for r in rows)
|
||||
by_scope = collections.Counter(r["scope"] for r in rows)
|
||||
by_kind = collections.Counter(r["kind"] for r in rows)
|
||||
pinned = [r for r in rows if r["argpin"]]
|
||||
print(f"argcheck: {len(defs)} definitions read; {len(rows)} declaration(s) narrower than the definition, "
|
||||
f"over {len(by_callee)} callee(s)")
|
||||
print(f" by scope: {dict(by_scope)} (the CAST route is body-only at either scope; only widening the "
|
||||
f"DECLARATION needs block scope)")
|
||||
print(f" by kind: {dict(by_kind)}")
|
||||
print(f" in a body that still holds a NEEDED $4-$7 pin: {len(pinned)} row(s) in "
|
||||
f"{len({(r['tu'], r['in_fn']) for r in pinned})} body/bodies — the targeted draw")
|
||||
for c, n in by_callee.most_common(12):
|
||||
ex = next(r for r in rows if r["callee"] == c)
|
||||
print(f" {c:16s} {n:5d} site(s) declared {ex['declared']} vs real {ex['real']} ({ex['defined_in']})")
|
||||
if a.json:
|
||||
pathlib.Path(a.json).write_text(json.dumps(rows, indent=1))
|
||||
print(f" rows -> {a.json}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user