Files

115 lines
6.4 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():
# global.c:594-603 stores the priority in a `register int` — TRUNCATED — and breaks a tie by allocno number (:607).
# S104 e12/e14: two closes hinged on ties the old float column hid (245.6 vs 245.2 are both 245).
pri = int((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")
tied = {p_ for p_, c in __import__("collections").Counter(p_ for p_, _n, _r in rows).items() if c > 1 and p_ > 0}
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
if pri in tied:
note = "TIE (lower allocno wins, global.c:607) " + note
print(f" r{n:<6} {hn:>5} {pri:10d} {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())