[AUTOMATED] feat(p8): orchain - returndup must not split the operand chain of a short-circuit (DIV-69) - #285
Conversation
…ections it forced Round 4 targeted the slice the campaign had not attacked: the O0 cases where IDA's GED is 0 and kuna's is not. This lands the reusable parts only -- the instrument, the analysis, and the two documentation errors the round proved. The per-case triage records are deliberately not committed. ## The instrument `scripts/decbench/optsweep.py` -- a bidirectional corpus-scale A/B for any single option flip. `rescore` prices ONE mined case, which is the wrong unit for anything that changes a default: the mining pipeline only ever produces one direction of evidence, mining losses and never pricing the wins the same change destroys. At O0 the net deficit against IDA is 129 functions while every candidate cluster is 300-430 wide, so a change that flips 400 each way moves the scoreboard by zero and nothing upstream of this tool would show it. It reports both directions of movement by name, the summed GED delta per population, a per-project and per-architecture breakdown, and the standing-requirement-8 changed set, with three harness controls built in: functions the pass never fires on must score identically in both arms, banner-only diffs must score identically, and the ON arm must reproduce the published column. A run that fails any of them is not a measurement. Cross-validated against the ad-hoc harness that produced the returndup measurement: 157 of 157 functions agree in both arms on O0 zlib. ## Correction 1: the scored column is `aggressive`, not `reliable` `docs/decbench-loop.md` has been telling every triage agent that the benchmark injected `option listing on`, the equivalent of `--mode reliable`. It does not. `decbench/decompilers/raw/kuna_raw.py`'s `_build_command` builds `kuna decompile-all <bin> --json --max-fn-seconds 120`, appends `--mode` only when `DECBENCH_KUNA_MODE` is set, and appends `--option` only from `config.extra_options`, which is empty. So the benchmark ran `--mode auto` = aggressive on 768 of 803 binaries. An aggressive-only regression is therefore INSIDE the published number, and any triage that assumes `reliable` mis-attributes it. ## Correction 2: DIV-54's evidence never saw O0 `docs/decbench/returndup-o0-measurement.md` is the A/B that PR #246 did not run. #246 flipped `returndup` default-ON on 52,862 functions at O2 and O2-noinline; its table has two rows and neither is O0. Re-run over all three levels, 85,195 scored functions: O0 -192 GED-perfect, O2 +362, O2-noinline +470, total +640. Keep the default -- flipping back buys 192 at O0 and sells 832. But one structural sub-shape carries essentially all the O0 harm and almost none of the O2 benefit: the split that permanently blocks `rule_block_or` from folding a short-circuit chain (-665 at O0, +15 and +20 at the optimized levels). That is the gate PR #285 implements. ## The grounding `docs/decbench/round4-grounding.md` locates the gap before triaging anything. O0 is the only optimisation level where IDA holds more perfect functions than kuna; at O2 and O2-noinline kuna's lower percentage is a denominator effect, since it is scored on 2,343 more functions and holds more perfect at both. Total perfect: kuna 33,146, IDA 32,838. `docs/decbench/features-round4.md` is the ranked menu with its killed list. Zero symptoms fell to the adversarial pass, and four of six filed root causes were materially overturned -- continuing the campaign's most reliable finding exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
…ections it forced (#279) Round 4 targeted the slice the campaign had not attacked: the O0 cases where IDA's GED is 0 and kuna's is not. This lands the reusable parts only -- the instrument, the analysis, and the two documentation errors the round proved. The per-case triage records are deliberately not committed. ## The instrument `scripts/decbench/optsweep.py` -- a bidirectional corpus-scale A/B for any single option flip. `rescore` prices ONE mined case, which is the wrong unit for anything that changes a default: the mining pipeline only ever produces one direction of evidence, mining losses and never pricing the wins the same change destroys. At O0 the net deficit against IDA is 129 functions while every candidate cluster is 300-430 wide, so a change that flips 400 each way moves the scoreboard by zero and nothing upstream of this tool would show it. It reports both directions of movement by name, the summed GED delta per population, a per-project and per-architecture breakdown, and the standing-requirement-8 changed set, with three harness controls built in: functions the pass never fires on must score identically in both arms, banner-only diffs must score identically, and the ON arm must reproduce the published column. A run that fails any of them is not a measurement. Cross-validated against the ad-hoc harness that produced the returndup measurement: 157 of 157 functions agree in both arms on O0 zlib. ## Correction 1: the scored column is `aggressive`, not `reliable` `docs/decbench-loop.md` has been telling every triage agent that the benchmark injected `option listing on`, the equivalent of `--mode reliable`. It does not. `decbench/decompilers/raw/kuna_raw.py`'s `_build_command` builds `kuna decompile-all <bin> --json --max-fn-seconds 120`, appends `--mode` only when `DECBENCH_KUNA_MODE` is set, and appends `--option` only from `config.extra_options`, which is empty. So the benchmark ran `--mode auto` = aggressive on 768 of 803 binaries. An aggressive-only regression is therefore INSIDE the published number, and any triage that assumes `reliable` mis-attributes it. ## Correction 2: DIV-54's evidence never saw O0 `docs/decbench/returndup-o0-measurement.md` is the A/B that PR #246 did not run. #246 flipped `returndup` default-ON on 52,862 functions at O2 and O2-noinline; its table has two rows and neither is O0. Re-run over all three levels, 85,195 scored functions: O0 -192 GED-perfect, O2 +362, O2-noinline +470, total +640. Keep the default -- flipping back buys 192 at O0 and sells 832. But one structural sub-shape carries essentially all the O0 harm and almost none of the O2 benefit: the split that permanently blocks `rule_block_or` from folding a short-circuit chain (-665 at O0, +15 and +20 at the optimized levels). That is the gate PR #285 implements. ## The grounding `docs/decbench/round4-grounding.md` locates the gap before triaging anything. O0 is the only optimisation level where IDA holds more perfect functions than kuna; at O2 and O2-noinline kuna's lower percentage is a denominator effect, since it is scored on 2,343 more functions and holds more perfect at both. Total perfect: kuna 33,146, IDA 32,838. `docs/decbench/features-round4.md` is the ranked menu with its killed list. Zero symptoms fell to the adversarial pass, and four of six filed root causes were materially overturned -- continuing the campaign's most reliable finding exactly. Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…chain of a short-circuit (DIV-69) DIV-54 flipped `returndup` on evidence from two of the three optimisation levels. Extended to all three (795 slices / 85,195 scored functions) the pass is +640 GED-perfect corpus-wide but -192 at -O0, and one sub-shape carries essentially all of that harm and almost none of the optimized benefit: the split that permanently blocks `CollapseStructure::rule_block_or`. `returndup` gives every predecessor of a shared bare-epilogue RETURN its own private return. For a guard clause that recovers the source; but the operand blocks of a short-circuit expression are predecessors of that same block, and the shared out-target IS ruleBlockOr's entire precondition. ActionReturnDup runs in fullloop's `returnsplit` group and collapse_conditions runs later, so the fold is lost for good and one source `return a || (b && (c || d));` prints as a five-return constant-guard cascade (iproute2 ip::sci_complete, GED 21 against 0). New option `orchain on|off` (default ON, DIV-69, ELEM 4122). `kuna_orchain.rs (shortcircuit_shared_targets)` replays collapse_conditions read-only on the bblocks CFG, mirroring ruleBlockOr's own admission test rather than approximating it (one in-edge, two out-edges, not a switch, not BlockBasic::isComplex, a shared out-target, sibling's other target is not the head), and `returndup_apply` declines the whole function's splits when one of its own candidates is a recorded target. It also protects a block joining two or more single-exit arms that are themselves fold targets, because the first returndup invocation of the fullloop still sees the constant-materialisation blocks in between. Sweep over the same corpus (base = shipped defaults at merge-base 82dd39a): O0 +611 GED-perfect / -2,816 GED, O2 -13 / +1,232, O2-noinline -15 / +617 -- +583 net and -967 aggregate, 781 functions to perfect against 198 off (3.94 : 1). That is 92% of the +630 the post-hoc "a short-circuit was lost" signal prices as a ceiling, at a smaller optimized-level cost than the ceiling itself (-28 vs -35). Predicate fidelity to that signal: 2,933 of 2,951 O0 positives recognised (99.4% recall) at 126 further firings (95.9% precision); 94.2% of firings reproduce the `option returndup off` body byte for byte. Default-ON rather than an `aggressive` preset member because `returndup` is a shipped default in every mode and 45% of the corpus (38,216 of 85,195 functions, 36 of 795 slices) is over the 500 KiB `auto` threshold and runs `reliable`, where a preset-only gate never runs -- measured: with the gate in the preset only, the no-option arm reproduced the ungated body on iproute2 ip (76 functions), bash (366), sshd (201). 675/675 PARITY OK with the raw flip, zero assertion movement and no per-test opt-out; stages 415/415 with the new two-pass tests/stages/ghdec-orchain.xml; rust-test 4,540 passed / 0 failed; check-spec OK lenient + strict; catalog --check OK. Speed worst +1.01% (two of four probes are net speedups). Rebased onto #280 (funcboundflow, DIV-67): every shared counter re-derived from the built artifact on the rebased tree (97 settables, tiers 20/43/34, xml corpus 195), and DIV-68 was renumbered to 69 after Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
…e stages baseline Rebased onto #278/#281/#283/#284. Every shared counter re-derived from the BUILT artifact rather than by arithmetic: live catalog 101, phases.toml 101, catalog_bytecompat.rs 101, kuna_phases/tests.rs 101 / tiers (21, 45, 35) / `},\n` 100, phase_catalog.json 101 (recaptured), docs/options.md 101 (regenerated), xml.rs corpus 199, kuna-catalog.xml structure-recovery 26. Stages baseline re-recorded on the merged base: 425 -> 433, +8 orchain keys and the #7 text update, nothing removed. DIV-69 re-checked against origin/main immediately before pushing: main's highest row is DIV-68 (#277), so 69 stands unrenumbered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
e76e049 to
8415155
Compare
[AUTOMATED] Measurement staleness after the five-PR merge trainThis PR landed last in a train of five: #278 What each figure was measured on
Which numbers are now stale, and which are notThe default-off/default-on split decides this, and it does not fall on "whichever landed last".
Follow-upOne combined re-measure with None of this is a correctness question. All four gates were re-run on each rebased tree in the train and were green, and every shared counter was re-derived from the built artifact rather than by arithmetic — which caught three that git had auto-merged at the wrong value with no conflict marker. On this PR specifically: 675/675 datatest assertions PARITY OK with the raw default-ON flip and no |
…l the build `ElementId`'s `PartialEq` is an id comparison (ported from C++), so two options sharing an id compare EQUAL and the first code that dispatches on one takes the wrong branch. Three ids were allocated twice on main: 4110 iteregion / returndup (one commit, 2026-07-10) 4122 funcboundflow / orchain (a day apart, #280 and #285) 4132 varargstackargs / linuxsyscall (THIS ROUND -- two concurrent builders) The later of each pair is renumbered: returndup 4135, orchain 4136, linuxsyscall 4134. Renumbering is free here because nothing consumes any of them -- `grep ELEM_ITEREGION` etc. finds only the declaration -- which is also precisely why three of these landed unnoticed. The hazard is the first consumer, not today's output; that is a reason to fix it now rather than a reason not to. Both 4122 sources carried a comment reasoning "the next free id above itecondlist's 4121 is 4122". Two agents grepped for the high-water mark a day apart and got the same answer. 4132 is the same race inside a single round of this pipeline. Reading the high-water mark by hand cannot be made safe, so the comments now point at the deriving tool instead. `counters.py --check` already FOUND duplicates and printed them -- while exiting 0. That is what let all three land, and it is fixed here: duplicates are now a `FAIL` line and a non-zero exit, verified by injecting one. The check is wired into the parity-gates CI job, which is the cheap job that runs on every event, so this cannot recur silently. Gates on the renumbered tree: make test 675/675 PARITY OK · make test-stages PARITY OK · make test-cli 10/10 · make check-spec OK · kuna catalog --check OK · counters --check clean (38 ids, 0 duplicates) · smoke 86/86. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…iage, and six correctness options (#367) * [AUTOMATED] fix(p4): varargstackargs + calleearity — two ways a call argument was recovered and then discarded RE-friction round 1 filed four "call and argument recovery" observations across three challenges. They are NOT one bug; reproduced and root-caused separately, they are three causes, two of which are call-site argument loss with different owners. Both are fixed here, each behind its own option, each default-ON on a 0/675 ablation and a classified whole-corpus sweep. varargstackargs (DIV-101, P4 active-input-trial-scoring, ElementId 4132) 68149b8a `_main`: scanf("%d") loses its destination and three printf calls lose the buffer their %s consumes, and the local scanf writes ends up read but never written. checkInputTrialUse scores the [sp+0] trial ACTIVE; ParamListStandard::fillinMap deactivates it again, because on AArch64 x0-x7 and the outgoing stack area are ONE resource section and its two positional rules read the seven registers Apple's arm64 ABI structurally leaves empty between the fixed parameter and the first vararg as the end of the argument list. The section is now cut in two at its first stack trial, gated on the callee being variadic and on the stack area starting at callee-relative offset 0. Sweep: 1 changed function of 3857. calleearity (DIV-102, P4 trial-finalization, ElementId 4133) 69a54bd7 0x1400024a0: the same allocator wrapper renders sub_140008160(0x28) at one site and sub_140008160() thirty bytes later, at the site where the size is also the operand of an MSVC operator new[] overflow guard -- only_op_use rejects the trial on its CPUI_CBRANCH descendant. Relaxing that rejection would fabricate an argument at every `test rcx,rcx; jz; call`, so buildInputFromTrials instead reconciles a call whose argument list came out EMPTY with a sibling call to the same callee whose list is already final. The storage is recorded on the call spec, because a CALL op's inputs carry argument values (0x28 is a constant) and the trials that knew the location are deleted one statement later. Sweep: 25 changed of 3857, no regression; the first cut ("same callee, same arity") changed 43 and produced Sleep(200,0), which is where the empty-list condition comes from. Its acceptance probe flips. Gates: make test 675/675 PARITY OK (docs/baseline.json untouched) · make test-stages 578/578 PARITY OK · make rust-test 334 targets / 5018 passed · make check-spec OK · kuna catalog --check OK · counters re-derived, no drift. Speed flat for both, measured on interleaved child CPU time with a null control because the box carried load average 11-14 throughout. Bundles: docs/features/varargstackargs/, docs/features/calleearity/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * [AUTOMATED] feat(cli+analysis): strings, disassemble, function triage, and four correctness options Round 1 of the RE-friction loop: three codex testers on nine crackmes filed 26 observations; 23 passed the two-arm gate. This lands the fixes for them. Interface (Track T) -- five testers across four challenges independently asked for a string inventory, three for disassembly, two for whole-binary triage: kuna strings VMA + section + owning function, which strings(1) cannot give kuna disassemble named function, bare address, or range; --json carries bytes kuna functions --summary --filter --reachable-from --min-size --sort --limit (211 KB PE: 174 KB of JSON -> 2.8 KB that answers "start where") Correctness (Track D), each behind a named option with a two-pass stage test: unmappedentry ON a direct CALL target outside every executable range became a DiscoveredFunction; the instruction worklist had already refused to decode it. 250-binary sweep: 150 phantom entries removed, 0 added, 1 function's C changed and it improves. entrymainproto ON main typed void(void); parameters are recovered from the callee's own body and main never reads its argument registers, so the in-image CRT call site is read instead. linuxsyscall OFF int 0x80 rendered as an indirect call through swi(0x80) switchselector OFF loweredswitch detects a cascade on the simplified graph and installs the BRANCHIND on re-lifted raw p-code it cannot re-find the selector in -- and commits regardless, so a Win32 dialog dispatcher rendered switch(0), every case dead tests/cli grows 5 -> 10 promoted acceptance probes, run by make test-cli in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * [AUTOMATED] chore: re-derive shared counters after the merge, and close two guard gaps Counters after merging both lanes: 133 settables (30 core / 54 transform / 49 analysis), stage corpus 228, catalog records 132. Neither branch's number: this branch said 131 and callargrecovery said 129, both having bumped from 127. Every site was re-derived from the live tree with `counters.py --fix`, and phase_catalog.json / docs/options.md / docs/baseline-stages.json were REGENERATED rather than merged. Two guards were blind and are now not: - `counters.py` had no site for the catalog-record assert in kuna_phases/tests.rs (`json.matches("},\n").count()`), so the merge left it at 130 against a truth of 132 and only `make rust-test` caught it. Added as its own site with a `settables_minus_one` derivation -- a regex pinned to `settables` would have written the wrong number. Verified it catches injected drift. - `smoke.sh` asserted the literal strings `"settables": 127` and `"corpus_files": 222`, so every option that lands broke the smoke. That is a false alarm that teaches people to ignore the smoke, and it never caught a real drift because `counters --check` is what does that. It now asserts the mechanism: the derivation is plausible and every hard-coded site agrees with it. Gates: make test 675/675 PARITY OK (baseline.json untouched) · make test-stages 594/594 PARITY OK · make rust-test 339 targets, 5203 passed, 0 failed · make check-spec OK · kuna catalog --check OK · make test-cli 10/10 · tools/repipe/smoke.sh 72/72. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * [AUTOMATED] chore(re-needs): file round 1 -- 8 closed, 6 carried into round 2 The backlog was empty because round 1's observations were gated straight from the reports; the records are what round 2's tester brief is rendered from, so without them the next testers would re-file "kuna has no strings command" against a kuna that now has one. Closure is the VERIFIED state, from re-running both arms on the merged build: 8 closed (12 of the 23 gated observations, several of them duplicates of each other), 6 carried open. `void-callee-spurious-arg` is carried OPEN despite its probe no longer matching -- the probe pinned `_secret_function(v2);` and the build now emits `_secret_function(v3);`, which is the same defect with one renumbered local. Also fixes the renderer: `_recently_shipped` printed each closed need's TITLE, which states the problem ("kuna cannot list strings"), under a heading that says "newly available capabilities" -- telling the tester the exact opposite of the truth. It now renders the `Shipped:` line the record carries, so the brief names the flag or subcommand to actually exercise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * [AUTOMATED] docs(re-pipeline): record that a CONFLICTING PR runs no gates and looks green Round 1's own PR hit this: six green CodeQL checks, the Tests workflow with no run object at all, and `gh pr checks` presenting an all-green list. GitHub does not dispatch `pull_request` workflows while it cannot compute the merge commit. The `missing` arm of open_pr.sh --merge -- written for a renamed check -- is what stands between that and an auto-merge of wholly ungated code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * [AUTOMATED] fix: three duplicate ElementIds, and make a duplicate fail the build `ElementId`'s `PartialEq` is an id comparison (ported from C++), so two options sharing an id compare EQUAL and the first code that dispatches on one takes the wrong branch. Three ids were allocated twice on main: 4110 iteregion / returndup (one commit, 2026-07-10) 4122 funcboundflow / orchain (a day apart, #280 and #285) 4132 varargstackargs / linuxsyscall (THIS ROUND -- two concurrent builders) The later of each pair is renumbered: returndup 4135, orchain 4136, linuxsyscall 4134. Renumbering is free here because nothing consumes any of them -- `grep ELEM_ITEREGION` etc. finds only the declaration -- which is also precisely why three of these landed unnoticed. The hazard is the first consumer, not today's output; that is a reason to fix it now rather than a reason not to. Both 4122 sources carried a comment reasoning "the next free id above itecondlist's 4121 is 4122". Two agents grepped for the high-water mark a day apart and got the same answer. 4132 is the same race inside a single round of this pipeline. Reading the high-water mark by hand cannot be made safe, so the comments now point at the deriving tool instead. `counters.py --check` already FOUND duplicates and printed them -- while exiting 0. That is what let all three land, and it is fixed here: duplicates are now a `FAIL` line and a non-zero exit, verified by injecting one. The check is wired into the parity-gates CI job, which is the cheap job that runs on every event, so this cannot recur silently. Gates on the renumbered tree: make test 675/675 PARITY OK · make test-stages PARITY OK · make test-cli 10/10 · make check-spec OK · kuna catalog --check OK · counters --check clean (38 ids, 0 duplicates) · smoke 86/86. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * [AUTOMATED] chore(re-needs): seed round 2 with the analysis-override gap A source survey of what an agent can OVERRIDE (as opposed to query) found that kuna has a working console vocabulary for exactly the levers an obfuscated binary needs -- `map function`, `map address`, `parse line`, `override flow`, `rename`, `retype`, `map param/return`, `comment instruction` -- and the `kuna` binary can reach none of it. `decompile.rs::build_script` emits a fixed vocabulary and there is no `kuna console`, no `--script` and no passthrough. Three further findings, all verified by running the commands: - `function F spans [start,end)` does not exist anywhere. Extent is derived in funcextent.rs with no override, and phases.toml has no subphase for function-entry identification at all. - The structuring overrides phases.toml advertises in its own `exposure` fields are mostly stubs: `force goto`, `override jumptable` and `structure blocks` all return `engine integration not yet ported`. Only `override flow` works. - `--kassert` is the interface that was meant to carry this, and only its `naming-policy` and `Option` dispatch arms do real work. Four needs seeded, top of the backlog. They are NOT tester-filed and say so; round 2 confirms the demand. The hypothesis section records the thing a builder must not assume: the cheap half is exposure and the expensive half is the stubs, and a scriptable console is a different product from flags an agent composes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
The finding
returndup(P8ActionReturnDup, DIV-54) was flipped default-ON on an ablation over O2and O2-noinline only. Extended to all three levels -- 795 slices, 85,195 scored
functions -- it is +640 GED-perfect corpus-wide but -192 at -O0, and one structural
sub-shape carries essentially all of the O0 harm and almost none of the optimized
benefit: the split that permanently blocks
CollapseStructure::rule_block_or.returndupgives every predecessor of a shared bare-epilogue RETURN block its own privatereturn. For a guard clause that recovers the source shape; but the operand blocks of a
short-circuit expression are predecessors of that same block, and the shared out-target
is
ruleBlockOr's entire precondition (bl -> {orblock, clauseblock},orblock -> {clauseblock, X}).ActionReturnDupruns in fullloop'sreturnsplitgroupand
collapse_conditionsruns later, so the fold is lost for good:(
iproute2 ip::sci_complete; the source is one booleanreturn.)The predicate, and its measured fidelity
The measurement identified this shape post hoc, by diffing the two arms' bodies for
&&/||operators the OFF body has and the ON body does not. That is an outcome, not animplementable precondition, so +630 was a ceiling, not a promise, and the fidelity of a
real pre-split predicate to it is the first thing this PR has to publish.
p8_structure/kuna_orchain.rs (shortcircuit_shared_targets)is a read-only replay ofcollapse_conditionson the bblocks CFG, run insidereturndup_applybefore the firstnode_split. It mirrorsruleBlockOr's own admission test rather than approximatingit: two condition nodes fuse when the sibling has exactly one in-edge, has two out-edges,
is not a switch block, is not
BlockBasic::isComplex(the samebb_is_complexverdict therule reads, off the same op lists), and shares one out-target with the head whose other
target is not the head. Each fuse records the shared target and rewrites the head's
out-targets, so a chain collapses inside-out as the real fixpoint does. Three upstream
tests are omitted because they cannot hold at this point in the schedule (no goto has been
elected --
select_gotoruns later; and a back edge into a single-in-edge block would makethat block unreachable). It additionally protects a block joining two or more single-exit
arms that are themselves fold targets: before the constants reach the epilogue phi the arms
are one bare
v = Kblock each, which is the graph the firstreturndupinvocation ofthe fullloop sees, and without that clause the archetype recovers its
||chain but losesthe 0/1 select
itebooleanre-rolls.Fidelity to the post-hoc proxy, over 33,804 O0 marker functions:
sc_off > sc_on)returndup offbodyThe 18 misses are one shape: a degenerate two-out block (both edges to the same target)
between two operands, removed later in P8 than this replay runs.
The three-level sweep -- this implementation, not the ceiling
Base arm = shipped defaults at merge-base
82dd39a7; variant arm = the gate. decbench'spopulation, names and
GEDMetric.Both directions, by name (
docs/features/orchain/sweep-o0/moved.csvlists every one):McNemar z = +18.63. Per architecture none negative: x86-64 +530, arm +47, pe-x86 +6.
Per project 34 of 37 net-positive; the three that are not are bzip2 -3, libselinux -3,
kmod -2. Biggest moves to perfect:
iproute2 ip::sci_complete21 -> 0,gzip make_ofname20 -> 0,
bash skipname/wskipname17 -> 0,crazyflie sensorsFindBiasValue15 -> 0,cleanflight/betaflight nextArg14 -> 0 at all three levels -- that last is thefor-loop de-structuring DIV-54 recorded as its one net-negative cell, closed as a sideeffect. Biggest move off perfect:
coreutils factor::factor0 -> 12, DIV-54's flagshipwin, which is the honest cost: that source did write the guard cascade and nothing in the
binary says so.
Against the ceiling, and against the stated falsifier
The post-hoc counterfactual recomputed on this instrument (it reproduces the measurement's
published table exactly):
sc_off > sc_onreturndupoff entirely)The agreed stop condition was "below about +300 total GED-perfect, or negative at either
optimized level". +583 clears the first by a wide margin. On the second clause, stated
plainly rather than shaded: both optimized levels are slightly negative, -13 and -15 --
and so is the ceiling the measurement recommended building, by more (-15 and -20). A
literal reading of that clause falsifies the recommendation itself; the decision-relevant
comparison is implementation against ceiling, where this is better at both optimized levels
and delivers 92% of the perfect count. A reviewer who wants the strict reading has the
numbers to apply it.
Method
The two arms the corpus measurement scored are still on disk and this PR's baseline arm
reproduces its ON arm byte for byte on 33,804 of 33,804 O0 functions, so every function
whose gate-arm body equals one of those two takes that arm's score (exact -- GED is a
function of the body text, which is what optsweep's "unchanged bodies score identically"
control asserts); only the 312 hybrid bodies were Joern-scored fresh. Controls: 809
unchanged-body controls reproduced their cached score with 0 mismatches, and the O0 level
is not a projection at all -- a full independent
scripts/decbench/optsweep.pyrun overall 265 O0 slices reproduces its row digit for digit (
docs/features/orchain/sweep-o0/report.md),with all three of its built-in controls passing (29,325 unchanged bodies 0 differ, 30
slug-only diffs 0 differ, baseline agrees with the published verdict on 99.8%).
Standing-requirement-8 audit
Over all 7,018 changed bodies: the set of called functions is identical in 7,018 of
7,018; 2,883 of 3,059 O0 firings and 3,823 of 3,959 optimized-level firings are
byte-identical to the shipped
option returndup offrendering. Gotos: O0 -19/+14,optimized -12/+126 (the real cost, visible in O2's +1,232 aggregate GED).
while( true ): -365 / +53 -- the gate un-destructures seven loops for every one itcreates.
Why default-ON and not the
aggressivepresetA new default-off option normally joins
AGGRESSIVE_OVERRIDES. That is not enough here:36 of 795 slices are over the 500 KiB
autothreshold and runreliable, and they hold38,216 of the 85,195 scored functions (45%), including every one of the biggest winners.
Measured directly -- with the gate in the preset only, the no-option arm reproduced the
ungated body on
iproute2 ip(76 functions),bash(366) andopenssh sshd(201).returndupis a shipped default in every mode, so its narrowing has to be one too. Hencethe DIV row.
Speed
Whole-binary
decompile-all, arms interleaved and pinned to the same four cores, min of 9:example(O0, x86-64)ls(O0, x86-64)cf2.elf(O0, ARM Cortex-M)ip(O0, x86-64; min of 5, unpinned)Worst +1.01% against the 5% budget; two of four are net speedups, because the gate only
ever declines a
node_splitand the structurer then runs on a smaller graph.Gates (verbatim, on the rebased tree)
675/675 is reached with the raw flip -- zero assertion movement, no per-test opt-out and
no re-pin of
docs/baseline.json. The gate only ever declines areturndupsplit, andthe nine datatest files that pin the merged epilogue already carry
option returndup off.Rebase, counts and sibling PRs
Rebased onto
11f40f46(#280funcboundflow, DIV-67). Every shared counter re-derivedfrom the built artifact on the rebased tree, never by arithmetic: live catalog 97,
catalog_bytecompat.rs97,kuna_phases/tests.rs97 / tiers (20, 43, 34) /},\n96,phase_catalog.json97 (recaptured),docs/options.md97 (regenerated),xml.rscorpus195,
kuna-catalog.xmlstructure-recovery 24.docs/baseline-stages.jsonre-recorded onthe merged base rather than hand-merged. DIV number: 68 was renumbered to 69 after #277
claimed 68; re-checked against
origin/mainimmediately before pushing.Rebase drift was measured, not assumed: re-running both arms with the rebased binary over
5,777 sampled O0 functions moves 20 functions, identically in both arms (crazyflie
cf2,funcboundflow's own effect), so the deltas above are unaffected.Sequencing with the sibling P8 work. This PR does not touch
p8_structure/blockaction.rs, so there is no textual conflict with #281 (guardarm) or#283 (
loopcondhoist); it sharessubstrate/funcdata_block.rswith #282 but in adifferent function (
returndup_applyvsblock_if_flip_negated_guard). All four willconflict on the shared catalog counters and
docs/baseline-stages.json, which resolve tobase + all merged. There is an outcome interaction to price rather than assume: this
gate leaves a merged short-circuit condition where main leaves a guard cascade, which
changes the block shapes
rule_block_if_no_exitand its deferred scan then see, sowhichever of #281 / #283 / this lands second should re-measure rather than add the three
deltas. Each was measured independently against shipped
main.The motivating measurement (
docs/decbench/returndup-o0-measurement.md) and thescripts/decbench/optsweep.pyinstrument both land in #279; this PR references themand deliberately does not carry a copy.
docs/decbench/returndup-regression-triage.mdisextended, not replaced -- its "no discriminator exists" and "net-positive in every
subpopulation" conclusions are now scoped to the data they were measured on, and its
Conclusion-4 loop cell is closed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH