Files
BFM-decomp/tools/alloc_table.py
T
Drew T e76cb1c89e phase-36: fix the instrument four agents asked for — alloc_table now covers every pseudo and asserts its own coverage
tools/alloc_table.py had only ever printed pseudos that landed in $s0-$s7/$fp, and it depended on 'Register N in M.'
lines the dumps often do not emit — so two agents in the burst were handed an EMPTY or one-row table and read it as an
answer. That is the silently-narrowed-scope defect class: a true number about a scope far narrower than the reader
believes.

- it now prints every pseudo with refs, live length, block, conflicts, copy preferences and allocno_compare's priority,
  takes the hard register from either dump, and ASSERTS ITS COVERAGE against the .greg order line (R32) — a named gap and
  a non-zero exit instead of a confident subset. A missing dump section is refused loudly rather than printed as an
  empty table.
- documented honestly: .greg carries the INPUTS to global allocation (order, conflicts, preferences), not the final
  assignment, so a global allocno's hard register prints as '-'. That is the dump's shape, not a gap.
- verified on a real dump from an agent's pack: 4 pseudos, the order line, preferences and conflicts all parsed,
  coverage OK. The old tool would have printed nothing for it — none of the four is callee-saved.
- tools/cc1_dumps_tu.sh: add -I<the TU's own directory> (an agent found it silently preprocessing to 44 lines and
  exiting 0 on any TU with relative includes), add -dR for the post-reload schedule, and REFUSE a preprocess under 200
  lines instead of producing empty dumps.
- the agent brief now says to read the allocation table first for any register residual, that the .greg assignment is
  absent by design, and that the residual text cannot distinguish a missing instruction from wrong registers — one agent
  chased a register lever for hours when the defect was a cse store-to-load forward that had deleted a load.
2026-09-10 11:05:21 -06:00

