From 975850ff84e25438a96cd5b8a32f9247498bc653 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:27:20 -0600 Subject: [PATCH] =?UTF-8?q?phase-36:=20T7=20toolify=20a2=20=E2=80=94=20gen?= =?UTF-8?q?erators=20R16=20(constant=20holder=20inlined)=20and=20R17=20(co?= =?UTF-8?q?nstant-run=20split),=20the=20directed=20form=20of=20a=20move=20?= =?UTF-8?q?R9=20reached=20only=20by=20luck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- decomp-architect/corpus/tools/P10/delever.py | 174 +++++++++++++++++- .../corpus/tools/P10/delever_search.py | 25 ++- docs/SETUP.md | 2 +- tools/delever.py | 158 +++++++++++++++- tools/delever_search.py | 25 ++- 5 files changed, 359 insertions(+), 25 deletions(-) diff --git a/decomp-architect/corpus/tools/P10/delever.py b/decomp-architect/corpus/tools/P10/delever.py index f0742e2d4..1b046c019 100644 --- a/decomp-architect/corpus/tools/P10/delever.py +++ b/decomp-architect/corpus/tools/P10/delever.py @@ -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])|(?])%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"(?])%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") diff --git a/decomp-architect/corpus/tools/P10/delever_search.py b/decomp-architect/corpus/tools/P10/delever_search.py index 4971dc03c..3527ded3d 100644 --- a/decomp-architect/corpus/tools/P10/delever_search.py +++ b/decomp-architect/corpus/tools/P10/delever_search.py @@ -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": diff --git a/docs/SETUP.md b/docs/SETUP.md index 4ad08f810..d35d13020 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -791,7 +791,7 @@ Every script under `tools/` (plus the two report make-targets), grouped by purpo | | `tools/verbatim_target_s.py --gas` (S99 amendment) | The gas listing is now VERIFIED before it is emitted: it is assembled, disassembled and compared word by word with the ROM image, every instruction that does not reproduce its word is replaced by `.word 0x…` with the mnemonic kept in the comment, and a listing that still disagrees is REFUSED. The class this catches: objdump prints the pseudo-instruction `move` for `addu rX,rY,$zero` and gas assembles `move` as `or` — 24 wrong words in one 234-instruction function, silently. Words carrying a relocation (jal/j, HI16/LO16) are excluded, since the linker fills those. NOTE for any consumer: the listing's %hi/%lo pairs are RESOLVED (no relocation), so an object assembled from it cannot be compared reloc-for-reloc against a compiled candidate — that is why the permuter's target is now the compiled body, not this file (see `tools/delever_permute.py`). | | | `tools/lever_progress.py` | **(P36, the record)** The lever series behind `docs/levers.md`: `--snapshot ""` appends one milestone row (the census's totals by class + the tree's HEAD) to `docs/lever-progress.tsv` and re-renders the document's generated block; `--render` rebuilds that block (the campaign half is derived from the de-lever ledger every time, scored as state TRANSITIONS so the rung that finishes a body gets the credit, not only the rung that first judged it); `--check` refuses a series whose last row is not this tree (a stale series is a wrong chart). Run it at the close of every task that changes the count, next to `lever_census --check`. | | | `tools/delever_permute.py` | **(P36 T6, rung D)** The permuter on the residue: `--plan` lists one exemplar per RESIDUE text class of the delever ledger (copies desc, then fewest NEEDED sites — a match banks every copy); `--prepare TU FN` builds that exemplar's scratch `.run/P36/permuter/__/` (R48: a function NAME repeats across overlapping overlays) — `tu.c` the lever-free TU (delever's own rung-A rewrite of every REMOVABLE site; a REFUSED site makes the exemplar UNSTRIPPABLE), `iso.c` the same with every OTHER definition reduced to a prototype, shared-header includes replaced by the prototypes they define, `INCLUDE_ASM`/`INCLUDE_RODATA` and file-scope asm statements dropped, `draft.c` that through `cpp -P` with the Makefile's own CPPFLAGS + `-I` + `-D__attribute__(x)=` (pycparser rejects `__attribute__` and decomp-permuter then REFUSES base.c and permutes nothing), `levered.c` the same pipeline with the levers KEPT, and the target in both forms — `gas/.s` (`verbatim_target_s --gas`, the only assemblable one: `mipsel-as` refuses the splat listing's bare `addiu sp,sp,-152` exactly as decomp.me did, R98) for `p16_permute.setup`, `splat/.s` for `match_one`. `--positive-control TU FN` perturbs a MATCHING body by one commutative swap and requires the permuter to find its way back to score 0 — the control that tells a hard population from a broken scorer (S99: two campaigns returned 0 of 16 against a target assembled from a listing, whose base score for the tree's own body was 28, not 0). `--calibrate [--limit N]` and every `--run` attempt: the LEVERED body must be `match_one` MATCH against the regenerated target (R39/R56 — the harness must agree with the tree before it may judge a candidate; 12 of 12 at S99), then the lever-free body's distance is recorded as the search's starting point (min 8 / median 78 / max 276 over those 12; a removed hand-placed `instruction` changes the instruction COUNT and shifts everything after it — `--max-start N` triages those as FAR with their number). `--run [--limit K] [--workers W] [--secs S] [--cycles C] [-j J]` runs `permuter_ils.py` per exemplar (profile from the NEEDED kinds: pins → regalloc, barriers/launders/keep-alives → schedule), `--winners`/`--pd` pointed at the scratch; every attempt appended to `.run/P36/permuter/outcomes.jsonl` (also the skip list; `--include-done` redraws). `--bank [--label dN]` puts each winner's definition back through `delever --apply-body … --rung D` (which refuses a body still carrying a class A/B lever and judges the real object through every recipe) and then `gte_consolidate --apply --rejudge` to re-fold the cpp-expanded GTE asm; serial, because each step runs `make`. Verdicts distinguish MATCH / NO-MATCH / FAR / UNSTRIPPABLE / UNCALIBRATED / SETUP-FAILED / ABORTED / NOT-JUDGED (R61: a run that never iterated is not a no-match). Run the campaign DETACHED (`setsid nohup … &` + a `Monitor`), never as a harness background task. | -| | `tools/delever_search.py` | **(P36 S101, rung G — the guided search)** Rung R tests ~57 one-move candidates per body for IDENTICAL and learns nothing from a miss (0 of 300 where no shape was known, §454a); rung D discovers but reprints the source. This engine keeps rung R's generators (`delever.recipe_candidates`, now with a `families` filter and two new moves: R8 a temp introduced/hoisted — R6's inverse — and R9 two adjacent statements swapped; R7 gained its own inverse, the unwrap) and the per-TU oracle, and SCORES every candidate: the function's instructions are read from the scratch object (`objdump -drz`, 35 ms on the largest object) and compared with the same function in the fleet run's baseline object under `build/` — the tree's own bytes, carrying the candidates' relocations by construction, so no listing is assembled and nothing is isolated (the two instrument classes of §454). The score is an EDIT DISTANCE over the reloc-masked words (a positional count read one inlined temp as 43 shifted words; difflib reads the real delta), the residual is classified from the diff blocks (REG on the caller- or callee-saved bank from the register pairs, COUNT, ORDER, MIXED) and the class picks the move families, ranked round-robin (a strict family order starved the inverse of a one-move perturbation behind 64 block wraps). The search is a beam (`--beam 3 --depth 3 --cap 48 --budget 400`): a child worse than its parent is dropped, the first score-0 is verified on every recipe of the file and banked through `delever --apply-body --rung G`, siblings by `--propagate` serially after the parallel phase. `--plan [--score]` lists the residue's exemplars (largest class first) with their starting distance and class; `--run [--limit K] [-j W] [--label gN]`; `--positive-control TU FN [--moves 1\|2]` perturbs a matching body by generator moves and requires the search to return to 0 without writing the tree (S101: one move back in 23 compiles; inline+wrap back in 74 by hoist+unwrap; an inlined pointer temp under a dereference has no inverse generator yet and stalls at 10 — the measurable stall mode); `--explain TU FN [--path "m1|m2"]` reads one body's residual as mnemonic blocks (mine vs the target) after any move path — the instrument that turns a stall into a generator (S101: the two 6-distance bodies are one surviving copy; a 7 is an association order plus a negation named once; a 40 is a duplicated call tail); `--selftest` (the classifier on synthetic streams, the beam on a stub needing three composed moves, the generators on fixtures). The registry's later moves, each from lane B's map of the compiler source and each with its selftest: R10 a parameter routed through a body-local copy and the reverse (map 1a-9), R12 a local's scalar width (1c-1 — verified NOT to reach a copy whose value's known bits fit the narrow mode), R13 two terms of a `+`/`-` chain exchanged, R8's third form a repeated RHS named once; a constant-operand R5 swap is never generated (fold moves it right — verified on bytes, `.run/P36/engine/micro/`), and the `do { } while (0)` lever works through the ref weight (`.run/P36/engine/micro/dowhile/`: `.lreg` `used 5` → `used 6 times`, `$18` → `$17`). Second round (S101, from `--explain` on the thirteen small residuals g3 left unmoved): R12 now covers `u8` and the `short`/`int`/`char` spellings; **R14** a PARAMETER's declared width in the header (a `short` parameter is sign-extended in place, `sra a1,a1,16; move s4,a1`, where a cast at the use extends into the destination); R8's fourth form one address local shared by every dereference of one base, stores included (lane B 2-6 and 2-12: a store through a bare pointer flushes cse's memory table and a global is re-loaded); R10's alias form through casts (`src = (u8 *)((u32)param_2)`). The bank and the propagation run IN PROCESS (`delever.apply_body_core`, `delever.propagate` returning `(banked, n, refused)`): run g3's 1,503 siblings had cost ≈40 min as a subprocess each, and a `delever.py` edit during a run could break a bank mid-run — now the running process holds its own copy. Measured runs: g1 1/16 (0.12 h), g2 1/16 (0.32 h, the wider beam), **g3 13/64 (1.02 h, 1,516 bodies; by first family R12 ×5, R7 ×3, R9 ×3, R10 ×2 — lane B's width and parameter moves half the closes)**. Fourth round: R14 rewrites the TU's prototypes with the header (else every candidate is a conflicting-types error); R8's named-once form also names a repeated depth-0 operand or parenthesised group; R12 splits a multi-declarator line to widen one name (`MULTI_DECL` also ends neither the declaration run nor is offered as a statement). Draw rules: a body whose NEEDED sites are all class C/D (a byte-needed `volatile` cast, a bare `register`) is DONE by decision 3 and is not drawn, and the seed keeps such sites; the scorer's scratch object is keyed by the TU (a per-tag name let two workers share one file — a byte-identical body read as 14,871 mismatches). g4s 3/38 (the thirteen explained names across the fleet, 133 bodies). Every attempt in `.run/P36/engine/outcomes.jsonl`, every scored candidate in `.run/P36/engine/trace/__.jsonl` — the byte record lane B's compiler-source hypotheses are checked against. A worker owns a TU; headers serial; `--dirty-ok` for a run that banks several bodies before one commit; run a campaign DETACHED with `nice`. `delever.py --repair-nhash` filled the 301 rung-R RESIDUE rows that carried no text hash (S99/S100 sweeps) from their bodies' earlier rows — the cause fixed in `recipes()`. **R15, the sink (P36 S102, the first T7 agent's crack toolified):** the statement AFTER an if/else chain pushed into every arm and the variables it consumed deleted — `if (c) { v = e1; } else { v = e2; } w = f(v);` becomes `if (c) { w = f(e1); } else { w = f(e2); }`. It is a REGISTER move, not a scheduling one: a value set in every arm and read after the merge is a CROSS-BLOCK pseudo local-alloc never gives a quantity to (`local-alloc.c:472`, `next_qty` reset per block at `:517`), so each arm holds two quantities and takes `block_alloc`'s unrolled `case 2` (`:1499-1502`, one `qty_compare` `:1578-1596`, higher density first); sinking makes it a third block-local quantity and `case 3` (`:1491-1496`) FALLS THROUGH into `case 2`, applying that comparison a second time and undoing its own exchange, so the two caller-saved colours swap — and while it is a global allocno it can inherit a copy preference from whatever the merge result is passed to (`set_preference` `global.c:1535+`, merged by `expand_preferences` `global.c:781-825`, overriding first-fit at `:1034-1067`), which sinking removes with it. Applicability is CHECKED: every consumed variable must be assigned exactly once in every arm by a simple statement, appear in the merge statement, and occur nowhere else in the function. `if_chains()` walks the arms counting a line's CLOSING braces before its opening ones — on a `} else if (…) {` line the two net to zero and a naive depth counter never closes the arm (the defect the generator's first run had). Ranked third in REG-caller and REG-mixed, third in COUNT; three selftest controls (a variable read after the merge, a variable one arm does not set, the brace walk's three arms). Its known-true check: run on `func_80156044`'s own pre-bank text it reproduces the agent's crack and scores 0. | +| | `tools/delever_search.py` | **(P36 S101, rung G — the guided search)** Rung R tests ~57 one-move candidates per body for IDENTICAL and learns nothing from a miss (0 of 300 where no shape was known, §454a); rung D discovers but reprints the source. This engine keeps rung R's generators (`delever.recipe_candidates`, now with a `families` filter and two new moves: R8 a temp introduced/hoisted — R6's inverse — and R9 two adjacent statements swapped; R7 gained its own inverse, the unwrap) and the per-TU oracle, and SCORES every candidate: the function's instructions are read from the scratch object (`objdump -drz`, 35 ms on the largest object) and compared with the same function in the fleet run's baseline object under `build/` — the tree's own bytes, carrying the candidates' relocations by construction, so no listing is assembled and nothing is isolated (the two instrument classes of §454). The score is an EDIT DISTANCE over the reloc-masked words (a positional count read one inlined temp as 43 shifted words; difflib reads the real delta), the residual is classified from the diff blocks (REG on the caller- or callee-saved bank from the register pairs, COUNT, ORDER, MIXED) and the class picks the move families, ranked round-robin (a strict family order starved the inverse of a one-move perturbation behind 64 block wraps). The search is a beam (`--beam 3 --depth 3 --cap 48 --budget 400`): a child worse than its parent is dropped, the first score-0 is verified on every recipe of the file and banked through `delever --apply-body --rung G`, siblings by `--propagate` serially after the parallel phase. `--plan [--score]` lists the residue's exemplars (largest class first) with their starting distance and class; `--run [--limit K] [-j W] [--label gN]`; `--positive-control TU FN [--moves 1\|2]` perturbs a matching body by generator moves and requires the search to return to 0 without writing the tree (S101: one move back in 23 compiles; inline+wrap back in 74 by hoist+unwrap; an inlined pointer temp under a dereference has no inverse generator yet and stalls at 10 — the measurable stall mode); `--explain TU FN [--path "m1|m2"]` reads one body's residual as mnemonic blocks (mine vs the target) after any move path — the instrument that turns a stall into a generator (S101: the two 6-distance bodies are one surviving copy; a 7 is an association order plus a negation named once; a 40 is a duplicated call tail); `--selftest` (the classifier on synthetic streams, the beam on a stub needing three composed moves, the generators on fixtures). The registry's later moves, each from lane B's map of the compiler source and each with its selftest: R10 a parameter routed through a body-local copy and the reverse (map 1a-9), R12 a local's scalar width (1c-1 — verified NOT to reach a copy whose value's known bits fit the narrow mode), R13 two terms of a `+`/`-` chain exchanged, R8's third form a repeated RHS named once; a constant-operand R5 swap is never generated (fold moves it right — verified on bytes, `.run/P36/engine/micro/`), and the `do { } while (0)` lever works through the ref weight (`.run/P36/engine/micro/dowhile/`: `.lreg` `used 5` → `used 6 times`, `$18` → `$17`). Second round (S101, from `--explain` on the thirteen small residuals g3 left unmoved): R12 now covers `u8` and the `short`/`int`/`char` spellings; **R14** a PARAMETER's declared width in the header (a `short` parameter is sign-extended in place, `sra a1,a1,16; move s4,a1`, where a cast at the use extends into the destination); R8's fourth form one address local shared by every dereference of one base, stores included (lane B 2-6 and 2-12: a store through a bare pointer flushes cse's memory table and a global is re-loaded); R10's alias form through casts (`src = (u8 *)((u32)param_2)`). The bank and the propagation run IN PROCESS (`delever.apply_body_core`, `delever.propagate` returning `(banked, n, refused)`): run g3's 1,503 siblings had cost ≈40 min as a subprocess each, and a `delever.py` edit during a run could break a bank mid-run — now the running process holds its own copy. Measured runs: g1 1/16 (0.12 h), g2 1/16 (0.32 h, the wider beam), **g3 13/64 (1.02 h, 1,516 bodies; by first family R12 ×5, R7 ×3, R9 ×3, R10 ×2 — lane B's width and parameter moves half the closes)**. Fourth round: R14 rewrites the TU's prototypes with the header (else every candidate is a conflicting-types error); R8's named-once form also names a repeated depth-0 operand or parenthesised group; R12 splits a multi-declarator line to widen one name (`MULTI_DECL` also ends neither the declaration run nor is offered as a statement). Draw rules: a body whose NEEDED sites are all class C/D (a byte-needed `volatile` cast, a bare `register`) is DONE by decision 3 and is not drawn, and the seed keeps such sites; the scorer's scratch object is keyed by the TU (a per-tag name let two workers share one file — a byte-identical body read as 14,871 mismatches). g4s 3/38 (the thirteen explained names across the fleet, 133 bodies). Every attempt in `.run/P36/engine/outcomes.jsonl`, every scored candidate in `.run/P36/engine/trace/__.jsonl` — the byte record lane B's compiler-source hypotheses are checked against. A worker owns a TU; headers serial; `--dirty-ok` for a run that banks several bodies before one commit; run a campaign DETACHED with `nice`. `delever.py --repair-nhash` filled the 301 rung-R RESIDUE rows that carried no text hash (S99/S100 sweeps) from their bodies' earlier rows — the cause fixed in `recipes()`. **R15, the sink (P36 S102, the first T7 agent's crack toolified):** the statement AFTER an if/else chain pushed into every arm and the variables it consumed deleted — `if (c) { v = e1; } else { v = e2; } w = f(v);` becomes `if (c) { w = f(e1); } else { w = f(e2); }`. It is a REGISTER move, not a scheduling one: a value set in every arm and read after the merge is a CROSS-BLOCK pseudo local-alloc never gives a quantity to (`local-alloc.c:472`, `next_qty` reset per block at `:517`), so each arm holds two quantities and takes `block_alloc`'s unrolled `case 2` (`:1499-1502`, one `qty_compare` `:1578-1596`, higher density first); sinking makes it a third block-local quantity and `case 3` (`:1491-1496`) FALLS THROUGH into `case 2`, applying that comparison a second time and undoing its own exchange, so the two caller-saved colours swap — and while it is a global allocno it can inherit a copy preference from whatever the merge result is passed to (`set_preference` `global.c:1535+`, merged by `expand_preferences` `global.c:781-825`, overriding first-fit at `:1034-1067`), which sinking removes with it. Applicability is CHECKED: every consumed variable must be assigned exactly once in every arm by a simple statement, appear in the merge statement, and occur nowhere else in the function. `if_chains()` walks the arms counting a line's CLOSING braces before its opening ones — on a `} else if (…) {` line the two net to zero and a naive depth counter never closes the arm (the defect the generator's first run had). Ranked third in REG-caller and REG-mixed, third in COUNT; three selftest controls (a variable read after the merge, a variable one arm does not set, the brace walk's three arms). Its known-true check: run on `func_80156044`'s own pre-bank text it reproduces the agent's crack and scores 0. **R16 the constant holder and R17 the constant-run split (P36 S102, the second T7 agent's crack toolified):** 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 the whole family with it; deleting it is byte-neutral by itself but removes a quantity from the block, which is what lets the next move reach the allocator. R17 splits a run of consecutive statements assigning the same integer literal by moving the nearest differently-valued literal assignment into it, at each interior split point: the decision is `find_free_reg`'s live-range scan (`local-alloc.c:2109-2110`) — while the two constants' ranges are disjoint they share one caller-saved register, and splitting the run makes the first live across the second so it takes another colour; the discriminator in the dumps is the `.lreg` line `Register N used K times across M insns`, whose M grows when the split lands. R17 is the DIRECTED form of a move R9 contains but reaches by luck (agent a2 measured 2,271 compiles for R9 in `func_80168828`; **R16 + R17 reproduce that close in ten**, the known-true check, on the seed that keeps the body's semantically-forced `$4` pin). Ranked: R16 second in REG-caller/REG-mixed and second in COUNT, R17 FIRST in ORDER. The engine selftest's caller-saved assertion is now the ORDERING INVARIANT — every targeted lever (R5, R15, R16) before every blind family (R9, R2, R4) — instead of a fixed window widened once per generator. `--propagate` inherits the exemplar's lever allowance, derived from the count of surviving `// !FAKE:` markers in its own banked text, and refuses by name any sibling whose remap would carry more (agent a2's reshape keeps one pin: 124 of 124 siblings banked once this existed). | | | `tools/delever_search.py --try TU FN FILE [--body]` + `tools/delever_pack.py` | **(P36 S101, T7 one agent at a time — Drew: efficiency, not wall-clock; hone the method after each lands.)** `--try` scores a candidate translation-unit text (or, with `--body`, a function body spliced into the tree's TU) WITHOUT writing the tree: the TU's own recipe is run on a scratch copy (`-I` so its relative includes resolve; a header candidate through a shadowing include dir) and the function's instructions are compared with the fleet run's baseline object — the same score, class and mnemonic diff as `--explain`. Proven on a known-true case (the tree's text 0; one pin removed the known residual). It is the loop an agent runs on its own candidate, so any number may run beside a campaign; the bank stays the coordinator's (`delever.apply_body_core` on the real recipe, then `--propagate`). `delever_pack.py --build [--min-copies 100] [--limit N]` writes one pack per residue exemplar under `.run/P36/agents/__/` (tu.txt, body_tree.c, body_free.c = the seed, residual.txt from `--try`, sites.txt, history.txt = every engine attempt and the best-scoring moves of the last trace) and `ORDER.tsv` (best distance reached ascending, then copies); `PROMPT.md` beside them is the brief (read the residual → name the pass from `tools/reference/gcc-2.7.2/` + the map → test on bytes with `--try` → deliverables early: `body.c` + `mechanism.md` with a GENERATOR PROPOSAL). `delever --restore` now refuses loudly on an empty or torn inflight.json (a kill mid-write) and says how to reconcile (the oracle: a bank is IDENTICAL, a leftover candidate DIFFERS). | | | `tools/kit_coverage.py` | **(P33.5 task 14.5)** The kit's DISTILLATION coverage: derives the rule population (every `- **R` of DIGEST §3, asserted contiguous) and the hindsight population (every `## ` heading of `docs/accelerators.md` at numbered-item granularity — 58 at S92) and asserts each is cited by a `provenance:` line of the registry seed / the kernels OR dispositioned in `config/kit_coverage_map.tsv` (`G` / `DK-` / `FOLDED:G` / `ENV` / `PA` / `SEED:` / `KIT:` / `RECORD` / `COOKBOOK` / `NOT-PORTABLE`; unknown ids refused, R43); counts with denominators (R41); rc 1 on any gap. In `tools-health` after `tool_census --check`. Its first run found 26 uncited rules and 21 uncited entries → three new kernels (DK-66–DK-68) and 41 authored dispositions. | | | `decomp-architect/` (the day-one decomp kit) | **(P33.5 tasks 9–14)** The package a new matching-decomp project installs as Phase 0.5 on ProjectArchitect 2.0: `README.md` (the three steps), `intake.decomp.md` (ProjectArchitect's twelve items pre-answered + the phase ladder + the six readability inversions), `SETUP.md` (the installer, Step 0 contract … Step 10 verify + hard stop; `answers: ` for unattended runs), `decomp-architect.md` (the methodology), `templates/` (the firewall pack — `gitignore.decomp`, `firewall.txt`, `audit_public.template.py`, the planted fixture, `no-rom.template.yml` —, the READMEs, `pa-overlays.md`, `registry-E.decomp.md` G1–G67, the skeletons, `PLACEHOLDERS.md`, `layout-contract.md`), `corpus/decomp-kernels.md` (DK-1 … DK-80), the three dictionaries `corpus/tools//` + `corpus/cookbook/` + `corpus/record/` (generated by `make kit-corpus`), `memory-seed/` (18), `tools/MANIFEST.md` (generated). **How it is checked:** `tools/kit_lint.py` (de-specialisation, placeholders, syntax, the gitignore-template diff) + `tools/tool_census.py --check` (the corpora equal their sources) + `tools/gitignore_template_check.py`, all in `tools-health`; the dry-run harness under `.run/P33.5/kit-dryrun/` (`answers.md`, `expected-manifest.txt`, `judge.py`, the install logs and verdicts of runs 1–5 — a kit change is re-verified by a resume on the last throwaway `repo/`, a fresh full run only when SETUP's steps change; the judge compares the real tree's dirty PATH SETS before/after). Wiki page: `docs/wiki/Start-a-new-decomp-project.md`. Split into its own repository after the flip. | diff --git a/tools/delever.py b/tools/delever.py index a5b656046..1b046c019 100644 --- a/tools/delever.py +++ b/tools/delever.py @@ -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])|(?])%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"(?])%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") diff --git a/tools/delever_search.py b/tools/delever_search.py index 4971dc03c..3527ded3d 100644 --- a/tools/delever_search.py +++ b/tools/delever_search.py @@ -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":