diff --git a/docs/SETUP.md b/docs/SETUP.md index c090aaad2..88cb3d037 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -1841,7 +1841,7 @@ CLAIM, not a fact — two were refuted on bytes on 2026-09-10.** R28 …` feeds R28 the real TU like R27. **Then (S104, from d22/d24/d25):** **R23 accepts a `case K:` / `default:` label as a statement boundary** (it had refused d22's `t` after `case 2:`; known-true `split t into 2` → 0); **R35 `drop_param_copies`** (`T x = argN;` never reassigned → `argN` used; d24's start 36 → 23 — its close was joint); **R36 `merge_set_chains`** (`x = A; - [≤2 unrelated lines] x += B;` → one assignment; d25's frame-only residual; known-true 0). **R31 widened** (d39: an all-shifts `(s16)` candidate — its two sites closed only together; known-true 0), **R35 widened** (d38: a CAST copy `T *p = (T *)a1;` becomes `((T *)a1)` at each use; known-true 0), `named_definitions` indexes `func_X_body(` asm-label definitions (two bank lists failed to resolve them); **R38 `shift_to_division`** (e7: `if (v < 0) v += 2^k-1; v = v >> k;` — gcc's own expansion — written `v = v / 2^k;` or merged into the preceding `v = (E) / 2^k;`; on e7's start text alone 6 → 6/7: e7's close was joint with a width move, so R38 is a composition move; 7 residue bodies carry the shape), **R39 `duplicate_join_statement`** (e12/e14: the simple statement after an if/else join copied into both arms to split an integer-truncated `allocno_compare` tie — cross-jump re-merges the copies; known-true 0 on e14's func_8017BEBC and func_8017CAD4; needs the `} else {` line shape — e12's func_8017E35C produced no candidate; widened S104: the first 1–3 simple statements after the join, blank lines skipped — e24's func_80180E24 store pair closes at ×2), **R40 `return_preincrement`** (e2/e16: `return i + 1;` → `return ++i;`, zero bytes, more refs; known-true 0 on both), **R41 `swap_if_else_arms`** (e16: `if (C) {A} else {B}` → `if (!C) {B} else {A}`, one site at a time — the arm order decides reorg's delay-slot steal; known-true 0), **R42 `move_statement_far`** (e19: one simple statement moved 2–6 simple statements down within its block — R9 only swaps neighbours; on e19's func_8018230C start text alone best 8: its close also inlined a temp and dropped an inner block, so R42 is a composition move), **R43 `sign_test_to_mask`** (e24/e26: `if (E < 0)` whose arms set/clear bit 31 → `if ((u32)(E) & 0x80000000)`, plus the variant with never-used pad arrays dropped; alone 9 / 4 on the two start texts — both closes also deleted temps: a composition move), **R37 `return_constants`** (d27: a result local `r = 0; if (A) r = (B); return r;` → `if (A && B) return 1; return 0;` or the nested form — jump1's store-flag on the hard `$v0`; known-true 0 both spellings). **(S105) R44 `counter_derived_pointer`** (e21/f2: a pointer initialised before a counted loop and stepped `p += K` inside it — its own biv, never eliminated while a bare `p[0]` is read (`loop.c:4196`, `:6022`) — re-derived as `p = (T *)(BASE) + i * K;` at the loop top for each up-counter `i` of that block (`(i - C)` when it starts at C; a second `&SYM[c + i * K]` spelling for an uncast symbol base); refuses a down-counter, a second assignment, `&p`; known-true: alone 0 on e21's func_80037EA0 start text (from 33) and f2's measured 18 on func_80038838's loop 2; composed with R22 it reaches f2's 0). **R22 extended + a blind spot fixed (S105):** two pointers derived from ONE base expression at two offsets (`a1 = (u8 *)arg0 + 0x1B` beside `a3 = … + 0x1A`) merge like `q = p + K` (f2's loop 1, alone 3); and R22/R44's `&p` refusal had matched the `&&` operator (`… != 0 && p[0]`) since S103 — every body testing its pointer with `&&` was silently never offered R22 (fixed to `(?> k;` — gcc's own expansion — written `v = v / 2^k;` or merged into the preceding `v = (E) / 2^k;`; on e7's start text alone 6 → 6/7: e7's close was joint with a width move, so R38 is a composition move; 7 residue bodies carry the shape), **R39 `duplicate_join_statement`** (e12/e14: the simple statement after an if/else join copied into both arms to split an integer-truncated `allocno_compare` tie — cross-jump re-merges the copies; known-true 0 on e14's func_8017BEBC and func_8017CAD4; needs the `} else {` line shape — e12's func_8017E35C produced no candidate; widened S104: the first 1–3 simple statements after the join, blank lines skipped — e24's func_80180E24 store pair closes at ×2), **R40 `return_preincrement`** (e2/e16: `return i + 1;` → `return ++i;`, zero bytes, more refs; known-true 0 on both), **R41 `swap_if_else_arms`** (e16: `if (C) {A} else {B}` → `if (!C) {B} else {A}`, one site at a time — the arm order decides reorg's delay-slot steal; known-true 0), **R42 `move_statement_far`** (e19: one simple statement moved 2–6 simple statements down within its block — R9 only swaps neighbours; on e19's func_8018230C start text alone best 8: its close also inlined a temp and dropped an inner block, so R42 is a composition move), **R43 `sign_test_to_mask`** (e24/e26: `if (E < 0)` whose arms set/clear bit 31 → `if ((u32)(E) & 0x80000000)`, plus the variant with never-used pad arrays dropped; alone 9 / 4 on the two start texts — both closes also deleted temps: a composition move), **R37 `return_constants`** (d27: a result local `r = 0; if (A) r = (B); return r;` → `if (A && B) return 1; return 0;` or the nested form — jump1's store-flag on the hard `$v0`; known-true 0 both spellings). **(S105) R44 `counter_derived_pointer`** (e21/f2: a pointer initialised before a counted loop and stepped `p += K` inside it — its own biv, never eliminated while a bare `p[0]` is read (`loop.c:4196`, `:6022`) — re-derived as `p = (T *)(BASE) + i * K;` at the loop top for each up-counter `i` of that block (`(i - C)` when it starts at C; a second `&SYM[c + i * K]` spelling for an uncast symbol base); refuses a down-counter, a second assignment, `&p`; known-true: alone 0 on e21's func_80037EA0 start text (from 33) and f2's measured 18 on func_80038838's loop 2; composed with R22 it reaches f2's 0). **R22 extended + a blind spot fixed (S105):** two pointers derived from ONE base expression at two offsets (`a1 = (u8 *)arg0 + 0x1B` beside `a3 = … + 0x1A`) merge like `q = p + K` (f2's loop 1, alone 3); and R22/R44's `&p` refusal had matched the `&&` operator (`… != 0 && p[0]`) since S103 — every body testing its pointer with `&&` was silently never offered R22 (fixed to `(?])([A-Za-z_]\w*)\s*\+\s*(%s)(?=\s*[,)]))" % (_INT, _INT), masked[i]): + base, K = (m.group(1), m.group(2)) if m.group(1) else (m.group(3), m.group(4)) + if base not in ptrs or not re.search(r"\b[A-Za-z_]\w*\s*\(", masked[i]): + continue + k = int(K, 0) + stores = [j for j in range(lo, i) if re.match(r"^\s*%s\s*\[\s*%s\s*\]\s*(?:[-+|&^*/%%]|<<|>>)?=(?!=)" % (re.escape(base), _INT), masked[j]) + and int(re.match(r"^\s*%s\s*\[\s*(%s)\s*\]" % (re.escape(base), _INT), masked[j]).group(1), 0) == k] + if stores and (base, k) not in [(b, kk) for b, kk, _, _ in pairs]: + pairs.append((base, k, i, stores)) + if not pairs: + return [] + used = {n for i in range(lo, hi) for n in re.findall(r"\b[A-Za-z_]\w*\b", masked[i])} + + def apply(sel): + cand = list(lines) + decls = [] + names = {} + for base, k, ci, stores in sel: + n = 1 + while f"p{n}" in used or f"p{n}" in names.values(): + n += 1 + names[(base, k)] = f"p{n}" + used.add(f"p{n}") + bi, bt, ind = ptrs[base] + decls.append(f"{ind}{bt} *{names[(base, k)]};") + for j in stores: + cand[j] = re.sub(r"^(\s*)%s\s*\[\s*%s\s*\]" % (re.escape(base), _INT), lambda m: f"{m.group(1)}*{names[(base, k)]}", cand[j], count=1) + ks = re.search(r"%s\s*\[\s*(%s)\s*\]|%s\s*\+\s*(%s)" % (re.escape(base), _INT, re.escape(base), _INT), lines[ci]) + cand[ci] = re.sub(r"&\s*%s\s*\[\s*(%s)\s*\]|(?])%s\s*\+\s*(%s)(?=\s*[,)])" % (re.escape(base), _INT, re.escape(base), _INT), + lambda m: names[(base, k)] if int(m.group(1) or m.group(2), 0) == k else m.group(0), cand[ci]) + first = min(stores) + ind2 = re.match(r"^\s*", lines[first]).group(0) + kspell = ks.group(1) or ks.group(2) + cand[first] = f"{ind2}{names[(base, k)]} = &{base}[{kspell}];\n" + cand[first] + cand[decl_end] = cand[decl_end] + "\n" + "\n".join(decls) + return "\n".join(cand) + out = [(f"derived-ptr {b}[{k}] @{min(st) + 1}", apply([(b, k, ci, st)])) for b, k, ci, st in pairs] + if len(pairs) > 1: + out.append((f"derived-ptr ALL {len(pairs)}", apply(pairs))) + return out + + +def set_once_chain(text, tu, fn, d_, _no_all=False): + """[(description, candidate text)] — R46: a compound-assignment CHAIN on one temp folded into its final store. + + T7 agent f5 (ov_SC02_005, three witnesses, P36 S105): `v &= 0x1F; v -= K; DST = v;` sets ONE pseudo several times, so + neither the `andi` nor the `addiu` is a `birthing_insn_p` (`reg_n_sets == 1` fails, `sched.c:2468-2546`); their priority + stays 1 and the backward list scheduler emits the chain at the block TOP, ahead of the boosted `la`/`li` setups the target + has first. Written `DST = (v & 0x1F) - K;` every temp is set once, the chain gets the boost too and the LUID tie-break + (`sched.c:2428`) puts it after the independent instructions. The exact INVERSE of METHOD step 18's multi-set trick: the + `.sched` priority column (`7f000001` vs `1`) says which direction a body needs, and the byte oracle decides. + + The rewrite: 1–3 consecutive `v OP= E;` lines (or `v = v OP E;`) followed by `DST = v;` → `DST = ((v OP1 E1) OP2 E2);` with + the chain lines deleted; refused unless `v` is dead after the store until its next plain assignment (no read of `v` in + between), and unless every chain operand is a literal or an identifier.""" + lines = text.split("\n") + masked = [sc.mask_text(l) for l in lines] + lo, hi = d_["line"], d_["end"] - 1 + if any(l.lstrip().startswith("#") for l in lines[lo:hi]): + return [] + OPS = r"(\+|-|\*|/|%|&|\||\^|<<|>>)" + CH = re.compile(r"^(\s*)([A-Za-z_]\w*)\s*%s=\s*([A-Za-z_]\w*|-?%s)\s*;\s*$" % (OPS, _INT)) + CH2 = re.compile(r"^(\s*)([A-Za-z_]\w*)\s*=\s*\2\s*%s\s*([A-Za-z_]\w*|-?%s)\s*;\s*$" % (OPS, _INT)) + ST = re.compile(r"^(\s*)(.+?)\s*=\s*([A-Za-z_]\w*)\s*;\s*$") + out = [] + i = lo + while i < hi: + m = CH.match(masked[i]) or CH2.match(masked[i]) + if not m: + i += 1 + continue + v = m.group(2) + chain = [] + j = i + while j < hi and len(chain) < 3: + mm = CH.match(masked[j]) or CH2.match(masked[j]) + if not mm or mm.group(2) != v: + break + chain.append((mm.group(3), mm.group(4))) + j += 1 + ms = ST.match(masked[j]) if j < hi else None + if not chain or not ms or ms.group(3) != v or "(" in ms.group(2) and ms.group(2).count("(") != ms.group(2).count(")"): + i += 1 + continue + # v must be dead after the store until its next plain assignment + dead = True + for k in range(j + 1, hi): + if re.match(r"^\s*%s\s*=(?!=)" % re.escape(v), masked[k]): + break + if re.search(r"(?])%s\b" % re.escape(v), masked[k]): + dead = False + break + if not dead: + i = j + 1 + continue + expr = v + for op, e in chain: + expr = f"({expr} {op} {e})" + expr = expr[1:-1] if len(chain) == 1 else expr + cand = list(lines) + for k in range(i, j): + cand[k] = None + cand[j] = f"{ms.group(1)}{lines[j][len(ms.group(1)):lines[j].index('=')].rstrip()} = {expr};" + out.append((f"set-once chain {v} @{j + 1}", "\n".join(l for l in cand if l is not None))) + i = j + 1 + if len(out) > 1 and not _no_all: + # all chains at once (f5's bodies carried two): apply the first single candidate repeatedly on the evolving text + t = text + for _ in range(len(out)): + r = set_once_chain(t, tu, fn, body_span_of(t, tu, fn) or d_, _no_all=True) + if not r: + break + t = r[0][1] + if t != text: + out.append((f"set-once chain ALL {len(out)}", t)) + return out + + +def body_span_of(text, tu, fn): + return next((r for r in sc.scan_text(text, tu, shared_defs=None) if r["form"] == "def" and r["name"] == fn), None) + + def _drop_dead_pads(lines, lo, hi): """lines with every never-used (or only `(void)&x;`-used) array local deleted; None if there is none.""" masked = [sc.mask_text(l) for l in lines] @@ -4094,7 +4249,7 @@ def named_ports(tu, fn, max_donors=6): return out -ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14", "R15", "R16", "R17", "R18", "R19", "R20", "R21", "R22", "R23", "R24", "R25", "R26", "R27", "R28", "R29", "R31", "R32", "R33", "R34", "R35", "R36", "R37", "R38", "R39", "R40", "R41", "R42", "R43", "R44") +ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14", "R15", "R16", "R17", "R18", "R19", "R20", "R21", "R22", "R23", "R24", "R25", "R26", "R27", "R28", "R29", "R31", "R32", "R33", "R34", "R35", "R36", "R37", "R38", "R39", "R40", "R41", "R42", "R43", "R44", "R45", "R46") RUNG_R_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7") # the free sweep's set (R8/R9 are the search engine's until measured) @@ -4259,6 +4414,12 @@ def recipe_candidates(text, tu, fn, names, limit=24, rng=None, cap=40, blocks=Tr if "R43" in fam: for desc, cand in sign_test_to_mask(text, tu, fn, d_): out.append(("R43", desc, cand)) + if "R45" in fam: + for desc, cand in derived_pointer_store(text, tu, fn, d_): + out.append(("R45", desc, cand)) + if "R46" in fam: + for desc, cand in set_once_chain(text, tu, fn, d_): + out.append(("R46", desc, cand)) if "R44" in fam: for desc, cand in counter_derived_pointer(text, tu, fn, d_): out.append(("R44", desc, cand)) @@ -5519,6 +5680,44 @@ def selftest(): if sorted(m22) != ["merge-ptr a1 into a3+1"] or "a3[4] = 0x40 && a3[1];" not in m22["merge-ptr a1 into a3+1"]: fail(f"R22 must merge two walked pointers derived from one base at two offsets (K = 1): {m22!r}") + # R45, the derived-pointer store (T7 agent f5, S105; known-true: ALONE it reproduces f5's closes of func_8018F944 (4 -> 0) + # and, as the ALL form, func_8018FEA0 (16 -> 0)); R46, the set-once chain (f5; known-true: the ALL form closes func_8018FA34 + # 6 -> 0; the two singles score 2 and 4 — the agent's numbers). + DFIX = ("void func_80100000(void) {\n" + " s32 *s0;\n" + " s32 v0;\n" + " s0 = &D_800A5E88;\n" + " v0 = rand();\n" + " s0[8] = 0;\n" + " v0 &= 0x1F;\n" + " v0 -= 0x10;\n" + " s0[0] = v0;\n" + " func_80028620(2, &s0[8]);\n" + " v0 = rand();\n" + " v0 &= 0x1F;\n" + " D_800A5E90 = v0;\n" + " func_80028620(1, s0 + 4);\n" + "}") + d45 = dict(derived_pointer_store(DFIX, "src/fx/d.c", "func_80100000", + next(r for r in sc.scan_text(DFIX, "src/fx/d.c", shared_defs=None) if r["form"] == "def"))) + if sorted(d45) != ["derived-ptr s0[8] @6"]: + fail(f"R45 must derive a pointer for the stored-then-passed slot only (s0 + 4 has no store): {sorted(d45)}") + elif " s32 *p1;" not in d45["derived-ptr s0[8] @6"] or " p1 = &s0[8];\n *p1 = 0;" not in d45["derived-ptr s0[8] @6"] \ + or "func_80028620(2, p1);" not in d45["derived-ptr s0[8] @6"]: + fail(f"R45 must declare the pointer, bear it before the store, store through it and pass it: {d45!r}") + d46 = dict(set_once_chain(DFIX, "src/fx/d.c", "func_80100000", + next(r for r in sc.scan_text(DFIX, "src/fx/d.c", shared_defs=None) if r["form"] == "def"))) + if sorted(d46) != ["set-once chain ALL 2", "set-once chain v0 @13", "set-once chain v0 @9"]: + fail(f"R46 must fold each chain into its store and offer both together: {sorted(d46)}") + elif " s0[0] = ((v0 & 0x1F) - 0x10);" not in d46["set-once chain v0 @9"] or " D_800A5E90 = v0 & 0x1F;" not in d46["set-once chain ALL 2"] \ + or "v0 -= 0x10;" in d46["set-once chain v0 @9"]: + fail(f"R46 must delete the chain lines and write one expression: {d46!r}") + # a chain whose temp is read after the store is refused + DF2 = DFIX.replace(" func_80028620(2, &s0[8]);\n", " func_80028620(v0, &s0[8]);\n") + if "set-once chain v0 @9" in dict(set_once_chain(DF2, "src/fx/d.c", "func_80100000", + next(r for r in sc.scan_text(DF2, "src/fx/d.c", shared_defs=None) if r["form"] == "def"))): + fail("R46 must refuse a chain whose temp is read after the store") + # R26, the address alias (T7 agent c45, S103; known-true: on func_80183E3C's start text R26's "second" candidate # scores 0 from 38 — the agent's close, reproduced by the generator alone) AAF = ("void func_80100000(void) {\n" diff --git a/tools/delever_search.py b/tools/delever_search.py index 35d21d31e..4aefc2150 100644 --- a/tools/delever_search.py +++ b/tools/delever_search.py @@ -88,17 +88,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": ("R19", "R25", "R26", "R23", "R24", "R21", "R6", "R20", "R16", "R8", "R15", "R17", "R5", "R18", "R10", "R12", "R14", "R13", "R3", "R7", "R9", "R2", "R4", "R22", "R44"), + "REG-caller": ("R19", "R25", "R26", "R23", "R24", "R21", "R6", "R20", "R16", "R8", "R15", "R17", "R5", "R18", "R10", "R12", "R14", "R13", "R3", "R7", "R9", "R2", "R4", "R22", "R44", "R45", "R46"), # the s-bank order is global.c's allocno_compare (ref weight x live length), declaration order only on an exact tie - "REG-callee": ("R19", "R25", "R26", "R23", "R24", "R21", "R20", "R2", "R4", "R3", "R6", "R16", "R8", "R15", "R18", "R12", "R7", "R9", "R17", "R10", "R14", "R13", "R5", "R22", "R44"), - "REG-mixed": ("R19", "R25", "R26", "R23", "R24", "R21", "R6", "R20", "R16", "R2", "R15", "R17", "R18", "R5", "R10", "R4", "R3", "R8", "R12", "R13", "R14", "R7", "R9", "R22", "R44"), + "REG-callee": ("R19", "R25", "R26", "R23", "R24", "R21", "R20", "R2", "R4", "R3", "R6", "R16", "R8", "R15", "R18", "R12", "R7", "R9", "R17", "R10", "R14", "R13", "R5", "R22", "R44", "R45", "R46"), + "REG-mixed": ("R19", "R25", "R26", "R23", "R24", "R21", "R6", "R20", "R16", "R2", "R15", "R17", "R18", "R5", "R10", "R4", "R3", "R8", "R12", "R13", "R14", "R7", "R9", "R22", "R44", "R45", "R46"), # 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": ("R19", "R25", "R26", "R22", "R44", "R21", "R20", "R12", "R16", "R14", "R15", "R6", "R8", "R3", "R18", "R7", "R17", "R5", "R13", "R9", "R10", "R2", "R4", "R23", "R24"), + "COUNT": ("R19", "R25", "R26", "R22", "R44", "R45", "R21", "R20", "R12", "R16", "R14", "R15", "R6", "R8", "R3", "R18", "R7", "R17", "R5", "R13", "R9", "R10", "R2", "R4", "R23", "R24", "R46"), # statement order IS the schedule among equal-priority insns (rank_for_schedule's LUID tie-break); do-while is a barrier # 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": ("R19", "R25", "R17", "R18", "R20", "R21", "R9", "R7", "R13", "R3", "R16", "R6", "R8", "R5", "R12", "R14", "R10", "R15", "R2", "R4", "R22", "R23", "R24", "R26", "R44"), + "ORDER": ("R19", "R25", "R46", "R17", "R18", "R20", "R21", "R9", "R7", "R13", "R3", "R16", "R6", "R8", "R5", "R12", "R14", "R10", "R15", "R2", "R4", "R22", "R23", "R24", "R26", "R44", "R45"), "MIXED": dl.ALL_FAMILIES, "OTHER": dl.ALL_FAMILIES, }