mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-28 06:49:47 -04:00
phase-36: T7 toolify a2 — generators R16 (constant holder inlined) and R17 (constant-run split), the directed form of a move R9 reached only by luck
- R16 writes a local whose only assignment is one integer literal at every use and deletes it. R6 stops at a temp read exactly once, so a holder read four times was invisible to the search and its whole family with it. Deleting it is byte-neutral alone but removes a quantity from the block, which is what lets the next move reach the allocator. - R17 splits a run of consecutive same-literal assignments by moving the nearest differently-valued one into it, at each interior split point. find_free_reg's live-range scan (local-alloc.c:2109-2110): while the two constants' ranges are disjoint they share a caller-saved register; splitting makes the first live across the second and it takes another colour. - known-true check: on the seed that keeps func_80168828's semantically-forced $4 pin, R16 then R17 reaches score 0 (OTHER; mine 108 ins, target 108) — MATCH at three of the six offered split points, in ten compiles where the blind search needed 2,271. - the engine selftest's caller-saved assertion is now the ordering invariant (every targeted lever before every blind family) rather than a fixed window widened once per new generator. - delever --selftest OK (4 new controls); delever_search --selftest OK; tool_census --check OK; SETUP row (R21).
This commit is contained in:
@@ -2212,7 +2212,117 @@ def sink_merges(text, tu, fn, d_):
|
||||
return out
|
||||
|
||||
|
||||
ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14", "R15")
|
||||
INT_LIT = re.compile(r"^\s*(?:\(\s*[A-Za-z_][\w \t*]*\)\s*)?(0[xX][0-9A-Fa-f]+|\d+)\s*$")
|
||||
|
||||
|
||||
def constant_holders(text, tu, fn, d_):
|
||||
"""[(description, candidate text)] — R16: a local whose ONLY assignment is one integer literal, written at every use
|
||||
and its declaration removed. R6 stops at a temp read exactly ONCE (the classic def-with-one-use); a constant holder is
|
||||
read many times and R6 never offered it, so the whole family was invisible to the search.
|
||||
|
||||
T7 agent a2's crack of func_80168828 (2026-09-10): its `$3` pin held `0x40` and was read as the RHS of four stores.
|
||||
Deleting the variable is byte-neutral BY ITSELF — the pin was never doing the work — but it removes a quantity from
|
||||
the block, which is what lets the next move reach the allocator (`qty_compare`, `local-alloc.c:1579-1595`, through the
|
||||
unrolled switch at `:1485-1512`). A pinned local holding one literal is a CONSTANT-HOLDER, not a register lever, and
|
||||
the readable spelling of a constant is the constant."""
|
||||
lines = text.split("\n")
|
||||
lo, hi = d_["line"], d_["end"] - 1
|
||||
masked = [sc.mask_text(l) for l in lines]
|
||||
occ = collections.defaultdict(list)
|
||||
for i in range(lo, hi):
|
||||
for m in IDENT.finditer(masked[i]):
|
||||
occ[m.group(1)].append(i)
|
||||
out = []
|
||||
for v, where in occ.items():
|
||||
asg, decl, uses = [], [], []
|
||||
for i in where:
|
||||
st = masked[i].strip()
|
||||
m = re.match(r"^(?:[A-Za-z_][\w \t]*[\s*]\s*\*?\s*)?%s\s*=(?!=)\s*(.+);\s*$" % re.escape(v), st)
|
||||
if m:
|
||||
asg.append((i, m.group(1)))
|
||||
elif is_decl_line(st) and "=" not in st.split(";")[0]:
|
||||
decl.append(i)
|
||||
else:
|
||||
uses.append(i)
|
||||
if len(asg) != 1 or len(decl) != 1 or not uses:
|
||||
continue
|
||||
if MULTI_DECL.match(masked[decl[0]]): # a shared declaration line: removing it would take the others
|
||||
continue
|
||||
lit = INT_LIT.match(asg[0][1])
|
||||
if not lit:
|
||||
continue
|
||||
# every use must be a plain read — never an address-of, a member/arrow base, or another assignment's target
|
||||
if any(re.search(r"&\s*%s(?![\w])|(?<![\w.>])%s\s*(?:\.|->|\[|=(?!=))" % (re.escape(v), re.escape(v)),
|
||||
masked[i]) for i in uses):
|
||||
continue
|
||||
cand = []
|
||||
for i, l in enumerate(lines):
|
||||
if i == decl[0] or i == asg[0][0]:
|
||||
continue
|
||||
cand.append(re.sub(r"(?<![\w.>])%s(?![\w])" % re.escape(v), lit.group(1), l) if i in uses else l)
|
||||
out.append((f"const-holder {v}={lit.group(1)} x{len(uses)}", "\n".join(cand)))
|
||||
return out
|
||||
|
||||
|
||||
def constant_run_splits(text, tu, fn, d_):
|
||||
"""[(description, candidate text)] — R17: a run of consecutive statements assigning the SAME integer literal, split by
|
||||
moving the nearest differently-valued literal assignment into it, at each split point.
|
||||
|
||||
T7 agent a2 (2026-09-10), the directed form of a move R9 already contains but reaches by luck: the mechanical search
|
||||
needed 2,271 compiles to find this swap in func_80168828, and R17 offers it in a handful. The decision is
|
||||
`find_free_reg`'s live-range scan, `local-alloc.c:2109-2110`
|
||||
(`for (ins = born_index; ins < dead_index; ins++) IOR_HARD_REG_SET (used, regs_live_at[ins])`): while the two constants'
|
||||
ranges are disjoint they share one caller-saved register; splitting the run makes the first live across the second, the
|
||||
intervals overlap and the second takes another colour. The discriminator in the dumps is the `.lreg` line `Register N
|
||||
used K times across M insns` — M grows when the split lands."""
|
||||
lines = text.split("\n")
|
||||
lo, hi = d_["line"], d_["end"] - 1
|
||||
masked = [sc.mask_text(l) for l in lines]
|
||||
|
||||
def lit_of(i):
|
||||
if not simple_stmt(masked[i]):
|
||||
return None
|
||||
m = re.match(r"^\s*[^=]+=(?!=)\s*(.+);\s*$", masked[i])
|
||||
if not m:
|
||||
return None
|
||||
g = INT_LIT.match(m.group(1))
|
||||
return g.group(1) if g else None
|
||||
|
||||
out = []
|
||||
i = lo
|
||||
while i < hi:
|
||||
k = lit_of(i)
|
||||
if k is None:
|
||||
i += 1
|
||||
continue
|
||||
j = i
|
||||
while j + 1 < hi and lit_of(j + 1) == k:
|
||||
j += 1
|
||||
if j - i + 1 < 2: # a run is two or more stores of the same literal
|
||||
i = j + 1
|
||||
continue
|
||||
donors = [d for d in (i - 1, j + 1) if lo <= d < hi and lit_of(d) is not None and lit_of(d) != k]
|
||||
for d in donors:
|
||||
for cut in range(i + 1, j + 1): # every interior split point of the run
|
||||
order = [x for x in range(i, j + 1)]
|
||||
seq = [lines[x] for x in order]
|
||||
seq.insert(cut - i, lines[d])
|
||||
cand = []
|
||||
for x, l in enumerate(lines):
|
||||
if x == d:
|
||||
continue
|
||||
if x == i:
|
||||
cand.extend(seq)
|
||||
elif i < x <= j:
|
||||
continue
|
||||
else:
|
||||
cand.append(l)
|
||||
out.append((f"const-split @{d + 1}->{cut + 1} ({lit_of(d)} into the {k} run)", "\n".join(cand)))
|
||||
i = j + 1
|
||||
return out
|
||||
|
||||
|
||||
ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14", "R15", "R16", "R17")
|
||||
RUNG_R_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7") # the free sweep's set (R8/R9 are the search engine's until measured)
|
||||
|
||||
|
||||
@@ -2320,6 +2430,12 @@ def recipe_candidates(text, tu, fn, names, limit=24, rng=None, cap=40, blocks=Tr
|
||||
if "R15" in fam:
|
||||
for desc, cand in sink_merges(text, tu, fn, d_):
|
||||
out.append(("R15", desc, cand))
|
||||
if "R16" in fam:
|
||||
for desc, cand in constant_holders(text, tu, fn, d_):
|
||||
out.append(("R16", desc, cand))
|
||||
if "R17" in fam:
|
||||
for desc, cand in constant_run_splits(text, tu, fn, d_):
|
||||
out.append(("R17", desc, cand))
|
||||
if blocks and "R7" in fam: # last: one candidate per statement, so the targeted recipes go first
|
||||
for desc, cand in block_wraps(text, tu, fn, d_):
|
||||
out.append(("R7", desc, cand))
|
||||
@@ -2585,7 +2701,13 @@ def propagate(a):
|
||||
if a.only:
|
||||
sibs = [k for k in sibs if any(o in k for o in a.only)]
|
||||
sibs = sibs[:a.limit] if a.limit else sibs
|
||||
print(f"delever --propagate: {tu}:{fn} -> {len(sibs)} sibling(s) of class {key[:12]}", flush=True)
|
||||
# A reshape may deliberately leave a lever standing (agent a2, S102: one of func_80168828's two pins is forced by the
|
||||
# shared header's `(void)` declaration and has no C source, the other was a constant-holder and came off). Its siblings
|
||||
# inherit exactly that shape, so the allowance is DERIVED from the exemplar's own banked text — the number of surviving
|
||||
# markers — and never simply asserted: a sibling that would carry MORE levers than the exemplar is refused below.
|
||||
ex_levers = src_row["after_text"].count(FAKE)
|
||||
print(f"delever --propagate: {tu}:{fn} -> {len(sibs)} sibling(s) of class {key[:12]}"
|
||||
+ (f"; the exemplar keeps {ex_levers} marked lever(s), so its siblings may too" if ex_levers else ""), flush=True)
|
||||
if not sibs:
|
||||
return 0, 0, 0 # R68: an empty work list is a refusal, not a success (a tuple like every return — the
|
||||
# bare `1` here killed run g4s's process after its real propagations, S101)
|
||||
@@ -2611,7 +2733,13 @@ def propagate(a):
|
||||
continue
|
||||
# IN PROCESS (S101): a subprocess per sibling reloaded the recipes and the includer map every time — ~1.3 s of the
|
||||
# ~1.5 s each sibling cost, ≈40 min for run g3's 1,503 siblings
|
||||
ok_, line = apply_body_core(stu, sfn, body, a.label, src_row.get("rung") or "R", source=f"propagate:{tu}:{fn}")
|
||||
if body.count(FAKE) > ex_levers:
|
||||
print(f" {stu}:{sfn}: the remap left {body.count(FAKE)} lever(s) where the exemplar keeps {ex_levers} "
|
||||
f"— SKIPPED", flush=True)
|
||||
bad += 1
|
||||
continue
|
||||
ok_, line = apply_body_core(stu, sfn, body, a.label, src_row.get("rung") or "R", source=f"propagate:{tu}:{fn}",
|
||||
allow_residue=(a.allow_residue or ex_levers > 0))
|
||||
print(f" {line[:200]}", flush=True)
|
||||
ok += ok_
|
||||
bad += not ok_
|
||||
@@ -3077,6 +3205,46 @@ def selftest():
|
||||
if len(ch) != 1 or len(ch[0][2]) != 3:
|
||||
fail(f"if_chains must see three arms in the fixture, got {ch}")
|
||||
|
||||
# R16 / R17 (T7 agent a2's crack of func_80168828, 2026-09-10): a constant holder inlined, then the run it fed split.
|
||||
CFIX = ("void func_80100000(void) {\n"
|
||||
" s32 c40;\n"
|
||||
" s32 other;\n"
|
||||
"\n"
|
||||
" c40 = 0x40;\n"
|
||||
" other = 0x10;\n"
|
||||
" st(0) = c40;\n"
|
||||
" st(1) = c40;\n"
|
||||
" st(2) = c40;\n"
|
||||
" st(3) = other;\n"
|
||||
"}")
|
||||
dC = next(r for r in sc.scan_text(CFIX, "src/fx/c.c", shared_defs=None)
|
||||
if r["form"] == "def" and r["name"] == "func_80100000")
|
||||
h16 = constant_holders(CFIX, "src/fx/c.c", "func_80100000", dC)
|
||||
if len(h16) != 2 or not any(d.startswith("const-holder c40=0x40 x3") for d, _ in h16):
|
||||
fail(f"R16 must inline a 3-use constant holder, got {[d for d, _ in h16]}")
|
||||
else:
|
||||
c16 = next(c for d, c in h16 if d.startswith("const-holder c40"))
|
||||
if "s32 c40;" in c16 or "c40 = 0x40;" in c16 or c16.count("st(0) = 0x40;") != 1:
|
||||
fail(f"R16 must delete the declaration and the assignment and write the literal: {c16!r}")
|
||||
# control: a holder whose value is not a literal, and one written twice, are not constant holders
|
||||
if any(d.startswith("const-holder") for d, _ in
|
||||
constant_holders(CFIX.replace("c40 = 0x40;", "c40 = f();"), "src/fx/c.c", "func_80100000",
|
||||
next(r for r in sc.scan_text(CFIX.replace("c40 = 0x40;", "c40 = f();"), "src/fx/c.c",
|
||||
shared_defs=None) if r["form"] == "def"))
|
||||
if d.startswith("const-holder c40")):
|
||||
fail("R16 must refuse a holder whose single assignment is not an integer literal")
|
||||
# R17 on the inlined text: the 0x10 store moved into the run of three 0x40 stores, at each interior split point
|
||||
c16 = next(c for d, c in h16 if d.startswith("const-holder c40"))
|
||||
d17 = next(r for r in sc.scan_text(c16, "src/fx/c.c", shared_defs=None) if r["form"] == "def")
|
||||
r17 = [d for d, _ in constant_run_splits(c16, "src/fx/c.c", "func_80100000", d17) if "into the 0x40 run" in d]
|
||||
if len(r17) != 2:
|
||||
fail(f"R17 must offer both interior split points of a three-store run, got {r17}")
|
||||
# control: a run of one store has no split
|
||||
if constant_run_splits(CFIX.replace(" st(1) = c40;\n st(2) = c40;\n", ""), "src/fx/c.c", "func_80100000",
|
||||
next(r for r in sc.scan_text(CFIX.replace(" st(1) = c40;\n st(2) = c40;\n", ""),
|
||||
"src/fx/c.c", shared_defs=None) if r["form"] == "def")):
|
||||
fail("R17 must refuse a run shorter than two statements")
|
||||
|
||||
# the oracle's crash classification on its real message forms (R103)
|
||||
if not oracle.SIGNAL_LINE.search("bash: line 1: 3845091 Done mipsel-linux-gnu-cpp ...\n 3845092 Aborted (core dumped) | tools/bin/gcc-2.7.2-psx/cc1 -quiet\n"):
|
||||
fail("SIGNAL_LINE must match bash's job-status block")
|
||||
|
||||
@@ -85,15 +85,17 @@ FAMILIES = {
|
||||
# R15 (the sink) is the arm-scoped form of the same tie: a value set in every arm of an if/else chain is a CROSS-BLOCK
|
||||
# pseudo local-alloc never gives a quantity, so the arm holds two quantities and takes block_alloc's unrolled case 2;
|
||||
# sinking makes it three, and case 3 falls through into case 2 and undoes its own exchange (T7 agent a1, func_80156044).
|
||||
"REG-caller": ("R6", "R8", "R15", "R5", "R10", "R12", "R14", "R13", "R3", "R7", "R9", "R2", "R4"),
|
||||
"REG-caller": ("R6", "R16", "R8", "R15", "R17", "R5", "R10", "R12", "R14", "R13", "R3", "R7", "R9", "R2", "R4"),
|
||||
# the s-bank order is global.c's allocno_compare (ref weight x live length), declaration order only on an exact tie
|
||||
"REG-callee": ("R2", "R4", "R3", "R6", "R8", "R15", "R12", "R7", "R9", "R10", "R14", "R13", "R5"),
|
||||
"REG-mixed": ("R6", "R2", "R15", "R5", "R10", "R4", "R3", "R8", "R12", "R13", "R14", "R7", "R9"),
|
||||
"REG-callee": ("R2", "R4", "R3", "R6", "R16", "R8", "R15", "R12", "R7", "R9", "R17", "R10", "R14", "R13", "R5"),
|
||||
"REG-mixed": ("R6", "R16", "R2", "R15", "R17", "R5", "R10", "R4", "R3", "R8", "R12", "R13", "R14", "R7", "R9"),
|
||||
# a copy dies to cse's canon_reg or the local-alloc tie unless its destination changes MODE (the width); an address
|
||||
# pseudo lives when a pointer local is used twice; a value named once is computed once; a short PARAMETER is extended in place
|
||||
"COUNT": ("R12", "R14", "R15", "R6", "R8", "R3", "R7", "R5", "R13", "R9", "R10", "R2", "R4"),
|
||||
"COUNT": ("R12", "R16", "R14", "R15", "R6", "R8", "R3", "R7", "R17", "R5", "R13", "R9", "R10", "R2", "R4"),
|
||||
# statement order IS the schedule among equal-priority insns (rank_for_schedule's LUID tie-break); do-while is a barrier
|
||||
"ORDER": ("R9", "R7", "R13", "R3", "R6", "R8", "R5", "R12", "R14", "R10", "R15", "R2", "R4"),
|
||||
# R17 is the DIRECTED form of the run-split R9 reaches only by luck: agent a2 measured 2,271 compiles for R9 to find it
|
||||
# in func_80168828 and R16+R17 reproduce the same close in ten.
|
||||
"ORDER": ("R17", "R9", "R7", "R13", "R3", "R16", "R6", "R8", "R5", "R12", "R14", "R10", "R15", "R2", "R4"),
|
||||
"MIXED": dl.ALL_FAMILIES,
|
||||
"OTHER": dl.ALL_FAMILIES,
|
||||
}
|
||||
@@ -842,11 +844,14 @@ def selftest():
|
||||
c = classify(mine, tgt)
|
||||
if c["kind"] != "REG" or c["bank"] != "caller" or c["score"] != 3:
|
||||
fail(f"REG-caller classification wrong: {c}")
|
||||
# the temp move leads, and the two byte-proven caller-saved levers (R5 the commutative swap, R15 the sink) are drawn
|
||||
# early — R15 joined the front at S102 when agent a1's crack showed the arm-scoped form of the same allocator tie.
|
||||
if family_key(c) != "REG-caller" or FAMILIES["REG-caller"][0] != "R6" \
|
||||
or not {"R5", "R15"} <= set(FAMILIES["REG-caller"][:4]):
|
||||
fail(f"REG-caller family wrong: {family_key(c)} {FAMILIES['REG-caller'][:4]}")
|
||||
# The invariant, not a fixed window (widening the window once per new generator hid what it was for): the temp move
|
||||
# leads, and every TARGETED caller-saved lever — R5 the commutative swap, R15 the sink, R16 the constant holder —
|
||||
# is drawn before the BLIND families that permute declarations or statements wholesale (R9, R2, R4).
|
||||
caller = FAMILIES["REG-caller"]
|
||||
targeted, blind = {"R5", "R15", "R16"}, {"R9", "R2", "R4"}
|
||||
if family_key(c) != "REG-caller" or caller[0] != "R6" or not targeted <= set(caller) \
|
||||
or max(caller.index(t) for t in targeted) > min(caller.index(b) for b in blind):
|
||||
fail(f"REG-caller family wrong: {family_key(c)} {caller}")
|
||||
# a callee-saved swap: addu s0,a0,zero vs addu s1,a0,zero
|
||||
c = classify([_ins(0x00808021)], [_ins(0x00808821)])
|
||||
if c["kind"] != "REG" or c["bank"] != "callee":
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+157
-1
@@ -2212,7 +2212,117 @@ def sink_merges(text, tu, fn, d_):
|
||||
return out
|
||||
|
||||
|
||||
ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14", "R15")
|
||||
INT_LIT = re.compile(r"^\s*(?:\(\s*[A-Za-z_][\w \t*]*\)\s*)?(0[xX][0-9A-Fa-f]+|\d+)\s*$")
|
||||
|
||||
|
||||
def constant_holders(text, tu, fn, d_):
|
||||
"""[(description, candidate text)] — R16: a local whose ONLY assignment is one integer literal, written at every use
|
||||
and its declaration removed. R6 stops at a temp read exactly ONCE (the classic def-with-one-use); a constant holder is
|
||||
read many times and R6 never offered it, so the whole family was invisible to the search.
|
||||
|
||||
T7 agent a2's crack of func_80168828 (2026-09-10): its `$3` pin held `0x40` and was read as the RHS of four stores.
|
||||
Deleting the variable is byte-neutral BY ITSELF — the pin was never doing the work — but it removes a quantity from
|
||||
the block, which is what lets the next move reach the allocator (`qty_compare`, `local-alloc.c:1579-1595`, through the
|
||||
unrolled switch at `:1485-1512`). A pinned local holding one literal is a CONSTANT-HOLDER, not a register lever, and
|
||||
the readable spelling of a constant is the constant."""
|
||||
lines = text.split("\n")
|
||||
lo, hi = d_["line"], d_["end"] - 1
|
||||
masked = [sc.mask_text(l) for l in lines]
|
||||
occ = collections.defaultdict(list)
|
||||
for i in range(lo, hi):
|
||||
for m in IDENT.finditer(masked[i]):
|
||||
occ[m.group(1)].append(i)
|
||||
out = []
|
||||
for v, where in occ.items():
|
||||
asg, decl, uses = [], [], []
|
||||
for i in where:
|
||||
st = masked[i].strip()
|
||||
m = re.match(r"^(?:[A-Za-z_][\w \t]*[\s*]\s*\*?\s*)?%s\s*=(?!=)\s*(.+);\s*$" % re.escape(v), st)
|
||||
if m:
|
||||
asg.append((i, m.group(1)))
|
||||
elif is_decl_line(st) and "=" not in st.split(";")[0]:
|
||||
decl.append(i)
|
||||
else:
|
||||
uses.append(i)
|
||||
if len(asg) != 1 or len(decl) != 1 or not uses:
|
||||
continue
|
||||
if MULTI_DECL.match(masked[decl[0]]): # a shared declaration line: removing it would take the others
|
||||
continue
|
||||
lit = INT_LIT.match(asg[0][1])
|
||||
if not lit:
|
||||
continue
|
||||
# every use must be a plain read — never an address-of, a member/arrow base, or another assignment's target
|
||||
if any(re.search(r"&\s*%s(?![\w])|(?<![\w.>])%s\s*(?:\.|->|\[|=(?!=))" % (re.escape(v), re.escape(v)),
|
||||
masked[i]) for i in uses):
|
||||
continue
|
||||
cand = []
|
||||
for i, l in enumerate(lines):
|
||||
if i == decl[0] or i == asg[0][0]:
|
||||
continue
|
||||
cand.append(re.sub(r"(?<![\w.>])%s(?![\w])" % re.escape(v), lit.group(1), l) if i in uses else l)
|
||||
out.append((f"const-holder {v}={lit.group(1)} x{len(uses)}", "\n".join(cand)))
|
||||
return out
|
||||
|
||||
|
||||
def constant_run_splits(text, tu, fn, d_):
|
||||
"""[(description, candidate text)] — R17: a run of consecutive statements assigning the SAME integer literal, split by
|
||||
moving the nearest differently-valued literal assignment into it, at each split point.
|
||||
|
||||
T7 agent a2 (2026-09-10), the directed form of a move R9 already contains but reaches by luck: the mechanical search
|
||||
needed 2,271 compiles to find this swap in func_80168828, and R17 offers it in a handful. The decision is
|
||||
`find_free_reg`'s live-range scan, `local-alloc.c:2109-2110`
|
||||
(`for (ins = born_index; ins < dead_index; ins++) IOR_HARD_REG_SET (used, regs_live_at[ins])`): while the two constants'
|
||||
ranges are disjoint they share one caller-saved register; splitting the run makes the first live across the second, the
|
||||
intervals overlap and the second takes another colour. The discriminator in the dumps is the `.lreg` line `Register N
|
||||
used K times across M insns` — M grows when the split lands."""
|
||||
lines = text.split("\n")
|
||||
lo, hi = d_["line"], d_["end"] - 1
|
||||
masked = [sc.mask_text(l) for l in lines]
|
||||
|
||||
def lit_of(i):
|
||||
if not simple_stmt(masked[i]):
|
||||
return None
|
||||
m = re.match(r"^\s*[^=]+=(?!=)\s*(.+);\s*$", masked[i])
|
||||
if not m:
|
||||
return None
|
||||
g = INT_LIT.match(m.group(1))
|
||||
return g.group(1) if g else None
|
||||
|
||||
out = []
|
||||
i = lo
|
||||
while i < hi:
|
||||
k = lit_of(i)
|
||||
if k is None:
|
||||
i += 1
|
||||
continue
|
||||
j = i
|
||||
while j + 1 < hi and lit_of(j + 1) == k:
|
||||
j += 1
|
||||
if j - i + 1 < 2: # a run is two or more stores of the same literal
|
||||
i = j + 1
|
||||
continue
|
||||
donors = [d for d in (i - 1, j + 1) if lo <= d < hi and lit_of(d) is not None and lit_of(d) != k]
|
||||
for d in donors:
|
||||
for cut in range(i + 1, j + 1): # every interior split point of the run
|
||||
order = [x for x in range(i, j + 1)]
|
||||
seq = [lines[x] for x in order]
|
||||
seq.insert(cut - i, lines[d])
|
||||
cand = []
|
||||
for x, l in enumerate(lines):
|
||||
if x == d:
|
||||
continue
|
||||
if x == i:
|
||||
cand.extend(seq)
|
||||
elif i < x <= j:
|
||||
continue
|
||||
else:
|
||||
cand.append(l)
|
||||
out.append((f"const-split @{d + 1}->{cut + 1} ({lit_of(d)} into the {k} run)", "\n".join(cand)))
|
||||
i = j + 1
|
||||
return out
|
||||
|
||||
|
||||
ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14", "R15", "R16", "R17")
|
||||
RUNG_R_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7") # the free sweep's set (R8/R9 are the search engine's until measured)
|
||||
|
||||
|
||||
@@ -2320,6 +2430,12 @@ def recipe_candidates(text, tu, fn, names, limit=24, rng=None, cap=40, blocks=Tr
|
||||
if "R15" in fam:
|
||||
for desc, cand in sink_merges(text, tu, fn, d_):
|
||||
out.append(("R15", desc, cand))
|
||||
if "R16" in fam:
|
||||
for desc, cand in constant_holders(text, tu, fn, d_):
|
||||
out.append(("R16", desc, cand))
|
||||
if "R17" in fam:
|
||||
for desc, cand in constant_run_splits(text, tu, fn, d_):
|
||||
out.append(("R17", desc, cand))
|
||||
if blocks and "R7" in fam: # last: one candidate per statement, so the targeted recipes go first
|
||||
for desc, cand in block_wraps(text, tu, fn, d_):
|
||||
out.append(("R7", desc, cand))
|
||||
@@ -3089,6 +3205,46 @@ def selftest():
|
||||
if len(ch) != 1 or len(ch[0][2]) != 3:
|
||||
fail(f"if_chains must see three arms in the fixture, got {ch}")
|
||||
|
||||
# R16 / R17 (T7 agent a2's crack of func_80168828, 2026-09-10): a constant holder inlined, then the run it fed split.
|
||||
CFIX = ("void func_80100000(void) {\n"
|
||||
" s32 c40;\n"
|
||||
" s32 other;\n"
|
||||
"\n"
|
||||
" c40 = 0x40;\n"
|
||||
" other = 0x10;\n"
|
||||
" st(0) = c40;\n"
|
||||
" st(1) = c40;\n"
|
||||
" st(2) = c40;\n"
|
||||
" st(3) = other;\n"
|
||||
"}")
|
||||
dC = next(r for r in sc.scan_text(CFIX, "src/fx/c.c", shared_defs=None)
|
||||
if r["form"] == "def" and r["name"] == "func_80100000")
|
||||
h16 = constant_holders(CFIX, "src/fx/c.c", "func_80100000", dC)
|
||||
if len(h16) != 2 or not any(d.startswith("const-holder c40=0x40 x3") for d, _ in h16):
|
||||
fail(f"R16 must inline a 3-use constant holder, got {[d for d, _ in h16]}")
|
||||
else:
|
||||
c16 = next(c for d, c in h16 if d.startswith("const-holder c40"))
|
||||
if "s32 c40;" in c16 or "c40 = 0x40;" in c16 or c16.count("st(0) = 0x40;") != 1:
|
||||
fail(f"R16 must delete the declaration and the assignment and write the literal: {c16!r}")
|
||||
# control: a holder whose value is not a literal, and one written twice, are not constant holders
|
||||
if any(d.startswith("const-holder") for d, _ in
|
||||
constant_holders(CFIX.replace("c40 = 0x40;", "c40 = f();"), "src/fx/c.c", "func_80100000",
|
||||
next(r for r in sc.scan_text(CFIX.replace("c40 = 0x40;", "c40 = f();"), "src/fx/c.c",
|
||||
shared_defs=None) if r["form"] == "def"))
|
||||
if d.startswith("const-holder c40")):
|
||||
fail("R16 must refuse a holder whose single assignment is not an integer literal")
|
||||
# R17 on the inlined text: the 0x10 store moved into the run of three 0x40 stores, at each interior split point
|
||||
c16 = next(c for d, c in h16 if d.startswith("const-holder c40"))
|
||||
d17 = next(r for r in sc.scan_text(c16, "src/fx/c.c", shared_defs=None) if r["form"] == "def")
|
||||
r17 = [d for d, _ in constant_run_splits(c16, "src/fx/c.c", "func_80100000", d17) if "into the 0x40 run" in d]
|
||||
if len(r17) != 2:
|
||||
fail(f"R17 must offer both interior split points of a three-store run, got {r17}")
|
||||
# control: a run of one store has no split
|
||||
if constant_run_splits(CFIX.replace(" st(1) = c40;\n st(2) = c40;\n", ""), "src/fx/c.c", "func_80100000",
|
||||
next(r for r in sc.scan_text(CFIX.replace(" st(1) = c40;\n st(2) = c40;\n", ""),
|
||||
"src/fx/c.c", shared_defs=None) if r["form"] == "def")):
|
||||
fail("R17 must refuse a run shorter than two statements")
|
||||
|
||||
# the oracle's crash classification on its real message forms (R103)
|
||||
if not oracle.SIGNAL_LINE.search("bash: line 1: 3845091 Done mipsel-linux-gnu-cpp ...\n 3845092 Aborted (core dumped) | tools/bin/gcc-2.7.2-psx/cc1 -quiet\n"):
|
||||
fail("SIGNAL_LINE must match bash's job-status block")
|
||||
|
||||
+15
-10
@@ -85,15 +85,17 @@ FAMILIES = {
|
||||
# R15 (the sink) is the arm-scoped form of the same tie: a value set in every arm of an if/else chain is a CROSS-BLOCK
|
||||
# pseudo local-alloc never gives a quantity, so the arm holds two quantities and takes block_alloc's unrolled case 2;
|
||||
# sinking makes it three, and case 3 falls through into case 2 and undoes its own exchange (T7 agent a1, func_80156044).
|
||||
"REG-caller": ("R6", "R8", "R15", "R5", "R10", "R12", "R14", "R13", "R3", "R7", "R9", "R2", "R4"),
|
||||
"REG-caller": ("R6", "R16", "R8", "R15", "R17", "R5", "R10", "R12", "R14", "R13", "R3", "R7", "R9", "R2", "R4"),
|
||||
# the s-bank order is global.c's allocno_compare (ref weight x live length), declaration order only on an exact tie
|
||||
"REG-callee": ("R2", "R4", "R3", "R6", "R8", "R15", "R12", "R7", "R9", "R10", "R14", "R13", "R5"),
|
||||
"REG-mixed": ("R6", "R2", "R15", "R5", "R10", "R4", "R3", "R8", "R12", "R13", "R14", "R7", "R9"),
|
||||
"REG-callee": ("R2", "R4", "R3", "R6", "R16", "R8", "R15", "R12", "R7", "R9", "R17", "R10", "R14", "R13", "R5"),
|
||||
"REG-mixed": ("R6", "R16", "R2", "R15", "R17", "R5", "R10", "R4", "R3", "R8", "R12", "R13", "R14", "R7", "R9"),
|
||||
# a copy dies to cse's canon_reg or the local-alloc tie unless its destination changes MODE (the width); an address
|
||||
# pseudo lives when a pointer local is used twice; a value named once is computed once; a short PARAMETER is extended in place
|
||||
"COUNT": ("R12", "R14", "R15", "R6", "R8", "R3", "R7", "R5", "R13", "R9", "R10", "R2", "R4"),
|
||||
"COUNT": ("R12", "R16", "R14", "R15", "R6", "R8", "R3", "R7", "R17", "R5", "R13", "R9", "R10", "R2", "R4"),
|
||||
# statement order IS the schedule among equal-priority insns (rank_for_schedule's LUID tie-break); do-while is a barrier
|
||||
"ORDER": ("R9", "R7", "R13", "R3", "R6", "R8", "R5", "R12", "R14", "R10", "R15", "R2", "R4"),
|
||||
# R17 is the DIRECTED form of the run-split R9 reaches only by luck: agent a2 measured 2,271 compiles for R9 to find it
|
||||
# in func_80168828 and R16+R17 reproduce the same close in ten.
|
||||
"ORDER": ("R17", "R9", "R7", "R13", "R3", "R16", "R6", "R8", "R5", "R12", "R14", "R10", "R15", "R2", "R4"),
|
||||
"MIXED": dl.ALL_FAMILIES,
|
||||
"OTHER": dl.ALL_FAMILIES,
|
||||
}
|
||||
@@ -842,11 +844,14 @@ def selftest():
|
||||
c = classify(mine, tgt)
|
||||
if c["kind"] != "REG" or c["bank"] != "caller" or c["score"] != 3:
|
||||
fail(f"REG-caller classification wrong: {c}")
|
||||
# the temp move leads, and the two byte-proven caller-saved levers (R5 the commutative swap, R15 the sink) are drawn
|
||||
# early — R15 joined the front at S102 when agent a1's crack showed the arm-scoped form of the same allocator tie.
|
||||
if family_key(c) != "REG-caller" or FAMILIES["REG-caller"][0] != "R6" \
|
||||
or not {"R5", "R15"} <= set(FAMILIES["REG-caller"][:4]):
|
||||
fail(f"REG-caller family wrong: {family_key(c)} {FAMILIES['REG-caller'][:4]}")
|
||||
# The invariant, not a fixed window (widening the window once per new generator hid what it was for): the temp move
|
||||
# leads, and every TARGETED caller-saved lever — R5 the commutative swap, R15 the sink, R16 the constant holder —
|
||||
# is drawn before the BLIND families that permute declarations or statements wholesale (R9, R2, R4).
|
||||
caller = FAMILIES["REG-caller"]
|
||||
targeted, blind = {"R5", "R15", "R16"}, {"R9", "R2", "R4"}
|
||||
if family_key(c) != "REG-caller" or caller[0] != "R6" or not targeted <= set(caller) \
|
||||
or max(caller.index(t) for t in targeted) > min(caller.index(b) for b in blind):
|
||||
fail(f"REG-caller family wrong: {family_key(c)} {caller}")
|
||||
# a callee-saved swap: addu s0,a0,zero vs addu s1,a0,zero
|
||||
c = classify([_ins(0x00808021)], [_ins(0x00808821)])
|
||||
if c["kind"] != "REG" or c["bank"] != "callee":
|
||||
|
||||
Reference in New Issue
Block a user