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:
Drew T
2026-09-10 10:53:20 -06:00
parent 3464a25cd0
commit 161d36cf36
3 changed files with 1436416 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+34
View File
@@ -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
+166
View File
@@ -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())