From 861dd0651cfdccf3882dd0c51bfa0787d8edec93 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:45:15 -0600 Subject: [PATCH] =?UTF-8?q?phase-36:=20T7=20toolify=20a1=20=E2=80=94=20gen?= =?UTF-8?q?erator=20R15,=20the=20sink=20(agent=20a1's=20crack=20made=20mec?= =?UTF-8?q?hanical;=20reproduces=20it=20from=20the=20pre-bank=20text=20at?= =?UTF-8?q?=20score=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The harvest half of the one-at-a-time loop (R16): agent a1's reading of func_80156044 is now a move the engine can make on any body, so the remaining head classes get it for free. - R15 sinks the statement AFTER an if/else chain into every arm and deletes the variables it consumed: `if (c) { v = e1; } else { v = e2; } w = f(v);` -> `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 (local-alloc.c:472, next_qty reset at :517), so the arm holds two quantities and takes block_alloc's unrolled case 2 (:1499-1502, qty_compare :1578-1596). Sinking makes it a third block-local quantity, and case 3 (:1491-1496) falls through into case 2 and applies that comparison a second time, undoing its own exchange — the two caller-saved colours swap. It also takes the value out of global.c, where set_preference (global.c:1535+) had given it a copy preference through the merge result's argument copy. - applicability is checked, never assumed: each 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() counts a line's CLOSING braces before its opening ones. On a `} else if (...) {` line the two net to zero and the first version's depth counter never closed the arm — the generator found 0 candidates on the very body it was written from. Caught by running it on that known-true case before believing it. - ranked third in REG-caller / REG-mixed / COUNT; the engine selftest's "R5 in the first three" assertion widened to "R5 and R15 in the first four" rather than de-ranking the new move. - verified: delever --selftest OK (3 new controls: a variable read after the merge, a variable one arm does not set, the brace walk's three arms); delever_search --selftest OK; and the known-true check — R15 run on func_80156044's pre-bank text emits the agent's crack and `--try` scores it `score 0 (OTHER; mine 74 ins, target 74) — MATCH`. - SETUP row rewritten (R21), kit corpus regenerated, tool_census --check OK (371 copies + 30 pointers, 0 gaps). - no src/ change in this commit; the sweep of the other 56 head classes follows. --- .run/P36/delever/calibration.json | 360 +++++++++--------- .../corpus/record/docs/decision-log.md | 34 ++ decomp-architect/corpus/tools/P10/delever.py | 185 ++++++++- .../corpus/tools/P10/delever_search.py | 20 +- docs/SETUP.md | 2 +- tools/delever.py | 185 ++++++++- tools/delever_search.py | 20 +- 7 files changed, 609 insertions(+), 197 deletions(-) diff --git a/.run/P36/delever/calibration.json b/.run/P36/delever/calibration.json index 6ad81a23d..6662386dc 100644 --- a/.run/P36/delever/calibration.json +++ b/.run/P36/delever/calibration.json @@ -1,7 +1,7 @@ { - "head": "5a8bd1021", + "head": "fd2433b6d", "stamp": "b44c5a8a3423", - "generated": "2026-09-10 00:04", + "generated": "2026-09-10 00:39", "aliases": [ "main", "ov_SC03_014", @@ -21,207 +21,207 @@ "main": { "objects": 85, "identical": 85, - "seconds": 7.544000000000001, - "mean_s": 0.089 + "seconds": 6.715, + "mean_s": 0.079 }, "ov_SC03_014": { "objects": 32, "identical": 32, - "seconds": 4.776, - "mean_s": 0.149 + "seconds": 4.601000000000001, + "mean_s": 0.144 }, "ov_SC03_015": { "objects": 32, "identical": 32, - "seconds": 4.493000000000001, - "mean_s": 0.14 + "seconds": 4.5489999999999995, + "mean_s": 0.142 }, "ov_SC04_011": { "objects": 28, "identical": 28, - "seconds": 3.912000000000001, - "mean_s": 0.14 + "seconds": 3.94, + "mean_s": 0.141 } }, "per_object_seconds": { - "build/src/800.o": 0.814, - "build/src/800_b.o": 0.134, - "build/src/800_b_2.o": 0.367, - "build/src/800_b_o0a.o": 0.11, - "build/src/800_c.o": 0.246, - "build/src/800b2.o": 0.107, - "build/src/apicard1.o": 0.093, - "build/src/apicard2.o": 0.095, - "build/src/apicard3.o": 0.09, - "build/src/apicard4.o": 0.09, - "build/src/apicard5.o": 0.091, - "build/src/apicard6.o": 0.097, - "build/src/apicard7.o": 0.095, - "build/src/boot.o": 0.109, - "build/src/gap.o": 0.089, - "build/src/libapi1.o": 0.094, - "build/src/libapi2.o": 0.058, - "build/src/libc2_1.o": 0.103, + "build/src/800.o": 0.754, + "build/src/800_b.o": 0.084, + "build/src/800_b_2.o": 0.287, + "build/src/800_b_o0a.o": 0.073, + "build/src/800_c.o": 0.189, + "build/src/800b2.o": 0.072, + "build/src/apicard1.o": 0.066, + "build/src/apicard2.o": 0.075, + "build/src/apicard3.o": 0.077, + "build/src/apicard4.o": 0.065, + "build/src/apicard5.o": 0.071, + "build/src/apicard6.o": 0.066, + "build/src/apicard7.o": 0.083, + "build/src/boot.o": 0.07, + "build/src/gap.o": 0.07, + "build/src/libapi1.o": 0.081, + "build/src/libapi2.o": 0.054, + "build/src/libc2_1.o": 0.064, "build/src/libc2_2.o": 0.058, - "build/src/libcd1.o": 0.096, - "build/src/libcd2.o": 0.067, - "build/src/libetc.o": 0.071, - "build/src/libgpu.o": 0.079, - "build/src/libgpu2.o": 0.094, - "build/src/libgs1.o": 0.068, - "build/src/libgs2.o": 0.066, - "build/src/libgs3.o": 0.063, - "build/src/libgs4.o": 0.068, - "build/src/libgs5.o": 0.062, - "build/src/libgs6.o": 0.112, - "build/src/libgs7.o": 0.068, + "build/src/libcd1.o": 0.066, + "build/src/libcd2.o": 0.056, + "build/src/libetc.o": 0.063, + "build/src/libgpu.o": 0.065, + "build/src/libgpu2.o": 0.081, + "build/src/libgs1.o": 0.059, + "build/src/libgs2.o": 0.063, + "build/src/libgs3.o": 0.077, + "build/src/libgs4.o": 0.069, + "build/src/libgs5.o": 0.05, + "build/src/libgs6.o": 0.074, + "build/src/libgs7.o": 0.059, "build/src/libgs8.o": 0.061, - "build/src/libgte1.o": 0.062, - "build/src/libgte10.o": 0.056, - "build/src/libgte11.o": 0.06, - "build/src/libgte12.o": 0.064, - "build/src/libgte13.o": 0.066, - "build/src/libgte14.o": 0.056, - "build/src/libgte15.o": 0.071, - "build/src/libgte16.o": 0.065, - "build/src/libgte17.o": 0.069, - "build/src/libgte18.o": 0.059, - "build/src/libgte19.o": 0.063, - "build/src/libgte2.o": 0.064, - "build/src/libgte20.o": 0.064, - "build/src/libgte21.o": 0.069, - "build/src/libgte22.o": 0.07, + "build/src/libgte1.o": 0.06, + "build/src/libgte10.o": 0.067, + "build/src/libgte11.o": 0.062, + "build/src/libgte12.o": 0.061, + "build/src/libgte13.o": 0.06, + "build/src/libgte14.o": 0.067, + "build/src/libgte15.o": 0.067, + "build/src/libgte16.o": 0.067, + "build/src/libgte17.o": 0.071, + "build/src/libgte18.o": 0.065, + "build/src/libgte19.o": 0.066, + "build/src/libgte2.o": 0.065, + "build/src/libgte20.o": 0.063, + "build/src/libgte21.o": 0.061, + "build/src/libgte22.o": 0.065, "build/src/libgte23.o": 0.065, - "build/src/libgte24.o": 0.067, - "build/src/libgte25.o": 0.086, - "build/src/libgte26.o": 0.059, - "build/src/libgte27.o": 0.071, - "build/src/libgte28.o": 0.064, - "build/src/libgte29.o": 0.071, - "build/src/libgte3.o": 0.07, - "build/src/libgte30.o": 0.072, - "build/src/libgte4.o": 0.066, + "build/src/libgte24.o": 0.064, + "build/src/libgte25.o": 0.072, + "build/src/libgte26.o": 0.067, + "build/src/libgte27.o": 0.063, + "build/src/libgte28.o": 0.063, + "build/src/libgte29.o": 0.062, + "build/src/libgte3.o": 0.068, + "build/src/libgte30.o": 0.064, + "build/src/libgte4.o": 0.063, "build/src/libgte5.o": 0.063, - "build/src/libgte6.o": 0.066, - "build/src/libgte7.o": 0.066, - "build/src/libgte8.o": 0.065, - "build/src/libgte9.o": 0.067, - "build/src/libmcrd1.o": 0.091, - "build/src/libmcrd2.o": 0.075, - "build/src/libpad1.o": 0.07, - "build/src/libpad2.o": 0.082, - "build/src/sgap.o": 0.063, - "build/src/sgap_2.o": 0.063, - "build/src/sgap_3.o": 0.062, - "build/src/sgap_4.o": 0.074, - "build/src/sgap_5.o": 0.071, - "build/src/sgap_6.o": 0.062, - "build/src/sgap_8.o": 0.073, - "build/src/snd1.o": 0.084, - "build/src/snd10.o": 0.071, - "build/src/snd11.o": 0.065, - "build/src/snd12.o": 0.065, - "build/src/snd2.o": 0.093, - "build/src/snd3.o": 0.065, - "build/src/snd4.o": 0.062, - "build/src/snd5.o": 0.061, - "build/src/snd6.o": 0.079, - "build/src/snd7.o": 0.068, - "build/src/snd8.o": 0.074, - "build/src/snd9.o": 0.08, - "build/src/ov_SC03_014/ov_SC03_014.o": 0.136, - "build/src/ov_SC03_014/ov_SC03_014_after.o": 0.505, - "build/src/ov_SC03_014/ov_SC03_014_jr_8012ACE0.o": 0.394, - "build/src/ov_SC03_014/ov_SC03_014_jr_80135888.o": 0.059, - "build/src/ov_SC03_014/ov_SC03_014_jr_80135A4C.o": 0.062, - "build/src/ov_SC03_014/ov_SC03_014_jr_80135D20.o": 0.127, - "build/src/ov_SC03_014/ov_SC03_014_jr_801380E0.o": 0.16, - "build/src/ov_SC03_014/ov_SC03_014_jr_8013C98C.o": 0.127, - "build/src/ov_SC03_014/ov_SC03_014_jr_8013F350.o": 0.08, - "build/src/ov_SC03_014/ov_SC03_014_jr_8013FFD8.o": 0.078, - "build/src/ov_SC03_014/ov_SC03_014_jr_80140608.o": 0.183, - "build/src/ov_SC03_014/ov_SC03_014_jr_8015444C.o": 0.07, - "build/src/ov_SC03_014/ov_SC03_014_jr_80154C24.o": 0.171, - "build/src/ov_SC03_014/ov_SC03_014_jr_801588CC.o": 0.1, - "build/src/ov_SC03_014/ov_SC03_014_jr_80159C84.o": 0.072, - "build/src/ov_SC03_014/ov_SC03_014_jr_8015A3C8.o": 0.072, - "build/src/ov_SC03_014/ov_SC03_014_jr_8015AE2C.o": 0.1, - "build/src/ov_SC03_014/ov_SC03_014_jr_8015C32C.o": 0.498, - "build/src/ov_SC03_014/ov_SC03_014_jr_8016AB6C.o": 0.275, - "build/src/ov_SC03_014/ov_SC03_014_jr_80171B4C.o": 0.101, - "build/src/ov_SC03_014/ov_SC03_014_jr_801734BC.o": 0.213, - "build/src/ov_SC03_014/ov_SC03_014_jr_801789AC.o": 0.061, - "build/src/ov_SC03_014/ov_SC03_014_jr_80178D40.o": 0.106, - "build/src/ov_SC03_014/ov_SC03_014_jr_8017A4AC.o": 0.074, - "build/src/ov_SC03_014/ov_SC03_014_jr_8017AE2C.o": 0.159, - "build/src/ov_SC03_014/ov_SC03_014_jr_8017EB7C.o": 0.212, - "build/src/ov_SC03_014/ov_SC03_014_jr_80184440.o": 0.054, - "build/src/ov_SC03_014/ov_SC03_014_jr_801848E4.o": 0.281, - "build/src/ov_SC03_014/ov_SC03_014_o0b.o": 0.061, + "build/src/libgte6.o": 0.068, + "build/src/libgte7.o": 0.064, + "build/src/libgte8.o": 0.067, + "build/src/libgte9.o": 0.066, + "build/src/libmcrd1.o": 0.075, + "build/src/libmcrd2.o": 0.068, + "build/src/libpad1.o": 0.069, + "build/src/libpad2.o": 0.069, + "build/src/sgap.o": 0.075, + "build/src/sgap_2.o": 0.088, + "build/src/sgap_3.o": 0.059, + "build/src/sgap_4.o": 0.075, + "build/src/sgap_5.o": 0.065, + "build/src/sgap_6.o": 0.06, + "build/src/sgap_8.o": 0.072, + "build/src/snd1.o": 0.07, + "build/src/snd10.o": 0.059, + "build/src/snd11.o": 0.069, + "build/src/snd12.o": 0.062, + "build/src/snd2.o": 0.072, + "build/src/snd3.o": 0.064, + "build/src/snd4.o": 0.066, + "build/src/snd5.o": 0.06, + "build/src/snd6.o": 0.071, + "build/src/snd7.o": 0.066, + "build/src/snd8.o": 0.069, + "build/src/snd9.o": 0.064, + "build/src/ov_SC03_014/ov_SC03_014.o": 0.134, + "build/src/ov_SC03_014/ov_SC03_014_after.o": 0.5, + "build/src/ov_SC03_014/ov_SC03_014_jr_8012ACE0.o": 0.379, + "build/src/ov_SC03_014/ov_SC03_014_jr_80135888.o": 0.06, + "build/src/ov_SC03_014/ov_SC03_014_jr_80135A4C.o": 0.057, + "build/src/ov_SC03_014/ov_SC03_014_jr_80135D20.o": 0.121, + "build/src/ov_SC03_014/ov_SC03_014_jr_801380E0.o": 0.165, + "build/src/ov_SC03_014/ov_SC03_014_jr_8013C98C.o": 0.12, + "build/src/ov_SC03_014/ov_SC03_014_jr_8013F350.o": 0.079, + "build/src/ov_SC03_014/ov_SC03_014_jr_8013FFD8.o": 0.07, + "build/src/ov_SC03_014/ov_SC03_014_jr_80140608.o": 0.181, + "build/src/ov_SC03_014/ov_SC03_014_jr_8015444C.o": 0.08, + "build/src/ov_SC03_014/ov_SC03_014_jr_80154C24.o": 0.176, + "build/src/ov_SC03_014/ov_SC03_014_jr_801588CC.o": 0.099, + "build/src/ov_SC03_014/ov_SC03_014_jr_80159C84.o": 0.069, + "build/src/ov_SC03_014/ov_SC03_014_jr_8015A3C8.o": 0.067, + "build/src/ov_SC03_014/ov_SC03_014_jr_8015AE2C.o": 0.101, + "build/src/ov_SC03_014/ov_SC03_014_jr_8015C32C.o": 0.449, + "build/src/ov_SC03_014/ov_SC03_014_jr_8016AB6C.o": 0.257, + "build/src/ov_SC03_014/ov_SC03_014_jr_80171B4C.o": 0.095, + "build/src/ov_SC03_014/ov_SC03_014_jr_801734BC.o": 0.197, + "build/src/ov_SC03_014/ov_SC03_014_jr_801789AC.o": 0.056, + "build/src/ov_SC03_014/ov_SC03_014_jr_80178D40.o": 0.097, + "build/src/ov_SC03_014/ov_SC03_014_jr_8017A4AC.o": 0.068, + "build/src/ov_SC03_014/ov_SC03_014_jr_8017AE2C.o": 0.155, + "build/src/ov_SC03_014/ov_SC03_014_jr_8017EB7C.o": 0.196, + "build/src/ov_SC03_014/ov_SC03_014_jr_80184440.o": 0.053, + "build/src/ov_SC03_014/ov_SC03_014_jr_801848E4.o": 0.276, + "build/src/ov_SC03_014/ov_SC03_014_o0b.o": 0.059, "build/src/ov_SC03_014/ov_SC03_014_o0c.o": 0.061, - "build/src/ov_SC03_014/ov_SC03_014_o0d.o": 0.058, - "build/src/ov_SC03_014/ov_SC03_014_o0e.o": 0.066, - "build/src/ov_SC03_015/ov_SC03_015.o": 0.112, - "build/src/ov_SC03_015/ov_SC03_015_after.o": 0.469, - "build/src/ov_SC03_015/ov_SC03_015_jr_8012ACE0.o": 0.382, - "build/src/ov_SC03_015/ov_SC03_015_jr_80135888.o": 0.059, - "build/src/ov_SC03_015/ov_SC03_015_jr_80135A4C.o": 0.057, - "build/src/ov_SC03_015/ov_SC03_015_jr_80135D20.o": 0.112, - "build/src/ov_SC03_015/ov_SC03_015_jr_801380E0.o": 0.144, - "build/src/ov_SC03_015/ov_SC03_015_jr_8013C98C.o": 0.111, - "build/src/ov_SC03_015/ov_SC03_015_jr_8013F350.o": 0.067, - "build/src/ov_SC03_015/ov_SC03_015_jr_8013FFD8.o": 0.066, - "build/src/ov_SC03_015/ov_SC03_015_jr_80140608.o": 0.18, - "build/src/ov_SC03_015/ov_SC03_015_jr_8015444C.o": 0.071, - "build/src/ov_SC03_015/ov_SC03_015_jr_80154C24.o": 0.171, + "build/src/ov_SC03_014/ov_SC03_014_o0d.o": 0.057, + "build/src/ov_SC03_014/ov_SC03_014_o0e.o": 0.067, + "build/src/ov_SC03_015/ov_SC03_015.o": 0.123, + "build/src/ov_SC03_015/ov_SC03_015_after.o": 0.504, + "build/src/ov_SC03_015/ov_SC03_015_jr_8012ACE0.o": 0.393, + "build/src/ov_SC03_015/ov_SC03_015_jr_80135888.o": 0.049, + "build/src/ov_SC03_015/ov_SC03_015_jr_80135A4C.o": 0.054, + "build/src/ov_SC03_015/ov_SC03_015_jr_80135D20.o": 0.121, + "build/src/ov_SC03_015/ov_SC03_015_jr_801380E0.o": 0.148, + "build/src/ov_SC03_015/ov_SC03_015_jr_8013C98C.o": 0.104, + "build/src/ov_SC03_015/ov_SC03_015_jr_8013F350.o": 0.068, + "build/src/ov_SC03_015/ov_SC03_015_jr_8013FFD8.o": 0.064, + "build/src/ov_SC03_015/ov_SC03_015_jr_80140608.o": 0.168, + "build/src/ov_SC03_015/ov_SC03_015_jr_8015444C.o": 0.065, + "build/src/ov_SC03_015/ov_SC03_015_jr_80154C24.o": 0.167, "build/src/ov_SC03_015/ov_SC03_015_jr_801588CC.o": 0.09, - "build/src/ov_SC03_015/ov_SC03_015_jr_80159C84.o": 0.068, - "build/src/ov_SC03_015/ov_SC03_015_jr_8015A3C8.o": 0.071, - "build/src/ov_SC03_015/ov_SC03_015_jr_8015AE2C.o": 0.093, - "build/src/ov_SC03_015/ov_SC03_015_jr_8015C32C.o": 0.455, + "build/src/ov_SC03_015/ov_SC03_015_jr_80159C84.o": 0.065, + "build/src/ov_SC03_015/ov_SC03_015_jr_8015A3C8.o": 0.067, + "build/src/ov_SC03_015/ov_SC03_015_jr_8015AE2C.o": 0.098, + "build/src/ov_SC03_015/ov_SC03_015_jr_8015C32C.o": 0.449, "build/src/ov_SC03_015/ov_SC03_015_jr_8016AB6C.o": 0.27, - "build/src/ov_SC03_015/ov_SC03_015_jr_80171B4C.o": 0.1, - "build/src/ov_SC03_015/ov_SC03_015_jr_801734BC.o": 0.197, - "build/src/ov_SC03_015/ov_SC03_015_jr_801789AC.o": 0.057, - "build/src/ov_SC03_015/ov_SC03_015_jr_80178D40.o": 0.099, - "build/src/ov_SC03_015/ov_SC03_015_jr_8017A4AC.o": 0.059, - "build/src/ov_SC03_015/ov_SC03_015_jr_8017AE2C.o": 0.168, - "build/src/ov_SC03_015/ov_SC03_015_jr_8017EB7C.o": 0.195, - "build/src/ov_SC03_015/ov_SC03_015_jr_80184440.o": 0.06, - "build/src/ov_SC03_015/ov_SC03_015_jr_801848E4.o": 0.274, - "build/src/ov_SC03_015/ov_SC03_015_o0b.o": 0.062, - "build/src/ov_SC03_015/ov_SC03_015_o0c.o": 0.058, - "build/src/ov_SC03_015/ov_SC03_015_o0d.o": 0.054, - "build/src/ov_SC03_015/ov_SC03_015_o0e.o": 0.062, - "build/src/ov_SC04_011/ov_SC04_011.o": 0.126, - "build/src/ov_SC04_011/ov_SC04_011_after.o": 0.422, - "build/src/ov_SC04_011/ov_SC04_011_jr_8012ACE0.o": 0.338, - "build/src/ov_SC04_011/ov_SC04_011_jr_80135888.o": 0.05, - "build/src/ov_SC04_011/ov_SC04_011_jr_80135A4C.o": 0.055, + "build/src/ov_SC03_015/ov_SC03_015_jr_80171B4C.o": 0.101, + "build/src/ov_SC03_015/ov_SC03_015_jr_801734BC.o": 0.194, + "build/src/ov_SC03_015/ov_SC03_015_jr_801789AC.o": 0.061, + "build/src/ov_SC03_015/ov_SC03_015_jr_80178D40.o": 0.098, + "build/src/ov_SC03_015/ov_SC03_015_jr_8017A4AC.o": 0.063, + "build/src/ov_SC03_015/ov_SC03_015_jr_8017AE2C.o": 0.171, + "build/src/ov_SC03_015/ov_SC03_015_jr_8017EB7C.o": 0.198, + "build/src/ov_SC03_015/ov_SC03_015_jr_80184440.o": 0.053, + "build/src/ov_SC03_015/ov_SC03_015_jr_801848E4.o": 0.279, + "build/src/ov_SC03_015/ov_SC03_015_o0b.o": 0.067, + "build/src/ov_SC03_015/ov_SC03_015_o0c.o": 0.061, + "build/src/ov_SC03_015/ov_SC03_015_o0d.o": 0.058, + "build/src/ov_SC03_015/ov_SC03_015_o0e.o": 0.078, + "build/src/ov_SC04_011/ov_SC04_011.o": 0.121, + "build/src/ov_SC04_011/ov_SC04_011_after.o": 0.419, + "build/src/ov_SC04_011/ov_SC04_011_jr_8012ACE0.o": 0.355, + "build/src/ov_SC04_011/ov_SC04_011_jr_80135888.o": 0.051, + "build/src/ov_SC04_011/ov_SC04_011_jr_80135A4C.o": 0.053, "build/src/ov_SC04_011/ov_SC04_011_jr_80135D20.o": 0.116, - "build/src/ov_SC04_011/ov_SC04_011_jr_801380E0.o": 0.155, - "build/src/ov_SC04_011/ov_SC04_011_jr_8013C98C.o": 0.121, - "build/src/ov_SC04_011/ov_SC04_011_jr_8013F350.o": 0.068, - "build/src/ov_SC04_011/ov_SC04_011_jr_8013FFD8.o": 0.062, - "build/src/ov_SC04_011/ov_SC04_011_jr_80140608.o": 0.175, - "build/src/ov_SC04_011/ov_SC04_011_jr_8015444C.o": 0.068, - "build/src/ov_SC04_011/ov_SC04_011_jr_80154C24.o": 0.158, - "build/src/ov_SC04_011/ov_SC04_011_jr_801588CC.o": 0.086, - "build/src/ov_SC04_011/ov_SC04_011_jr_80159C84.o": 0.063, - "build/src/ov_SC04_011/ov_SC04_011_jr_8015A3C8.o": 0.068, - "build/src/ov_SC04_011/ov_SC04_011_jr_8015AE2C.o": 0.094, - "build/src/ov_SC04_011/ov_SC04_011_jr_8015C32C.o": 0.358, - "build/src/ov_SC04_011/ov_SC04_011_jr_8016AB6C.o": 0.225, - "build/src/ov_SC04_011/ov_SC04_011_jr_80171B4C.o": 0.089, - "build/src/ov_SC04_011/ov_SC04_011_jr_801734BC.o": 0.172, - "build/src/ov_SC04_011/ov_SC04_011_jr_801789AC.o": 0.053, - "build/src/ov_SC04_011/ov_SC04_011_jr_80178D40.o": 0.087, - "build/src/ov_SC04_011/ov_SC04_011_jr_8017A4AC.o": 0.064, - "build/src/ov_SC04_011/ov_SC04_011_jr_8017AE2C.o": 0.101, - "build/src/ov_SC04_011/ov_SC04_011_jr_8017D494.o": 0.432, - "build/src/ov_SC04_011/ov_SC04_011_o0b.o": 0.046, - "build/src/ov_SC04_011/ov_SC04_011_o0c.o": 0.06 + "build/src/ov_SC04_011/ov_SC04_011_jr_801380E0.o": 0.142, + "build/src/ov_SC04_011/ov_SC04_011_jr_8013C98C.o": 0.113, + "build/src/ov_SC04_011/ov_SC04_011_jr_8013F350.o": 0.075, + "build/src/ov_SC04_011/ov_SC04_011_jr_8013FFD8.o": 0.06, + "build/src/ov_SC04_011/ov_SC04_011_jr_80140608.o": 0.179, + "build/src/ov_SC04_011/ov_SC04_011_jr_8015444C.o": 0.07, + "build/src/ov_SC04_011/ov_SC04_011_jr_80154C24.o": 0.163, + "build/src/ov_SC04_011/ov_SC04_011_jr_801588CC.o": 0.093, + "build/src/ov_SC04_011/ov_SC04_011_jr_80159C84.o": 0.066, + "build/src/ov_SC04_011/ov_SC04_011_jr_8015A3C8.o": 0.067, + "build/src/ov_SC04_011/ov_SC04_011_jr_8015AE2C.o": 0.091, + "build/src/ov_SC04_011/ov_SC04_011_jr_8015C32C.o": 0.351, + "build/src/ov_SC04_011/ov_SC04_011_jr_8016AB6C.o": 0.224, + "build/src/ov_SC04_011/ov_SC04_011_jr_80171B4C.o": 0.09, + "build/src/ov_SC04_011/ov_SC04_011_jr_801734BC.o": 0.175, + "build/src/ov_SC04_011/ov_SC04_011_jr_801789AC.o": 0.056, + "build/src/ov_SC04_011/ov_SC04_011_jr_80178D40.o": 0.092, + "build/src/ov_SC04_011/ov_SC04_011_jr_8017A4AC.o": 0.065, + "build/src/ov_SC04_011/ov_SC04_011_jr_8017AE2C.o": 0.11, + "build/src/ov_SC04_011/ov_SC04_011_jr_8017D494.o": 0.428, + "build/src/ov_SC04_011/ov_SC04_011_o0b.o": 0.049, + "build/src/ov_SC04_011/ov_SC04_011_o0c.o": 0.066 }, "ok": true, - "seconds": 2.3 + "seconds": 2.2 } diff --git a/decomp-architect/corpus/record/docs/decision-log.md b/decomp-architect/corpus/record/docs/decision-log.md index 947c3b5b8..f3efe08bf 100644 --- a/decomp-architect/corpus/record/docs/decision-log.md +++ b/decomp-architect/corpus/record/docs/decision-log.md @@ -3714,3 +3714,37 @@ register lever (accelerators (16)), and treat "PROVED" as "proved against this l 35 minutes of state only in its transcript. (3) Build the disagreeing oracle before the batch runs, not after: the text oracle found in one run what the byte join could never see. (4) A tool's restore path is part of the tool (R57): `git checkout` on an uncommitted tree is a destroyer with a green exit code. + +## P36 S101 (2026-09-09) — the levers phase pivots from search to reading: a scored engine, the head as the number, and one agent at a time + +**Context and belief.** After T6 the residue was 33,427 sites in 12,048 bodies (1,755 text classes); the free recipe rung had measured +as a replication engine (134/134 on a known shape, 0/300 on unknown ones, cookbook §454a) and the permuter as a random walk that +reprints the source. Drew's plan for S101: build a compute-only guided engine ("a fancier permuter, focused on pin pulling and C shape +matching") while a Fable agent reads gcc 2.7.2's source for the residual → move map, and defer the exemplar triage. + +**What was built and what it measured.** Rung G (`tools/delever_search.py`): the candidate is scored by the function's instructions in +the oracle's own scratch object against the fleet run's baseline object (an edit distance over the reloc-masked words — a positional +count read one inlined temp as 43), the residual classified from the diff blocks, the move families ordered by the class from lane +B's map and ranked round-robin, a beam composing two to five moves, every score-0 banked through the real recipe and propagated to the +class. Six runs, no drafting tokens: g1 1/16, g2 1/16, g3 13/64 (1,516 bodies), g4s 3/38, g5 5/70, g6+g6b 110/400 (the tail) — +33,427 → 30,358 sites, every bank gated 218/218. The generator registry grew from six moves to fourteen, each new one read off a +residual with `--explain` and, where lane B's map named it, verified on bytes (a constant-operand swap is byte-neutral; the do-while +lever is the ref weight in `.lreg`; the 16-bit copy does not reach a value whose known bits already fit). + +**What failed, and why it looked right.** Wider search looked like the lever after g3 (nine of fifteen moved past g1's best when the +beam widened); g5 then spent 112,216 compiles at three times the beam and eight times the budget for 62 bodies in small classes and +closed none of the ~50 head classes. The instrument also lied twice before it was caught by its own controls: a per-tag scratch +object let two workers read each other's objects (a byte-identical body scored 14,871), and the ledger's rung-R miss rows carried no +text hash (301 bodies read as one class). Both were found by reading an artifact, not by a red check. + +**The pivot.** Derived from the ledger at the end: 57 classes of ≥100 copies hold 7,318 of the 9,796 residue bodies, 17 within four +instructions. Each is a reading and a generator, not a search. Drew's decision: run T7 as ONE AGENT AT A TIME on the head (he had +approved a 57-agent wave and then reshaped it: "time clock isn't the issue, it's efficiency … hone our methodology each time one lands, +in the hopes of learning enough to be able to tool crack the rest"), starting in a fresh session. The agent's loop scores its +candidate without a tree write (`--try`), the coordinator banks and propagates, the move becomes a generator, the engine sweeps the +rest for free — the harvest→toolify gate at the granularity of one crack. + +**Hindsight (the better path).** Read the residual first, always: every generator that closed a class this session came from +`--explain` on one body and a line of the compiler's source, and every hour of wider search bought less than the previous one. The +engine's real value was not the search but the SCORE — a distance that turns a stall into a named shape — and the instrument that +prints it. Build that on day one of a de-lever campaign, before any search. diff --git a/decomp-architect/corpus/tools/P10/delever.py b/decomp-architect/corpus/tools/P10/delever.py index bb965646d..f0742e2d4 100644 --- a/decomp-architect/corpus/tools/P10/delever.py +++ b/decomp-architect/corpus/tools/P10/delever.py @@ -2079,7 +2079,140 @@ def param_copies(text, tu, fn, d_): return out -ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14") +def if_chains(masked, lo, hi): + """[(chain_lo, chain_hi, [(arm_lo, arm_hi)])] — every brace-form if / else-if / else chain whose lines lie in + masked[lo:hi] at the body's own depth. `chain_hi` is the line index just past the chain's last `}`; an arm span is the + lines strictly inside that arm's braces. Depth is counted on the MASKED text, so a brace in a string or a comment is + not a brace. Chains that do not open a brace on the `if` line (a one-statement `if` without braces) are skipped: the + sink rewrites arm bodies and needs somewhere to put the statement.""" + out = [] + i = lo + while i < hi: + s_ = masked[i].strip() + if not (s_.startswith("if") and re.match(r"^if\s*\(", s_) and s_.endswith("{")): + i += 1 + continue + arms, depth, arm_start, j = [], 0, i + 1, i + while j < hi: + nxt = masked[j].strip() + closes = masked[j].count("}") + # the CLOSES are counted before the OPENS: on a `} else if (…) {` line the two net to zero, and a depth + # counter that adds both at once never comes back to zero — the chain then looks like one unterminated arm. + if j > i and depth - closes == 0: # this arm's `}` is on line j + arms.append((arm_start, j)) + m = re.match(r"^\}\s*else\b(.*)$", nxt) + if m and m.group(1).strip().endswith("{"): # `} else {` or `} else if (…) {` + arm_start, depth, j = j + 1, 1, j + 1 + continue + if nxt == "}" and j + 1 < hi and re.match(r"^else\b", masked[j + 1].strip()) \ + and masked[j + 1].strip().endswith("{"): + arm_start, depth, j = j + 2, 1, j + 2 # the `else …{` on its own line + continue + break + depth += masked[j].count("{") - closes + j += 1 + if len(arms) >= 2: + out.append((i, j + 1, arms)) + i = j + 1 + else: + i += 1 + return out + + +def sink_merges(text, tu, fn, d_): + """[(description, candidate text)] — R15: the statement AFTER an if/else chain sunk into every arm, and the variables + it consumed deleted. `if (c) { v = e1; } else { v = e2; } w = f(v);` -> `if (c) { w = f(e1); } else { w = f(e2); }`. + + T7 agent a1's crack of func_80156044 (2026-09-10, 130 bodies, the rank-1 head class six rung-G runs left at best 1). + The mechanism is a REGISTER move, not a scheduling one: a variable set in every arm and read after the merge is a + CROSS-BLOCK pseudo, so (i) local-alloc never makes a quantity for it (`local-alloc.c:472`, `next_qty` reset per block + at `:517`) and each arm holds two quantities, which is `block_alloc`'s unrolled `case 2` at `local-alloc.c:1499-1502` + — one `qty_compare` (`:1578-1596`), higher density first; sinking makes it a third block-local quantity and + `case 3` at `: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 (ii) while it is a global allocno it can inherit a copy + preference from whatever the merge statement's result is passed to (`set_preference` `global.c:1535+`, merged by + `expand_preferences` `global.c:781-825`, overriding first-fit at `global.c:1034-1067`) — sinking removes it from + `global.c` entirely. Read the whole reading in `.run/P36/agents/ov_SC04_011__func_80156044/mechanism.md`. + + Applicability is checked, not assumed: every consumed variable must be assigned exactly once in EVERY arm by a simple + statement, must appear in the merge statement, and must occur nowhere else in the function (declaration + one + assignment per arm + its uses in the merge statement is its whole census) — otherwise the rewrite would change what + the code reads. The bytes remain the correctness proof (a rewrite that changes behaviour simply DIFFERS).""" + lines = text.split("\n") + masked = [sc.mask_text(l) for l in lines] + lo, hi = d_["line"], d_["end"] - 1 + whole = sc.mask_text("\n".join(lines[d_["line"] - 1:d_["end"]])) + out = [] + for c_lo, c_hi, arms in if_chains(masked, lo, hi): + j = next((k for k in range(c_hi, hi) if masked[k].strip()), None) + if j is None or not simple_stmt(masked[j]): + continue + merge_masked = masked[j] + eq = re.search(r"(?+\-*/%&|^~])=(?!=)", merge_masked) + if not eq: # only an assignment merges arm values + continue + rhs = merge_masked[eq.end():] + # every variable assigned exactly once, by a simple statement, in EVERY arm + per_arm = [] + for a_lo, a_hi in arms: + got = {} + for k in range(a_lo, a_hi): + if not simple_stmt(masked[k]): + continue + m = re.match(r"^\s*([A-Za-z_]\w*)\s*=\s*(.+);\s*$", masked[k]) + if not m: + continue + if m.group(1) in got: # assigned twice in one arm: not a single value + got[m.group(1)] = None + else: + got[m.group(1)] = k + per_arm.append(got) + common = set(k for k, v in per_arm[0].items() if v is not None) + for g in per_arm[1:]: + common &= set(k for k, v in g.items() if v is not None) + names_ = sorted(v for v in common if re.search(r"(?])%s(?![\w])" % re.escape(v), rhs)) + if not names_: + continue + decl = {} + for v in names_: + # the variable's whole census: one declaration, one assignment per arm, and its uses in the merge statement + uses = len(re.findall(r"(?])%s(?![\w])" % re.escape(v), whole)) + in_merge = len(re.findall(r"(?])%s(?![\w])" % re.escape(v), merge_masked)) + d_line = next((k for k in range(lo, hi) + if is_decl_line(masked[k].strip()) + and re.search(r"(?])%s(?![\w])\s*(?:=|;|,)" % re.escape(v), masked[k])), None) + if d_line is None or MULTI_DECL.match(masked[d_line]) or "=" in masked[d_line]: + decl[v] = None # a shared or initialised declaration: leave it standing + else: + decl[v] = d_line + if uses != len(arms) + in_merge + (1 if d_line is not None else 0): + names_ = None + break + if not names_: + continue + drop = {decl[v] for v in names_ if decl[v] is not None} | {j} + after = {} # line index -> the sunk statement to emit just after it + for a_idx, (a_lo, a_hi) in enumerate(arms): + stmt = lines[j].strip() + for v in names_: + k = per_arm[a_idx][v] + expr = re.match(r"^\s*[A-Za-z_]\w*\s*=\s*(.+);\s*$", lines[k]).group(1).strip() + stmt = re.sub(r"(?])%s(?![\w])" % re.escape(v), "(" + expr + ")", stmt) + drop.add(k) + tail = a_hi - 1 # the arm's LAST BODY line (a_hi is its `}`) + ref = lines[tail] if lines[tail].strip() else lines[a_lo] + after[tail] = ref[:len(ref) - len(ref.lstrip())] + stmt + cand = [] + for k, l in enumerate(lines): # a dropped line may still be the one we append after + if k not in drop: + cand.append(l) + if k in after: + cand.append(after[k]) + out.append((f"sink @{j + 1} ({','.join(names_)})", "\n".join(cand))) + return out + + +ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14", "R15") RUNG_R_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7") # the free sweep's set (R8/R9 are the search engine's until measured) @@ -2184,6 +2317,9 @@ def recipe_candidates(text, tu, fn, names, limit=24, rng=None, cap=40, blocks=Tr if "R13" in fam: for desc, cand in reassociations(text, tu, fn, d_): out.append(("R13", desc, cand)) + if "R15" in fam: + for desc, cand in sink_merges(text, tu, fn, d_): + out.append(("R15", 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)) @@ -2894,6 +3030,53 @@ def selftest(): fail(f"remap_body produced {got!r} ({why})") if remap_body(exb, exa, "void func_80300000(void) { D_80400000 = 0; }")[0] is not None: fail("remap_body must refuse a sibling with a different token count") + # R15, the sink (T7 agent a1's crack of func_80156044, 2026-09-10): the merge statement pushed into every arm and the + # variables it consumed deleted. The three controls are the ones the applicability test exists for. + SINKFIX = ("void func_80100000(int c) {\n" + " s32 base;\n" + " u8 *row;\n" + "\n" + " if (c) {\n" + " base = 1;\n" + " } else if (c == 2) {\n" + " base = 2;\n" + " } else {\n" + " base = 3;\n" + " }\n" + " row = (u8 *)(base + 4);\n" + " use(row);\n" + "}") + d15 = next(r for r in sc.scan_text(SINKFIX, "src/fx/s.c", shared_defs=None) + if r["form"] == "def" and r["name"] == "func_80100000") + got15 = sink_merges(SINKFIX, "src/fx/s.c", "func_80100000", d15) + if len(got15) != 1 or "sink" not in got15[0][0] or "base" not in got15[0][0]: + fail(f"R15 must find one sink in the three-arm fixture, got {[g[0] for g in got15]}") + else: + c15 = got15[0][1] + want15 = ["row = (u8 *)((1) + 4);", "row = (u8 *)((2) + 4);", "row = (u8 *)((3) + 4);"] + if not all(w in c15 for w in want15): + fail(f"R15 must sink the merge statement into every arm: {c15!r}") + if "s32 base;" in c15 or "base = 1;" in c15: + fail("R15 must delete the consumed variable's declaration and its per-arm assignments") + if c15.count("row = ") != 3 or "row = (u8 *)(base + 4);" in c15: + fail("R15 must remove the merge statement itself") + if "u8 *row;" not in c15: + fail("R15 must keep the declaration of the variable the merge statement ASSIGNS") + # control 1: a consumed variable read anywhere else is not sinkable (its census would not add up) + if sink_merges(SINKFIX.replace(" use(row);", " use(row + base);"), "src/fx/s.c", "func_80100000", + next(r for r in sc.scan_text(SINKFIX.replace(" use(row);", " use(row + base);"), "src/fx/s.c", + shared_defs=None) if r["form"] == "def")): + fail("R15 must refuse a variable that is read after the merge statement") + # control 2: a variable not set in EVERY arm is not sinkable + if sink_merges(SINKFIX.replace(" base = 2;\n", ""), "src/fx/s.c", "func_80100000", + next(r for r in sc.scan_text(SINKFIX.replace(" base = 2;\n", ""), "src/fx/s.c", + shared_defs=None) if r["form"] == "def")): + fail("R15 must refuse a variable one arm does not set") + # control 3: the brace walk itself — a `} else if (…) {` line nets to zero braces and must still close its arm + ch = if_chains([sc.mask_text(l) for l in SINKFIX.split("\n")], d15["line"], d15["end"] - 1) + if len(ch) != 1 or len(ch[0][2]) != 3: + fail(f"if_chains must see three arms in the fixture, got {ch}") + # 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 097e9108f..4971dc03c 100644 --- a/decomp-architect/corpus/tools/P10/delever_search.py +++ b/decomp-architect/corpus/tools/P10/delever_search.py @@ -82,15 +82,18 @@ FAMILIES = { # lane B's map (`.run/P36/engine/residual_moves.md`, S101, gcc 2.7.2 source): the caller-saved swap is decided in # local-alloc's block_alloc/combine_regs by which dying pseudo the operand ties to — the temp inlined/introduced first; a # constant-operand commutative swap is undone by fold (fold-const.c) and only a var/var swap can reach the allocator. - "REG-caller": ("R6", "R8", "R5", "R10", "R12", "R14", "R13", "R3", "R7", "R9", "R2", "R4"), + # 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"), # 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", "R12", "R7", "R9", "R10", "R14", "R13", "R5"), - "REG-mixed": ("R6", "R2", "R5", "R10", "R4", "R3", "R8", "R12", "R13", "R14", "R7", "R9"), + "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"), # 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", "R6", "R8", "R3", "R7", "R5", "R13", "R9", "R10", "R2", "R4"), + "COUNT": ("R12", "R14", "R15", "R6", "R8", "R3", "R7", "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", "R2", "R4"), + "ORDER": ("R9", "R7", "R13", "R3", "R6", "R8", "R5", "R12", "R14", "R10", "R15", "R2", "R4"), "MIXED": dl.ALL_FAMILIES, "OTHER": dl.ALL_FAMILIES, } @@ -839,8 +842,11 @@ 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}") - if family_key(c) != "REG-caller" or FAMILIES["REG-caller"][0] != "R6" or "R5" not in FAMILIES["REG-caller"][:3]: - fail(f"REG-caller family wrong: {family_key(c)} {FAMILIES['REG-caller'][:3]}") + # 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]}") # 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 c8c9ee1c4..4ad08f810 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()`. | +| | `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 --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 bb965646d..f0742e2d4 100644 --- a/tools/delever.py +++ b/tools/delever.py @@ -2079,7 +2079,140 @@ def param_copies(text, tu, fn, d_): return out -ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14") +def if_chains(masked, lo, hi): + """[(chain_lo, chain_hi, [(arm_lo, arm_hi)])] — every brace-form if / else-if / else chain whose lines lie in + masked[lo:hi] at the body's own depth. `chain_hi` is the line index just past the chain's last `}`; an arm span is the + lines strictly inside that arm's braces. Depth is counted on the MASKED text, so a brace in a string or a comment is + not a brace. Chains that do not open a brace on the `if` line (a one-statement `if` without braces) are skipped: the + sink rewrites arm bodies and needs somewhere to put the statement.""" + out = [] + i = lo + while i < hi: + s_ = masked[i].strip() + if not (s_.startswith("if") and re.match(r"^if\s*\(", s_) and s_.endswith("{")): + i += 1 + continue + arms, depth, arm_start, j = [], 0, i + 1, i + while j < hi: + nxt = masked[j].strip() + closes = masked[j].count("}") + # the CLOSES are counted before the OPENS: on a `} else if (…) {` line the two net to zero, and a depth + # counter that adds both at once never comes back to zero — the chain then looks like one unterminated arm. + if j > i and depth - closes == 0: # this arm's `}` is on line j + arms.append((arm_start, j)) + m = re.match(r"^\}\s*else\b(.*)$", nxt) + if m and m.group(1).strip().endswith("{"): # `} else {` or `} else if (…) {` + arm_start, depth, j = j + 1, 1, j + 1 + continue + if nxt == "}" and j + 1 < hi and re.match(r"^else\b", masked[j + 1].strip()) \ + and masked[j + 1].strip().endswith("{"): + arm_start, depth, j = j + 2, 1, j + 2 # the `else …{` on its own line + continue + break + depth += masked[j].count("{") - closes + j += 1 + if len(arms) >= 2: + out.append((i, j + 1, arms)) + i = j + 1 + else: + i += 1 + return out + + +def sink_merges(text, tu, fn, d_): + """[(description, candidate text)] — R15: the statement AFTER an if/else chain sunk into every arm, and the variables + it consumed deleted. `if (c) { v = e1; } else { v = e2; } w = f(v);` -> `if (c) { w = f(e1); } else { w = f(e2); }`. + + T7 agent a1's crack of func_80156044 (2026-09-10, 130 bodies, the rank-1 head class six rung-G runs left at best 1). + The mechanism is a REGISTER move, not a scheduling one: a variable set in every arm and read after the merge is a + CROSS-BLOCK pseudo, so (i) local-alloc never makes a quantity for it (`local-alloc.c:472`, `next_qty` reset per block + at `:517`) and each arm holds two quantities, which is `block_alloc`'s unrolled `case 2` at `local-alloc.c:1499-1502` + — one `qty_compare` (`:1578-1596`), higher density first; sinking makes it a third block-local quantity and + `case 3` at `: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 (ii) while it is a global allocno it can inherit a copy + preference from whatever the merge statement's result is passed to (`set_preference` `global.c:1535+`, merged by + `expand_preferences` `global.c:781-825`, overriding first-fit at `global.c:1034-1067`) — sinking removes it from + `global.c` entirely. Read the whole reading in `.run/P36/agents/ov_SC04_011__func_80156044/mechanism.md`. + + Applicability is checked, not assumed: every consumed variable must be assigned exactly once in EVERY arm by a simple + statement, must appear in the merge statement, and must occur nowhere else in the function (declaration + one + assignment per arm + its uses in the merge statement is its whole census) — otherwise the rewrite would change what + the code reads. The bytes remain the correctness proof (a rewrite that changes behaviour simply DIFFERS).""" + lines = text.split("\n") + masked = [sc.mask_text(l) for l in lines] + lo, hi = d_["line"], d_["end"] - 1 + whole = sc.mask_text("\n".join(lines[d_["line"] - 1:d_["end"]])) + out = [] + for c_lo, c_hi, arms in if_chains(masked, lo, hi): + j = next((k for k in range(c_hi, hi) if masked[k].strip()), None) + if j is None or not simple_stmt(masked[j]): + continue + merge_masked = masked[j] + eq = re.search(r"(?+\-*/%&|^~])=(?!=)", merge_masked) + if not eq: # only an assignment merges arm values + continue + rhs = merge_masked[eq.end():] + # every variable assigned exactly once, by a simple statement, in EVERY arm + per_arm = [] + for a_lo, a_hi in arms: + got = {} + for k in range(a_lo, a_hi): + if not simple_stmt(masked[k]): + continue + m = re.match(r"^\s*([A-Za-z_]\w*)\s*=\s*(.+);\s*$", masked[k]) + if not m: + continue + if m.group(1) in got: # assigned twice in one arm: not a single value + got[m.group(1)] = None + else: + got[m.group(1)] = k + per_arm.append(got) + common = set(k for k, v in per_arm[0].items() if v is not None) + for g in per_arm[1:]: + common &= set(k for k, v in g.items() if v is not None) + names_ = sorted(v for v in common if re.search(r"(?])%s(?![\w])" % re.escape(v), rhs)) + if not names_: + continue + decl = {} + for v in names_: + # the variable's whole census: one declaration, one assignment per arm, and its uses in the merge statement + uses = len(re.findall(r"(?])%s(?![\w])" % re.escape(v), whole)) + in_merge = len(re.findall(r"(?])%s(?![\w])" % re.escape(v), merge_masked)) + d_line = next((k for k in range(lo, hi) + if is_decl_line(masked[k].strip()) + and re.search(r"(?])%s(?![\w])\s*(?:=|;|,)" % re.escape(v), masked[k])), None) + if d_line is None or MULTI_DECL.match(masked[d_line]) or "=" in masked[d_line]: + decl[v] = None # a shared or initialised declaration: leave it standing + else: + decl[v] = d_line + if uses != len(arms) + in_merge + (1 if d_line is not None else 0): + names_ = None + break + if not names_: + continue + drop = {decl[v] for v in names_ if decl[v] is not None} | {j} + after = {} # line index -> the sunk statement to emit just after it + for a_idx, (a_lo, a_hi) in enumerate(arms): + stmt = lines[j].strip() + for v in names_: + k = per_arm[a_idx][v] + expr = re.match(r"^\s*[A-Za-z_]\w*\s*=\s*(.+);\s*$", lines[k]).group(1).strip() + stmt = re.sub(r"(?])%s(?![\w])" % re.escape(v), "(" + expr + ")", stmt) + drop.add(k) + tail = a_hi - 1 # the arm's LAST BODY line (a_hi is its `}`) + ref = lines[tail] if lines[tail].strip() else lines[a_lo] + after[tail] = ref[:len(ref) - len(ref.lstrip())] + stmt + cand = [] + for k, l in enumerate(lines): # a dropped line may still be the one we append after + if k not in drop: + cand.append(l) + if k in after: + cand.append(after[k]) + out.append((f"sink @{j + 1} ({','.join(names_)})", "\n".join(cand))) + return out + + +ALL_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R13", "R14", "R15") RUNG_R_FAMILIES = ("R2", "R3", "R4", "R5", "R6", "R7") # the free sweep's set (R8/R9 are the search engine's until measured) @@ -2184,6 +2317,9 @@ def recipe_candidates(text, tu, fn, names, limit=24, rng=None, cap=40, blocks=Tr if "R13" in fam: for desc, cand in reassociations(text, tu, fn, d_): out.append(("R13", desc, cand)) + if "R15" in fam: + for desc, cand in sink_merges(text, tu, fn, d_): + out.append(("R15", 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)) @@ -2894,6 +3030,53 @@ def selftest(): fail(f"remap_body produced {got!r} ({why})") if remap_body(exb, exa, "void func_80300000(void) { D_80400000 = 0; }")[0] is not None: fail("remap_body must refuse a sibling with a different token count") + # R15, the sink (T7 agent a1's crack of func_80156044, 2026-09-10): the merge statement pushed into every arm and the + # variables it consumed deleted. The three controls are the ones the applicability test exists for. + SINKFIX = ("void func_80100000(int c) {\n" + " s32 base;\n" + " u8 *row;\n" + "\n" + " if (c) {\n" + " base = 1;\n" + " } else if (c == 2) {\n" + " base = 2;\n" + " } else {\n" + " base = 3;\n" + " }\n" + " row = (u8 *)(base + 4);\n" + " use(row);\n" + "}") + d15 = next(r for r in sc.scan_text(SINKFIX, "src/fx/s.c", shared_defs=None) + if r["form"] == "def" and r["name"] == "func_80100000") + got15 = sink_merges(SINKFIX, "src/fx/s.c", "func_80100000", d15) + if len(got15) != 1 or "sink" not in got15[0][0] or "base" not in got15[0][0]: + fail(f"R15 must find one sink in the three-arm fixture, got {[g[0] for g in got15]}") + else: + c15 = got15[0][1] + want15 = ["row = (u8 *)((1) + 4);", "row = (u8 *)((2) + 4);", "row = (u8 *)((3) + 4);"] + if not all(w in c15 for w in want15): + fail(f"R15 must sink the merge statement into every arm: {c15!r}") + if "s32 base;" in c15 or "base = 1;" in c15: + fail("R15 must delete the consumed variable's declaration and its per-arm assignments") + if c15.count("row = ") != 3 or "row = (u8 *)(base + 4);" in c15: + fail("R15 must remove the merge statement itself") + if "u8 *row;" not in c15: + fail("R15 must keep the declaration of the variable the merge statement ASSIGNS") + # control 1: a consumed variable read anywhere else is not sinkable (its census would not add up) + if sink_merges(SINKFIX.replace(" use(row);", " use(row + base);"), "src/fx/s.c", "func_80100000", + next(r for r in sc.scan_text(SINKFIX.replace(" use(row);", " use(row + base);"), "src/fx/s.c", + shared_defs=None) if r["form"] == "def")): + fail("R15 must refuse a variable that is read after the merge statement") + # control 2: a variable not set in EVERY arm is not sinkable + if sink_merges(SINKFIX.replace(" base = 2;\n", ""), "src/fx/s.c", "func_80100000", + next(r for r in sc.scan_text(SINKFIX.replace(" base = 2;\n", ""), "src/fx/s.c", + shared_defs=None) if r["form"] == "def")): + fail("R15 must refuse a variable one arm does not set") + # control 3: the brace walk itself — a `} else if (…) {` line nets to zero braces and must still close its arm + ch = if_chains([sc.mask_text(l) for l in SINKFIX.split("\n")], d15["line"], d15["end"] - 1) + if len(ch) != 1 or len(ch[0][2]) != 3: + fail(f"if_chains must see three arms in the fixture, got {ch}") + # 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 097e9108f..4971dc03c 100644 --- a/tools/delever_search.py +++ b/tools/delever_search.py @@ -82,15 +82,18 @@ FAMILIES = { # lane B's map (`.run/P36/engine/residual_moves.md`, S101, gcc 2.7.2 source): the caller-saved swap is decided in # local-alloc's block_alloc/combine_regs by which dying pseudo the operand ties to — the temp inlined/introduced first; a # constant-operand commutative swap is undone by fold (fold-const.c) and only a var/var swap can reach the allocator. - "REG-caller": ("R6", "R8", "R5", "R10", "R12", "R14", "R13", "R3", "R7", "R9", "R2", "R4"), + # 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"), # 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", "R12", "R7", "R9", "R10", "R14", "R13", "R5"), - "REG-mixed": ("R6", "R2", "R5", "R10", "R4", "R3", "R8", "R12", "R13", "R14", "R7", "R9"), + "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"), # 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", "R6", "R8", "R3", "R7", "R5", "R13", "R9", "R10", "R2", "R4"), + "COUNT": ("R12", "R14", "R15", "R6", "R8", "R3", "R7", "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", "R2", "R4"), + "ORDER": ("R9", "R7", "R13", "R3", "R6", "R8", "R5", "R12", "R14", "R10", "R15", "R2", "R4"), "MIXED": dl.ALL_FAMILIES, "OTHER": dl.ALL_FAMILIES, } @@ -839,8 +842,11 @@ 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}") - if family_key(c) != "REG-caller" or FAMILIES["REG-caller"][0] != "R6" or "R5" not in FAMILIES["REG-caller"][:3]: - fail(f"REG-caller family wrong: {family_key(c)} {FAMILIES['REG-caller'][:3]}") + # 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]}") # 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":