diff --git a/docs/SETUP.md b/docs/SETUP.md index 67e096f7a..c090aaad2 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). + [≤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 `(?])%s\s*(?:=(?!=)|\+=|-=|\+\+|--)|(?:\+\+|--)\s*%s\b" % (n, n)) + return [i for i in range(lo, hi) if a.search(masked[i])] + + def block_of(si): + """(open-brace line, close-brace line) of the innermost `{ … }` holding line si, or None.""" + depth = 0 + ob = None + for i in range(si - 1, lo - 1, -1): + depth += masked[i].count("}") - masked[i].count("{") + if depth < 0: + ob = i + break + if ob is None: + return None + depth = 0 + for i in range(ob, hi): + depth += masked[i].count("{") - masked[i].count("}") + if depth <= 0: + return ob, i + return None + + out = [] + for p, (pi, pt, pinit) in ptrs.items(): + pn = re.escape(p) + if re.search(r"(?])%s\b" % pn, sc.mask_text(base)): + continue # p = p-derived: not a base + blk = block_of(si) + if blk is None or ii >= blk[0]: + continue + ob, cb = blk + if not re.search(r"\b(for|while|do)\b", masked[ob]): + continue + # the counters of that block: stepped by +1 exactly once inside it (or in the `for` header), never assigned inside + inner = range(ob + 1, cb) + cands = {} + for i in list(inner) + [ob]: + for m in re.finditer(r"(?])([A-Za-z_]\w*)\s*(?:\+\+|\+=\s*1\b)|\+\+\s*([A-Za-z_]\w*)\b", masked[i]): + n = m.group(1) or m.group(2) + if n == p or n in ptrs: + continue + cands.setdefault(n, []).append(i) + for cn, where in cands.items(): + if len(where) != 1: + continue + cnn = re.escape(cn) + if any(re.search(r"(?])%s\s*(?:=(?!=)|-=|--)|--\s*%s\b" % (cnn, cnn), masked[i]) for i in inner): + continue + # the counter's start value: the last `cn = C;` before the loop (or in the for header) + C = None + for i in range(ob, lo - 1, -1): + m = re.search(r"(?])%s\s*=\s*(-?%s)\s*[;,)]" % (cnn, _INT), masked[i]) + if m: + C = int(m.group(1), 0) + break + if i != ob and re.search(r"(?])%s\s*(?:=(?!=)|\+=|-=|\+\+|--)" % cnn, masked[i]): + break + if C is None: + continue + idx = cn if C == 0 else f"({cn} - {C})" + hexlike = "0x" in masked[si] + ks = (hex(abs(K)) if hexlike else str(abs(K))) + if K < 0: + idx = f"-{cn}" if C == 0 else f"({C} - {cn})" + b, cast_stripped = base, False + if b.startswith(f"({pt} *)"): + b, cast_stripped = b[len(f"({pt} *)"):].strip(), True + spellings = [f"{p} = ({pt} *)({b}) + {idx} * {ks};"] + m = re.match(r"^&?\s*([A-Za-z_]\w*)\s*(?:\[\s*(%s)\s*\])?$" % _INT, b) + if m and not cast_stripped: + c0 = int(m.group(2), 0) if m.group(2) else 0 + spellings.append(f"{p} = &{m.group(1)}[{(hex(c0) + ' + ') if c0 else ''}{idx} * {ks}];") + indent = re.match(r"^\s*", lines[si]).group(0) + for k, sp in enumerate(spellings): + cand = list(lines) + if pinit is not None and ii == pi: + cand[pi] = lines[pi][:lines[pi].index("=")].rstrip() + ";" + else: + cand[ii] = None + cand[si] = None + cand.insert(ob + 1, indent + sp) + out.append((f"counter-ptr {p} by {cn}{' array' if k else ''} @{ob + 1}", + "\n".join(l for l in cand if l is not None))) + return out + + 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] @@ -3928,7 +4092,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") +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") RUNG_R_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7") # the free sweep's set (R8/R9 are the search engine's until measured) @@ -4093,6 +4257,9 @@ 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 "R44" in fam: + for desc, cand in counter_derived_pointer(text, tu, fn, d_): + out.append(("R44", desc, cand)) if "R42" in fam: for desc, cand in move_statement_far(text, tu, fn, d_): out.append(("R42", desc, cand)) @@ -5289,6 +5456,67 @@ def selftest(): fail(f"R25 must re-issue the call at the real arity: {t25!r}") _defs.pop("func_8FFFFFF0", None) + # R44, the counter-derived pointer (T7 agents e21 S104 + f2 S105; known-true: alone it reproduces e21's close of + # func_80037EA0 at 0 from 33, and on f2's func_80038838 start text scores the agent's "loop 2 alone" 18; composed with + # the extended R22 it reaches f2's 0). The fixture: a do-while with a counter and a walked pointer, an `&&` test on the + # pointer (the S103–S105 R22 blind spot: `&& p` read as `&p` and refused every such body), a down-counter control. + CFIX = ("void func_80100000(void *arg0) {\n" + " u8 *p;\n" + " u8 *q;\n" + " s32 i;\n" + " s32 n;\n" + " i = 0;\n" + " p = D_800C6E2E;\n" + " do {\n" + " if (p[-4] != 0 && p[0] != 0) {\n" + " p[0] = 0;\n" + " }\n" + " i++;\n" + " p += 0x60;\n" + " } while (i < 0x10);\n" + " n = 3;\n" + " q = (u8 *)arg0;\n" + " do {\n" + " q[0x18] = 0;\n" + " q += 8;\n" + " n--;\n" + " } while (n > 0);\n" + "}") + c44 = dict(counter_derived_pointer(CFIX, "src/fx/c.c", "func_80100000", + next(r for r in sc.scan_text(CFIX, "src/fx/c.c", shared_defs=None) if r["form"] == "def"))) + if sorted(c44) != ["counter-ptr p by i @8", "counter-ptr p by i array @8"]: + fail(f"R44 must re-derive the up-counted walk (both spellings) and refuse the down-counted one, got {sorted(c44)}") + elif " p = (u8 *)(D_800C6E2E) + i * 0x60;" not in c44["counter-ptr p by i @8"] or \ + " p = &D_800C6E2E[i * 0x60];" not in c44["counter-ptr p by i array @8"] or \ + "p += 0x60" in c44["counter-ptr p by i @8"] or " p = D_800C6E2E;" in c44["counter-ptr p by i @8"]: + fail(f"R44 must insert the derivation at the loop top and delete the walk and the initialiser: {c44!r}") + # the counter starting at 2: the derivation subtracts it + CF2 = CFIX.replace(" i = 0;\n", " i = 2;\n") + c44b = dict(counter_derived_pointer(CF2, "src/fx/c.c", "func_80100000", + next(r for r in sc.scan_text(CF2, "src/fx/c.c", shared_defs=None) if r["form"] == "def"))) + if "p = (u8 *)(D_800C6E2E) + (i - 2) * 0x60;" not in c44b.get("counter-ptr p by i @8", ""): + fail(f"R44 must offset a counter that does not start at 0: {c44b!r}") + # R22 extended (S105 f2): two pointers derived from ONE base expression at two offsets merge like `q = p + K` + MFIX = ("void func_80100000(void *arg0) {\n" + " u8 *a3;\n" + " u8 *a1;\n" + " s32 i;\n" + " a3 = (u8 *)arg0 + 0x1A;\n" + " i = 0;\n" + " a1 = (u8 *)arg0 + 0x1B;\n" + " do {\n" + " a3[0] = i;\n" + " a1[3] = 0x40 && a3[1];\n" + " i++;\n" + " a1 += 0x1A;\n" + " a3 += 0x1A;\n" + " } while (i < 0x10);\n" + "}") + m22 = dict(merge_walked_pointers(MFIX, "src/fx/m.c", "func_80100000", + next(r for r in sc.scan_text(MFIX, "src/fx/m.c", shared_defs=None) if r["form"] == "def"))) + 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}") + # 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 fd7455695..35d21d31e 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"), + "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"), # 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"), - "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"), + "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"), # 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", "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", "R21", "R20", "R12", "R16", "R14", "R15", "R6", "R8", "R3", "R18", "R7", "R17", "R5", "R13", "R9", "R10", "R2", "R4", "R23", "R24"), # 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"), + "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"), "MIXED": dl.ALL_FAMILIES, "OTHER": dl.ALL_FAMILIES, }