Files
BFM-decomp/tools/readability_progress.py
T
Drew T 69d83f7b61 phase-36: 16,759 lying call declarations repaired across 3,439 units, byte-identical (R22 218/218) + the Gen3 readability series
Drew: we do want C correctness on all funcs, and log it for the story and the chart.

- decl_repair --apply rewrote 3,439 units and repaired 16,759 declarations. check-all: 218 passed, 0 failed of 218.
  lever_census --check: 26,456 pin/asm sites, 0 UNMARKED — unchanged, as expected: this pass fixed TRUTH, not levers.
- only the free set was touched: a declaration is repaired when every call to that function in the unit already passes
  the arguments, so the code was right and only the promise was wrong. Calls that pass too few remain R19's population,
  where the argument must be chosen and the bytes decide.
- tools/readability_progress.py: the Gen3 series beside docs/levers.md, because levers are only one way the source is
  untrue. It counts lying call declarations (split by the K&R-empty and (void) forms, and how many sit in a body still
  holding an argument-register pin) and raw cast dereferences against struct member reads — the struct debt. Each row
  carries its date and commit so the chart is generated, never typed (R75). docs/readability.md renders it.
  First row after the repair: 94,001 lying declarations over 1,564 callees (86,701 (), 7,300 (void)), 461 in 314 pinned
  bodies; 414,148 raw cast dereferences against 173,286 struct member reads.
- dictionary rows for decl_repair and readability_progress; kit corpus regenerated; tool_census --check OK.
2026-09-10 12:11:22 -06:00

116 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""readability_progress — the Gen3 series: how honest and how readable the matched C is, over time.
WHY A SECOND SERIES. `docs/levers.md` (`tools/lever_progress.py`) counts LEVERS — the register pins and `asm`
statements this project inserted to force a byte match — and it is the chart of Phase 36's own goal. But Gen3 is about
the source being TRUE and readable, and the levers are only one of the ways it is not. Two more are measurable today and
neither is a lever:
* LYING CALL DECLARATIONS. A file declares `extern void f(void);` while the function really takes an argument. It
compiles, it matches, and it is false — and it was the largest single blocker in Phase 36's residue: six agents
independently reached byte-identical output by restoring an argument the decompiler had dropped, and the register
pin in each case existed only to fake the instruction the missing argument would have emitted.
* RAW CAST DEREFERENCES. `*(u16 *)(p + 0x12)` where the original wrote a struct field. A struct is not recoverable
from the binary — types are erased and a retail build carries no metadata — so this number falls only as the struct
layers are inferred from base+offset+width evidence and applied.
Each row is dated and carries the commit it was taken at, so the chart is reproducible from the tree rather than typed
(R75: a published number is generated). Append a row with `--snapshot "<label>"`; `--check` asserts the last row is
this tree's.
"""
import argparse, datetime, json, pathlib, re, subprocess, sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
import argcheck
REPO = pathlib.Path(__file__).resolve().parent.parent
TSV = REPO / "docs" / "readability-progress.tsv"
DOC = REPO / "docs" / "readability.md"
CAST = re.compile(r"\*\s*\(\s*(?:struct\s+|union\s+)?[A-Za-z_]\w*\s*\*+\s*\)\s*\(")
MEMBER = re.compile(r"->[A-Za-z_]\w*|\.[A-Za-z_]\w*\s*=")
def counts():
defs = argcheck.definitions()
rows = argcheck.scan(defs)
casts = members = 0
for f in (REPO / "src").glob("**/*.c"):
t = f.read_text(errors="surrogateescape")
casts += len(CAST.findall(t))
members += len(MEMBER.findall(t))
for f in (REPO / "src" / "shared").glob("**/*.h"):
t = f.read_text(errors="surrogateescape")
casts += len(CAST.findall(t))
members += len(MEMBER.findall(t))
pinned = [r for r in rows if r["argpin"]]
return dict(definitions=len(defs), narrow_decls=len(rows),
callees=len({r["callee"] for r in rows}),
kr_empty=sum(1 for r in rows if r["kind"] == "K&R-empty"),
narrow=sum(1 for r in rows if r["kind"] == "narrow"),
pinned_rows=len(pinned), pinned_bodies=len({(r["tu"], r["in_fn"]) for r in pinned}),
raw_casts=casts, struct_members=members)
HEAD = ("date\tlabel\tcommit\tdefinitions\tnarrow_decls\tcallees\tkr_empty\tnarrow\tpinned_rows\tpinned_bodies"
"\traw_casts\tstruct_members\n")
def render():
rows = [l.rstrip("\n").split("\t") for l in TSV.read_text().splitlines()[1:] if l.strip()]
out = ["# Readability of the matched C — the Gen3 series",
"",
"> **Generated by `tools/readability_progress.py --snapshot`; never typed (R75).** Companion to",
"> `docs/levers.md`, which counts the compiler-forcing levers. This one counts the two things that are wrong",
"> with the source even where no lever remains: call declarations that are FALSE, and memory read through raw",
"> pointer casts where the original had a struct.",
"",
"| date | label | commit | lying declarations | of which `()` | of which `(void)` | in a pinned body | raw cast derefs | struct member reads |",
"|---|---|---|---:|---:|---:|---:|---:|---:|"]
for r in rows:
out.append(f"| {r[0]} | {r[1]} | `{r[2][:9]}` | {r[4]} | {r[6]} | {r[7]} | {r[8]} | {r[10]} | {r[11]} |")
out += ["",
"**How to read it.** A *lying declaration* is a call site whose in-scope declaration names fewer parameters",
"than the callee's own definition. It is not a style problem: on this processor an argument travels in a",
"register, so a dropped argument removes an instruction, and Phase 36 found register pins inserted to fake",
"exactly that instruction. *Raw cast derefs* is the struct debt — a struct cannot be recovered from the",
"binary, only inferred from consistent base+offset+width evidence, so this figure falls as that work lands.",
""]
DOC.write_text("\n".join(out) + "\n")
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--snapshot", metavar="LABEL")
ap.add_argument("--check", action="store_true")
a = ap.parse_args()
head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=REPO, capture_output=True, text=True).stdout.strip()
if a.check:
if not TSV.exists():
print("readability_progress --check: no series yet")
return 1
last = TSV.read_text().splitlines()[-1].split("\t")
ok = last[2] == head
print(f"readability_progress --check: {'OK' if ok else 'STALE'} — last row {last[1]!r} at {last[2][:9]}, "
f"HEAD {head[:9]}")
return 0 if ok else 1
if not a.snapshot:
print(__doc__)
return 2
c = counts()
if not TSV.exists():
TSV.write_text(HEAD)
with TSV.open("a") as fh:
fh.write("\t".join([datetime.date.today().isoformat(), a.snapshot, head, str(c["definitions"]),
str(c["narrow_decls"]), str(c["callees"]), str(c["kr_empty"]), str(c["narrow"]),
str(c["pinned_rows"]), str(c["pinned_bodies"]), str(c["raw_casts"]),
str(c["struct_members"])]) + "\n")
render()
print(f"readability_progress: {a.snapshot} — {c['narrow_decls']} lying declaration(s) over {c['callees']} callee(s) "
f"({c['kr_empty']} `()`, {c['narrow']} `(void)`), {c['pinned_rows']} in {c['pinned_bodies']} pinned bodies; "
f"{c['raw_casts']} raw cast deref(s), {c['struct_members']} struct member read(s), at {head[:9]}")
return 0
if __name__ == "__main__":
sys.exit(main())