110 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""alloc_table.py <tag> [fn] [dump_root]: the ALLOCATION TABLE of one function, read from gcc 2.7.2's own dumps.
For every pseudo: references, live length, block, the hard register it got, and `global.c allocno_compare`'s priority
`floor(log2 refs) * refs / live * 1e4` — plus, when the `.greg` dump prints them, the allocation ORDER line
(`;; N regs to allocate: …`) and each allocno's copy PREFERENCES (`;; N preferences: …`).
Written at P32 T4c to read the callee-saved bank order. The `.greg` dump carries the INPUTS to global allocation — the order, each allocno's conflicts and its copy
preferences — and not the final assignment, so a global allocno's hard register prints as `-` here; that is the dump's
shape, not a gap, and the order line plus the preferences are what decide it. **Widened at P36 S102, after four T7
agents asked for exactly this table and two reported the tool printing an EMPTY or one-row result** (`func_80143D28`, `func_8016C49C`): it had
only ever printed pseudos that landed in `$s0..$s7`/`$fp`, and it depended on `Register N in M.` lines that the dumps
often do not emit at all — the silently-narrowed-scope defect class, a true number about a scope far narrower than the
reader believes. It now prints every pseudo, takes the hard register from either dump or from the order line, and
ASSERTS ITS OWN COVERAGE (R32): if the `.greg` order line names allocnos this table does not hold, it says so and exits
non-zero rather than printing a confident subset.
Why the table settles arguments the residual cannot: agent a4 flipped a 34-instruction residual to zero by moving one
allocno's priority 6524 → 6808 past another's 6666, and agent a16 closed a body by shortening a live length from 44 to
43. Both are arithmetic on these columns, not a search.
"""
import math, pathlib, re, sys
NAMES = {2: "v0", 3: "v1", 4: "a0", 5: "a1", 6: "a2", 7: "a3", 8: "t0", 9: "t1", 10: "t2", 11: "t3", 12: "t4",
13: "t5", 14: "t6", 15: "t7", 16: "s0", 17: "s1", 18: "s2", 19: "s3", 20: "s4", 21: "s5", 22: "s6",
23: "s7", 24: "t8", 25: "t9", 28: "gp", 29: "sp", 30: "fp", 31: "ra"}
def section(path, fn):
"""The dump text for one function, or "" when the dump has no such section (a missing dump is not an empty table)."""
p = pathlib.Path(path)
if not p.exists():
return ""
s = p.read_text(errors="surrogateescape")
i = s.find(f";; Function {fn}\n")
if i < 0:
i = s.find(f"Function {fn}\n")
if i < 0:
return ""
j = s.find(";; Function ", i + 20)
return s[i:j if j > 0 else None]
def main():
if len(sys.argv) < 2:
print(__doc__)
return 2
tag = sys.argv[1]
fn = sys.argv[2] if len(sys.argv) > 2 else None
root = sys.argv[3] if len(sys.argv) > 3 else ".run/P36/dumps"
d = f"{root}/dumps_{tag}"
if fn is None:
print("alloc_table: a function name is required (arg 2)")
return 2
lreg, greg = section(f"{d}/{tag}.i.lreg", fn), section(f"{d}/{tag}.i.greg", fn)
if not lreg and not greg:
print(f"alloc_table: NO DUMP for {fn} under {d} — the section is absent, which is not an empty table. "
f"Check that cc1_dumps_tu.sh ran (it refuses a short preprocess now) and that the function is in this TU.")
return 3
regs = {}
for m in re.finditer(r"Register (\d+) used (\d+) times across (\d+) insns(?: in block (-?\d+))?([^\n]*)", lreg):
regs[int(m.group(1))] = dict(refs=int(m.group(2)), live=int(m.group(3)), block=m.group(4),
extra=m.group(5).strip())
hard, pref, conf = {}, {}, {}
for txt in (greg, lreg):
for m in re.finditer(r"Register (\d+) in (\d+)\.", txt):
hard.setdefault(int(m.group(1)), int(m.group(2)))
for m in re.finditer(r";;\s*(\d+) in (\d+)\b", txt): # the `;; 74 in 5` form
hard.setdefault(int(m.group(1)), int(m.group(2)))
for m in re.finditer(r";;\s*(\d+) preferences:\s*([0-9 ]+)", txt):
pref[int(m.group(1))] = [int(x) for x in m.group(2).split()]
for m in re.finditer(r";;\s*(\d+) conflicts:\s*([0-9 ]+)", txt):
conf[int(m.group(1))] = [int(x) for x in m.group(2).split()]
order = []
mo = re.search(r";;\s*(\d+) regs to allocate:\s*([0-9 ]+)", greg)
if mo:
order = [int(x) for x in mo.group(2).split()]
rows = []
for n, r in regs.items():
pri = (math.floor(math.log2(r["refs"])) * r["refs"] / r["live"]) * 10000 if r["refs"] > 0 and r["live"] > 0 else 0
rows.append((pri, n, r))
rows.sort(key=lambda x: (-x[0], x[1]))
print(f"alloc_table {fn} ({tag}): {len(regs)} pseudo(s) in the .lreg table"
+ (f"; .greg order: {' '.join(str(x) for x in order)}" if order else "; .greg printed no order line"))
print(f" {'pseudo':>7} {'hard':>5} {'pri':>10} {'refs':>5} {'live':>5} {'blk':>5} prefs / notes")
for pri, n, r in rows:
h = hard.get(n)
hn = NAMES.get(h, str(h)) if h is not None else "-"
note = r["extra"][:44]
if n in pref:
note = f"prefers {','.join(NAMES.get(x, str(x)) for x in pref[n])} " + note
if n in conf:
hardconf = [NAMES[x] for x in conf[n] if x in NAMES] # the HARD regs it may not take
if hardconf:
note = f"conflicts {','.join(hardconf)} " + note
print(f" r{n:<6} {hn:>5} {pri:10.1f} {r['refs']:5d} {r['live']:5d} {str(r['block']):>5} {note}")
# R32: the order line over-approximates the allocnos; anything it names that this table lacks is a SILENT GAP
missing = [x for x in order if x not in regs]
if missing:
print(f" COVERAGE GAP: the .greg order line names {len(missing)} allocno(s) absent from the .lreg table: "
f"{missing} — the table above is a SUBSET, not the allocation.")
return 1
if order:
print(f" coverage OK: every allocno in the .greg order line ({len(order)}) is in the table")
return 0
if __name__ == "__main__":
sys.exit(main())