Files
Drew T 827295e241 tools+docs(phase-33.5): task 13.5 — the tools audit + the two dictionaries: tools/tool_census.py (two agreeing enumerations of 327 tool files; docstring/SETUP row/consumers/class derived from the tree; the authored half in config/tool_dictionary.tsv — phase · portability · the NEED each tool answers · what · adapts · verdict — with coverage asserted both ways) → docs/tool-index.md (need-keyed, KEEP-GEN, Reference-index row, wiki + how-to pointers), the kit's tools/MANIFEST.md regenerated (header states live 293 + superseded 28 = 321 rows), and the two verbatim corpora in-tree (Drew, confirmed S91): decomp-architect/corpus/tools/<phase>/ (302 copies + 28 superseded pointers + INDEX) and corpus/cookbook/ (the cookbook, its symptom index, the codegen map, a front page stating what transfers per compiler) — sha1-equal to their sources by tool_census --check in tools-health, regenerated by make kit-corpus; kit_lint exempts the corpus dirs (verbatim evidence) but syntax-checks them; G66 (consult the tool dictionary first) + G67 (translate an inherited idiom through its pass) + two memory seeds (34 at install); SETUP Step 6 installs docs/knowledge-corpus.md and checks the manifest against its own stated total; the ops-setup dictionary rows; the intake's Phase 7 cites G66/G67 and Phase 10 + Part C name the raw-cast → declared-symbol step; templates/layout-contract.md (the five-tool probe, a draft for the split). The review under Drew's criterion: 93 no-consumer tools (one Opus agent's draft, verified: 0 defects, every successor live, 0 live consumers, 0 collisions; four one-off verdicts overturned to STILL-NEEDED) → 34 retired by git mv to tools/sunset/ (28 superseded, 6 one-offs; README review table; SETUP rows moved; Archive-index group). Run 4 (fresh throwaway, the final kit): stopped on my Step-6 check (321 vs the live 293) → both sides derived → resumed → PASS 10/10, manifest 56 == 56, 4 commits, guardrails held (the one foreign path was the timeline regenerated by the detached tools-health). tools-health OK; doc_links --strict rc 0; audit_public OK over 6,842 paths; the purge probe PASSED (Phase 34's gate open). decision-log "P33.5 S91" + accelerators "P33.5 S91" banked; log + checkpoint (NEXT = task 14, xHigh, fresh session)
2026-09-07 22:09:15 -06:00

259 lines
12 KiB
Python

#!/usr/bin/env python3
"""fix_header_decl.py — reconcile shared-header forward-decls to a fn's BYTE-TRUE signature.
THE FRESH-138 DEF-SIDE BLOCKER (Phase-29 SESSION-13, cookbook §63). A shared caller macro in
`src/shared/engine_core.h` forward-declares an engine fn with a SIMPLIFIED, caller-derived signature
(`extern void func_X(s32 a0, void *a1, void *a2);`) that conflicts with the fn's BYTE-TRUE definition
(`int func_X(s32, u16*, u16*)`). When a wave drafts that fn, splicing its true def into an overlay TU
that `#include`s the header throws `conflicting types for func_X` (cc1 exit 33) — so the fn can NEVER
bank, even with a byte-perfect body. `gate_stage`'s arity pre-pass is param-COUNT-only (misses return
type + pointer element type); §54 `reconcile_def_sig` fixes the DRAFT to the header (wrong direction —
the header is the simplified one). This tool fixes the HEADER to the byte-true def.
PROVEN (func_8014CD80, SESSION-13): one byte-neutral header edit -> harvest_verify banks x1
BYTE-IDENTICAL -> `dedup_propagate --addr` fills 138/138 overlays byte-identical -> R22 140/140.
BYTE-NEUTRALITY (why rewriting the decl cannot change any shipped byte):
- return void <-> intN: the true fn always sets $v0; a caller that declared it `void` simply never
read $v0 (unchanged), and no current caller USES the return (it was declared void). Widening the
decl to the true return is a pure type-check change.
- pointer element type void* <-> T*: pointers are register-passed regardless of element type.
REFUSED (would change the ABI, left to a human / permuter): a param-COUNT change, or a param that
flips pointer<->scalar, or a scalar-CLASS change (int<->float — different register file).
The whole-binary byte-gate (harvest_verify / dedup_propagate / R22) is the FINAL arbiter regardless;
this tool only rewrites the decl, and never trusts its own byte-neutrality judgement.
FLEET-SHARED SAFETY (§61): editing engine_core.h touches all 138 overlays. --apply snapshots every
touched file first and prints the exact `git checkout` restore; the undo is ALWAYS a restore, never an
inverse transform, and correctness is validated FLEET-WIDE by R22, never by the per-binary gate that
authorised the bank (the SESSION-12 incident).
USAGE
# dry-run: show the rewrite the tool would make
fix_header_decl.py --fn func_8014CD80 --draft .run/drafts-X/func_8014CD80.c --check
# apply the header rewrite (snapshots first), then bank+propagate yourself:
fix_header_decl.py --fn func_8014D12C --draft .run/drafts-X/func_8014D12C.c --apply
# -> python tools/harvest_verify.py --binary <ov> --drafts <draftdir>
# -> python tools/dedup_propagate.py --addr 0x<addr> --recover
# -> make clean && make extract-all && make check-all (R22, MANDATORY — fleet-shared edit)
"""
import argparse, os, re, sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
HEADERS = ["src/shared/engine_core.h", "src/shared/engine_types.h"]
# canonical project spellings (byte-identical typedefs; used for the rewritten decl's style)
CANON = {
"int": "s32", "unsigned int": "u32", "unsigned": "u32",
"short": "s16", "unsigned short": "u16",
"char": "s8", "unsigned char": "u8", "signed char": "s8",
"long": "s32", "unsigned long": "u32",
}
INT_CLASS = {"s8", "u8", "s16", "u16", "s32", "u32", "int", "unsigned", "short", "long", "char",
"unsigned int", "unsigned short", "unsigned char", "signed char", "unsigned long"}
def canon_base(base):
base = " ".join(base.split())
return CANON.get(base, base)
def parse_param(p):
"""'unsigned short *param_2' -> (base='unsigned short', is_ptr=True, name='param_2')."""
p = " ".join(p.replace("*", " * ").split())
if p in ("", "void"):
return None
is_ptr = "*" in p
toks = [t for t in p.split() if t != "*"]
TYPEWORDS = {"unsigned", "signed", "short", "long", "int", "char", "void", "float", "double",
"u8", "u16", "u32", "s8", "s16", "s32", "f32", "const", "struct", "void"}
name = ""
if toks and toks[-1] not in TYPEWORDS:
name = toks[-1]
toks = toks[:-1]
return " ".join(toks), is_ptr, name
def parse_sig(sig_body):
"""sig_body = the text between ( and ). Returns list of (base, is_ptr, name)."""
sig_body = sig_body.strip()
if sig_body in ("", "void"):
return []
return [parse_param(p) for p in sig_body.split(",")]
def byte_true_sig(draft_path, fn):
"""Parse the fn's DEFINITION (has a { body) from the draft. Returns (ret, [params])."""
text = open(draft_path).read()
m = re.search(rf"(?:^|\n)[ \t]*([A-Za-z_][\w \t\*]*?)\b{re.escape(fn)}[ \t]*\(([^;{{]*?)\)[ \t\n]*\{{",
text)
if not m:
return None
return m.group(1).strip(), parse_sig(m.group(2))
DECL_RE_T = r"(extern\s+)([A-Za-z_][\w \t\*]*?)\b{fn}[ \t]*\(([^;]*?)\)([ \t]*;)"
def find_decls(fn):
"""Return [(hdr_path, lineno, full_line, ret, [params])] for every forward-decl of fn."""
out = []
rx = re.compile(DECL_RE_T.format(fn=re.escape(fn)))
for h in HEADERS:
path = os.path.join(REPO, h)
if not os.path.exists(path):
continue
for i, line in enumerate(open(path), 1):
m = rx.search(line)
if m:
out.append((h, i, line, m.group(2).strip(), parse_sig(m.group(3)), m))
return out
def compatible(true_ret, true_params, decl_ret, decl_params):
"""Byte-neutral iff same param count, ptr-ness matches per param, and scalar params stay in the
integer class (no int<->float flip). Return (ok, reason)."""
if len(true_params) != len(decl_params):
return False, f"param COUNT differs (true {len(true_params)} vs decl {len(decl_params)}) — ABI change, REFUSED"
for i, (tp, dp) in enumerate(zip(true_params, decl_params)):
if tp is None or dp is None:
return False, f"param {i} unpar4seable"
tb, tptr, _ = tp
db, dptr, _ = dp
if tptr != dptr:
return False, f"param {i} flips pointer<->scalar ({db}{'*' if dptr else ''} -> {tb}{'*' if tptr else ''}) — ABI change, REFUSED"
if not tptr: # scalar: both must be integer-class
if canon_base(tb) not in INT_CLASS or canon_base(db) not in INT_CLASS:
if canon_base(tb) != canon_base(db):
return False, f"param {i} scalar class change ({db} -> {tb}) — REFUSED"
return True, "ok"
def canon_param_str(p):
if p is None:
return "void"
base, is_ptr, _ = p
return canon_base(base) + ("*" if is_ptr else "")
def canon_sig(ret, params):
"""Typedef-normalized (ret, param-string) — the C-type identity, so int==s32, unsigned short*==u16*.
Two decls with the same canon_sig do NOT `conflicting types`; different ones DO."""
r = "void" if ret.strip() == "void" else canon_base(ret)
ps = ",".join(canon_param_str(p) for p in params) if params else "void"
return r, ps
def build_decl(m, true_ret, true_params, sym):
"""Return just the rewritten `extern <ret> <sym>(...);` span (the caller splices it back into the
line, preserving leading whitespace + any trailing macro `\\` continuation)."""
extern = m.group(1)
decl_params = parse_sig(m.group(3))
ret = canon_base(true_ret) if true_ret.strip() != "void" else "void"
parts = []
for i, tp in enumerate(true_params):
tb, tptr, _ = tp
name = decl_params[i][2] if i < len(decl_params) and decl_params[i] and decl_params[i][2] else ""
base = canon_base(tb) if tb else "void"
parts.append((f"{base} *{name}" if tptr else f"{base} {name}").rstrip())
inner = ", ".join(parts) if parts else "void"
return f"{extern}{ret} {sym}({inner});"
def parse_draft_fn_externs(draft_path):
"""Return {fn_name: (ret, [params])} for every `extern <ret> func_X(<params>);` in the draft."""
text = open(draft_path).read()
out = {}
for m in re.finditer(r"extern\s+([A-Za-z_][\w \t\*]*?)\b(func_[0-9A-Fa-f]+)\s*\(([^;]*?)\)\s*;", text):
out[m.group(2)] = (m.group(1).strip(), parse_sig(m.group(3)))
return out
def plan_symbol(sym, true_ret, true_params):
"""Plan header-decl rewrites so every shared-header decl of `sym` matches (true_ret, true_params).
Returns (changes, saw_decl) where changes = [(hdr, old_line, new_line)]. Prints per-decl status."""
changes = []
decls = find_decls(sym)
for h, ln, line, dret, dparams, m in decls:
already = (canon_sig(true_ret, true_params) == canon_sig(dret, dparams))
ok, why = compatible(true_ret, true_params, dret, dparams)
new_line = line[:m.start()] + build_decl(m, true_ret, true_params, sym) + line[m.end():]
status = "ALREADY-OK" if already else ("SAFE" if ok else "REFUSE")
print(f" {sym} {h}:{ln} [{status}]")
print(f" - {line.rstrip()}")
if already:
continue
if not ok:
print(f" ! {why}")
continue
print(f" + {new_line.rstrip()}")
changes.append((h, line, new_line))
return changes, bool(decls)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--fn", required=True, help="func_XXXXXXXX")
ap.add_argument("--draft", required=True, help="draft .c holding the byte-true DEFINITION")
ap.add_argument("--check", action="store_true", help="dry-run: show the rewrite, touch nothing")
ap.add_argument("--apply", action="store_true", help="snapshot + rewrite the header decls")
ap.add_argument("--reconcile-externs", action="store_true",
help="v2: ALSO reconcile every CALLEE the draft declares differently than the shared "
"header (byte-perfect body blocked by a callee-decl conflict, not its own def). "
"Sources the byte-true sig from the draft's `extern` line. The byte-gate + R22 "
"arbitrate — an over-reach (e.g. a scalar-width return change that adds a "
"sign-extension in some caller) fails to bank, never banks wrong.")
args = ap.parse_args()
global FN_ADDR
FN_ADDR = args.fn.replace("func_", "")
os.chdir(REPO)
true = byte_true_sig(args.draft, args.fn)
if not true:
print(f"[fix_header_decl] FATAL: no byte-true DEFINITION of {args.fn} in {args.draft}", file=sys.stderr)
return 2
true_ret, true_params = true
pstr = ", ".join(f"{b}{'*' if p else ''}" for (b, p, _) in true_params) or "void"
print(f"byte-true def: {true_ret} {args.fn}({pstr})")
changes, saw = plan_symbol(args.fn, true_ret, true_params) # v1: the fn's own decl
if args.reconcile_externs: # v2: each callee the draft declares
for callee, (cret, cparams) in parse_draft_fn_externs(args.draft).items():
if callee == args.fn:
continue
ch, s = plan_symbol(callee, cret, cparams)
changes += ch
saw = saw or s
if not saw:
print(f"[fix_header_decl] no shared-header forward-decl to reconcile "
f"(blocker is elsewhere: data/struct/DIFF).")
return 1
if not changes:
print("no change needed (decls already byte-true, or all REFUSED).")
return 0
if args.check:
print(f"[--check] {len(changes)} decl(s) WOULD be rewritten. Re-run with --apply.")
return 0
if not args.apply:
print("pass --apply to write (or --check to dry-run).", file=sys.stderr)
return 3
# §61: snapshot every touched header before writing; undo is a RESTORE (git checkout), never inverse.
touched = sorted({h for h, _, _ in changes})
for h in touched:
path = os.path.join(REPO, h)
text = open(path).read()
for _, old, new in [c for c in changes if c[0] == h]:
assert text.count(old) >= 1, f"decl line vanished in {h}"
text = text.replace(old, new, 1)
open(path, "w").write(text)
print(f"[--apply] rewrote {len(changes)} decl(s) in {len(touched)} header(s).")
print(f" UNDO (if the bank fails / R22 is not 140/140): git checkout -- {' '.join(touched)}")
print(f" NEXT: harvest_verify --binary <ov> --drafts <dir> ; dedup_propagate --addr 0x{FN_ADDR.lower()} --recover ; R22 (make clean && extract-all && check-all)")
return 0
if __name__ == "__main__":
sys.exit(main())