Skip to content

[AUTOMATED] feat(p8): guardarm - break the ruleBlockIfNoExit arm TIE by code layout (+297 GED-perfect / 32,339 O0 functions) - #281

Merged
mahaloz merged 2 commits into
mainfrom
feat/guardarm
Aug 11, 2026
Merged

[AUTOMATED] feat(p8): guardarm - break the ruleBlockIfNoExit arm TIE by code layout (+297 GED-perfect / 32,339 O0 functions)#281
mahaloz merged 2 commits into
mainfrom
feat/guardarm

Conversation

@mahaloz

@mahaloz mahaloz commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Closes symptom A of the round-4 ifnoexit cluster
(docs/decbench/features-round4.md Rank 2). The filed predicate there is
refuted (Killed K13) and is not what this ships.

Phase 1 — the decision trace, and its verdict

Three independent refuters asked for one experiment: a decision trace inside
CollapseStructure::rule_block_if_no_exit. It ships in this PR
(p8_structure/kuna_ifnoexit.rs + the KUNA_RS_DEBUG prints in
blockaction.rs), and the verdict is the deliverable, independent of the two
fixes.
Reproduce with
KUNA_RS_DEBUG=1 kuna decompile-all <bin> --addr 0x... — note kuna decompile
spawns decomp_dbg with piped stderr and swallows the trace, so decompile-all
is the surface that shows it.

Per candidate it prints bl's type / degree / start address, both arms'
size_in, size_out, is_decision_out and exit-leaf halt type (testing the
pcodeop_flags::noreturn bit, 0x1000000, not the return(#0x1:4) shape —
badinstruction/unimplemented/missing/halt are identical in shape),
whether bl is a live loop head, whether rule_block_while_do would fire right
now, the whole deferred-scan order, and the chosen i. The apply#N counter is
in every header because collapse_all runs twice per function.

1. Is A an arm predicate, a scan-order predicate, or both? — a pure arm
predicate, and only in a tie.
On dpkg-query control_list both guards are
in=1 out=0 dec=true on both arms, the rule takes i=0 in both, and the only
difference between the guard that is right and the guard that is wrong is which
side the halt happened to land on:

[ifne] === func=sub_6c1c entry=0x6c1c apply#0 ===
[ifne] cand bl=#2@0x6c7c:Copy in=1 out=2 loophead=no whiledo_would=no
[ifne]   arm0 #4@0x6cab:Copy in=1 out=0 dec=true halt=noreturn
[ifne]   arm1 #3@0x6cde:Copy in=1 out=0 dec=true halt=-
[ifne]   CHOSE i=0                                    <- CORRECT (halt on out(0))
[ifne] cand bl=#0@0x6c20:Condition in=0 out=2 loophead=no whiledo_would=no
[ifne]   arm0 #2@0x6c7c:Ls   in=1 out=0 dec=true halt=-
[ifne]   arm1 #5@0x6c53:Copy in=1 out=0 dec=true halt=noreturn
[ifne]   CHOSE i=0                                    <- INVERTED (halt on out(1))

When only one arm is eligible the for i in 0..2 loop already picks it, which is
why ~84% of the filed signature population is already right and is untouched by
any tie-break. The scan order is irrelevant to A: the same block is the only
candidate either way.

Why out-index carries no source information. The trace also settles the
"filed root cause predicts the opposite of the output" contradiction the refuters
logged on scp xcalloc (objdump puts the fatal block on out(0), yet kuna
emits the inverse). apply#0 picks the halt arm at i=0 correctly — and
that pick negates the condition, take_pending_flips ->
block_basic_negate_lastop -> swap_edges re-orients the block, and apply#1
sees the arms swapped and picks the return. The second run undoes the first.
The orientation at the rule is not the disassembly orientation.

2. Is B the same site, and does fixing the scan order alone fix A? — same
60 lines, different predicate, and no.
On coreutils od read_char and
libacl getfacl walk_tree_visited the trace shows loophead=yes,
whiledo_would=no, and exactly one eligible arm — the tie-break never runs,
so nothing an arm predicate does can touch B. Conversely the guards in A are
loophead=no and are the only candidate in their scan, so nothing a scan-order
predicate does can touch A. They are disjoint on every witness.

[ifne] --- deferred scan (apply#1) size=6 order=[#0 out=1  #1@0x5395 out=2  #2@0x5340 out=2  #3@0x53a4 out=0  #4 out=1  #5@0x53a4 out=0]
[ifne] cand bl=#1@0x5395:Copy in=2 out=2 loophead=YES(depth=0) whiledo_would=no
[ifne]   arm0 #5@0x53a4:Copy in=1 out=0 dec=true halt=-     <- the `return v3` exit
[ifne]   arm1 #2@0x5340:Copy in=1 out=2 dec=true halt=-     <- the loop body
[ifne]   CHOSE i=0

Two further facts fall out of that dump. The better candidate is already in
the same scan, one position behind
: #2@0x5340, the block carrying the
source's break, is itself ifNoExit-eligible (its arm #3@0x53a4 is
in=1 out=0) — first-match in component order simply reaches the head first.
And the rule only becomes eligible on the head after the return split: in
apply#0 the shared return v3 block is in=2, the rule declines, and nothing
fires; ActionReturnSplit/returndup then gives it in-degree 1 and apply#1
folds the head. That is why B is an -O0 population, and it is also why the filed
region_structurer.rs:1788 cb.size_out() != 1 diagnosis (Killed K7) could never
have been the site.

3. Which must land first, and does either regress the other? — guardarm
first, and no.
They are separate predicates on separate inputs, so they are
two PRs. guardarm is the narrower and more obviously symmetric change (the tie
is between two folds that are both legal; new_block_if is symmetric in the arm
it takes), so it goes first and loopcondhoist stacks on it. Measured, not
assumed: the four named counter-cases are byte-identical under each option
alone and under both together, and the two options' changed-function sets
were measured separately on the same corpus.

4. What separates the ~84% already-correct panes from the inverted ones?
The tie itself, plus code layout.
It must distinguish control_list's two
guards from each other, and the halt bit cannot — the trace above shows both
guards carry halt=noreturn on one arm. What differs is which arm, and the
compiler's own answer to that is the address: an unoptimised compiler emits the
taken clause of if (c) A; B; in front of B, and the then side of an
if/else in front of the else side. So: in a tie, the arm whose front leaf
lies earlier in the address space becomes the clause.
This deliberately does
NOT read op_mark_halt — preferring the no-return arm fixes scp xcalloc and
control_list and inverts coreutils make-prime-list xalloc, whose source
really is if (p) return p; fprintf(...); exit(1);
(O0/coreutils/compiled/make-prime-list.i:3645). Layout gets all three right.

5. For B form C, what is the exact in-code follower assertion? — it is
already in the rule: size_in() == 1.
The round required "the loop's
immediate structural follower, in-degree exactly 1" and warned that a matcher
keyed on "a return X reachable after the loop" frees libacl getfacl get_list's list and returns NULL. No new predicate is needed, because the fix
is not a hoist-and-relocate: rule_block_if_no_exit folds the body block
whose clause is the follower, and its own if size_in() != 1 { continue; }
is exactly that assertion. get_list's follower is sub_27de(v9); return NULL;
— not a bare return, reached only by the break — so it moves into the break
arm and the normal loop exit returns the list, which is the baseline's semantics
exactly. Verified on the rebased tree:

default:              while( true ) { if (v7 <= 0) return v9; ... if (!v10) break; ... }
                      sub_27de(v9); return NULL;
loopcondhoist on:     while (0 < v7) { ... if (!v10) { sub_27de(v9); return NULL; } ... }
                      return v9;

bzip2 mainGtU is dropped from the witness list as the round instructed: it
is a non-member (its source is a genuine do/while).


Phase 2 — what guardarm does

In a tie, the arm whose front leaf lies EARLIER in the address space becomes
the if clause.
Ties only: a block with a single eligible arm is
byte-identical to upstream, so the great majority of guards are untouched. The
change is symmetric by construction — both arms are legal folds and
new_block_if does not care which it takes — so nothing can be dropped or
duplicated by it.

function default guardarm on
dpkg-query control_list @0x6c1c guard 1 inverted, body nested one level deeper both guards flat = the Ghidra/IDA pane
openssh scp xcalloc @0x23fb9 if (v3) return v3; fatal(); if (!v3) fatal(); return v3;
coreutils [ posixtest @0x495f if (1 <= a0) return expr(); abort(); if (a0 <= 0) abort(); return expr();

Named counter-cases, verified on the rebased tree

Every one re-run on feat/guardarm @ee14e461 (rebased onto 11f40f46), with
diff against the default arm:

counter-case result
coreutils/tail xlseek @0x4804 byte-identical
coreutils/make-prime-list xalloc @0x1906 byte-identical
bash/mksyntax main @0x19f7 byte-identical
libacl/getfacl get_list @0x281a byte-identical

xlseek and xalloc are the two functions the filed predicate ("prefer the
arm whose component ends in an artificial no-return halt") was measured to
invert; mksyntax main and get_list are symptom B's counter-cases, checked
here too because the two options land as a stack.

Measurement — both directions

scripts/decbench/optsweep.py, which scores both arms of the flip against the
tree's own source CFGs and reports functions moved to and off perfect.

Full O0 slice (pre-rebase, 82dd39a7 base)

265 slices, 32,339 functions scored in both arms. Controls clean: 25,518
unchanged bodies, 0 scoring differently; baseline arm agrees with the
published verdict on 32,263/32,339.

bodies changed 6,821
GED 230,291 -> 227,646 (-2,645)
GED-perfect 14,785 -> 15,082 (+297)
to perfect / off perfect 316 / 19 (16.6 : 1, McNemar z = +16.2)
improved / worsened 726 / 243
x86-64 / ARM Cortex-M / i386 PE +305 / -7 / -1

Re-measured on the rebased tree (#280 funcboundflow is upstream of P8)

funcboundflow changes function boundaries, so the sweep was re-run on
bb05b83e over a representative 7-project slice spanning all three
architectures and containing every previously-recorded mover:
openssh-portable, betaflight, coreutils, dpkg, mydoom, libacl, zlib —
139 slices, 16,019 functions.

pre-rebase (same 7 projects) post-rebase
to perfect 209 209
off perfect 10 10
improved 247 247
worsened 112 115

The off-perfect set is identical function-for-function, and so is the
to-perfect count. The boundary change did not disturb this result; only three
functions moved by a point or two inside worsened. Post-rebase headline for
the slice: 3,575 bodies changed, GED 123,837 -> 121,961 (-1,876), perfect
6,817 -> 7,016 (+199), 209 / 10 (20.9 : 1, z = +13.45).

The mirror population, by name

All 19 full-corpus off-perfect functions (moved.csv in the sweep output):

bash/bash::call_expand_word_internal            0 -> 6   x86-64
betaflight/..._STM32F405::cliDumpPrintLinef     0 -> 8   arm
betaflight/..._STM32F405::cliModeColor          0 -> 5   arm
betaflight/..._STM32F405::rtcPersistRead        0 -> 8   arm
betaflight/..._STM32F405::writeConfigToEEPROM   0 -> 5   arm
cleanflight/..._DALRCF405::cliDumpPrintLinef    0 -> 8   arm
cleanflight/..._DALRCF405::cliModeColor         0 -> 5   arm
cleanflight/..._DALRCF405::writeConfigToEEPROM  0 -> 5   arm
dpkg/dpkg::setexecute                           0 -> 2   x86-64
e2fsprogs/e2fsck::e2fsck_simple_progress        0 -> 8   x86-64
e2fsprogs/e2fsck::preenhalt                     0 -> 6   x86-64
gnutls/certtool::pkcs8_info_int                 0 -> 2   x86-64
mydoom/mydoom::payload_sco                      0 -> 2   pe-x86
openssh-portable/sshd::do_nologin               0 -> 2   x86-64
shadow/lastlog::print_one                       0 -> 5   x86-64
sysvinit/init::coredump                         0 -> 2   x86-64
zlib/example::test_inflate                      0 -> 6   x86-64
zlib/example64::test_inflate                    0 -> 6   x86-64
zlib/examplesh::test_inflate                    0 -> 6   x86-64

Two were read line by line and are the metric, not the option. With
guardarm on, betaflight cliDumpPrintLinef and zlib test_inflate match
their preprocessed source literally
if (!((dumpMask & DO_DIFF) && equalsDefault)) { ...; return true; } return false;
(O0/betaflight/compiled/cli.i:17169) and
if (strcmp(...)) { fprintf(stderr,"bad inflate\n"); exit(1); } else { printf(...); }
(O0/zlib/compiled/example.i:4170) — and GED still charges 8 and 6. That
accounts for the whole ARM column.

One is a real limit and is written into the option's catalog row:
e2fsprogs preenhalt's source is if (!(ctx->options & E2F_OPT_PREEN)) return;
and the option inverts it, because that arm is a return-split clone whose
start address is the shared epilogue's, not the source position of the return.
When the arm is a clone the layout signal is destroyed.

Requirement 8 — every changed function, not the witness

All 6,821 changed bodies audited mechanically (callee multiset, string
multiset, numeric multiset, statement count, plus shape totals):

base var
callee multiset mismatches 0
goto 1,021 1,019
label 620 619
while( true ) 717 717
// no-return 5,291 5,291

1,036 of the 1,045 flagged functions differ only in the comparison constant
a negated condition renders (v4 <= 0xb -> 10 <= v4). The rest were read:
coreutils pr skip_read flips v3 = v4 == 0xc to v3 = v4 != 0xc and flips
its single use (equivalent); gnutls certtool process_options and
openssh ssh mux_client_request_terminate are de-nestings that drop redundant
v7 = v1 copy shadows.

Speed

Interleaved min-of-N whole-binary decompile-all, re-measured on the rebased
build:

binary fns base guardarm on delta
coreutils ls (min-of-5) 2,054 slice 8,357 ms 8,346 ms -0.13%
openssh sshd (min-of-3) 9,173 41,983 ms 42,020 ms +0.09%

Inside measurement noise and far within the 5% budget. (Per-sample variance on
this box is +-20% under load, which is why only interleaved minima are quoted;
an earlier loaded run of the same pair read +1.14% / +6.01% and did not
reproduce.)

Gates — run on the REBASED tree (ee14e461, base 11f40f46)

Exit codes captured directly (make ... > log 2>&1; echo $?), never through a
pipe to tail — a pipeline returns tail's status and hides a failure.

make test             exit=0   datatests: 675/675 assertions passed   ===  PARITY OK
make test-stages      exit=0   datatests: 410/410 assertions passed   ===  PARITY OK
make rust-test        exit=0   4,540 passed / 0 failed / 37 ignored
make check-spec       exit=0   check-spec OK (lenient mode)
check_spec.py --strict exit=0  check-spec OK (strict mode)
kuna catalog --check  exit=0   catalog OK: documents exactly the registered kuna options

Every hard-coded catalog count re-derived from the built artifact, not by
arithmetic, and then grepped back out of the file to prove the edit is not a
silent no-op.
(A clean rebase is not evidence of a correct count: #280 and
#278 made the identical 95 -> 96 edit at four sites, git auto-merged them
without a conflict marker, and catalog_bytecompat.rs did not even appear in
git status.) Sources: live kuna catalog --json,
grep -c '^\[\[settable\]\]' phases.toml, a fresh phase_catalog.json
recapture, and ls tests/{datatests,stages}/*.xml | wc -l:

counter derived
kuna catalog --json options 97
kuna_phases/tests.rs kuna_num_settables / SETTABLE_TABLE.len 97
tier split (core, transform, analysis) (20, 43, 34)
emit_catalog_json },\n count 96
catalog_bytecompat.rs fixture "option" / "tier" / "symptoms" 97
kuna-catalog.xml #7 structure-recovery 24
kuna-base/src/xml.rs corpus files 195
recaptured phase_catalog.json "option" entries 97

Read the assertions, not the test names: on main the function is called
settable_count_is_95 while its body asserts 96. This branch's function names
were updated too, but the numbers above are the contract.

docs/options.md and tests/fixtures/phase_catalog.json regenerated from that
same build; docs/baseline-stages.json re-recorded, not hand-merged.

Scope

Default-OFF opt-in, tier = transform. No DIV row: a new default-off
option changes no default. Listed in modes.rs's UNEVALUATED next to
paramcopyhoist — preset membership would make it the default output
(auto -> aggressive under 500 KiB) and that is the DIV-recorded change,
wanting its own PR whose only remaining work is the 0/675 ablation with the flag
forced on plus a preset-level speed number. The corpus half of that argument is
already done above.

Spec prose: docs/spec/08-structuring.md section 8.1.

Merge order

  1. [AUTOMATED] feat(p8): guardarm - break the ruleBlockIfNoExit arm TIE by code layout (+297 GED-perfect / 32,339 O0 functions) #281 (this PR) -> main
  2. [AUTOMATED] feat(p8): loopcondhoist - defer a loop head in the ifNoExit scan so ruleBlockWhileDo keeps the head test (+669 GED-perfect / 32,339 O0 functions) #283 (loopcondhoist, symptom B, stacked on this branch) -> retarget to
    main, then merge

delete_branch_on_merge is set repo-wide, so merging this PR deletes
feat/guardarm and auto-closes #283
. Retarget #283 to main before
merging this one.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH

mahaloz and others added 2 commits August 11, 2026 21:54
…by code layout, not by out-index (+297 GED-perfect over 32,339 O0 functions)

`CollapseStructure::rule_block_if_no_exit` folds the condition block and ONE
terminal out-arm into a `BlockIf`; the other arm survives as the fall-through.
Upstream walks the out-edges in index order. Usually only one arm is eligible
and there is no choice -- but when the condition guards a fatal no-return call
and the other side is the function's own `return`, BOTH arms are eligible and
index order decides a coin flip. A `KUNA_RS_DEBUG` decision trace added to the
rule (also in this commit) shows the mechanism: `negateCondition`'s `swapEdges`
re-orients the block between the two `collapse_all` runs the action pool
performs, so the arm at out(0) in the deciding run is not the arm the branch was
assembled with. `dpkg-query control_list` has both outcomes in ONE function --
its two guards are the same shape and the halt lands on out(0) for one and
out(1) for the other, so one comes out flat and the other inverted.

`option guardarm on` (default-off opt-in) resolves the tie by code layout: of
the two eligible arms, the one whose front leaf lies earlier in the address
space becomes the clause, because an unoptimized compiler emits the taken
clause of `if (c) A; B;` in front of `B`. Ties only; a single eligible arm is
byte-identical to upstream.

The predicate deliberately does NOT read the `op_mark_halt` no-return bit.
Preferring the no-return arm fixes `scp xcalloc` and `control_list` but inverts
`make-prime-list xalloc`, whose source really is `if (p) return p; ...; exit(1);`
Layout order gets all three right; `tail xlseek` and `xalloc` are byte-identical
with the option on.

Measured bidirectionally over the whole decbench O0 slice (32,339 scored
functions, 265 slices): 6,821 bodies change, GED 230,291 -> 227,646 (-2,645),
perfect 14,785 -> 15,082 (+297), 316 moved to perfect against 19 moved off
(16.6:1, McNemar z = +16.2). All 6,821 changed bodies audited: identical callee
and string multisets, gotos 1,021 -> 1,019, labels 620 -> 619, `while( true )`
and `// no-return` counts unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
…try rebase

The rebase onto #278 (poolentry) landed guardarm's three GUARDARM assertions
and the KUNA-CATALOG #7 rename on top of main's BRANCHFLIP-ARMSWAP six, so the
hand-merged data_footer was stale. Re-recorded from a live run rather than
hand-merged: 413 -> 416, +3 GUARDARM keys and the #7 text update, nothing
removed. Every shared counter was likewise re-derived from the BUILT artifact
rather than by arithmetic, which caught two the rebase auto-merged at the wrong
value with no conflict marker: catalog_bytecompat.rs (97, must be 98) and
xml.rs's corpus file count (195, must be 196).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
@mahaloz
mahaloz merged commit b9d1b08 into main Aug 11, 2026
9 checks passed
@mahaloz
mahaloz deleted the feat/guardarm branch August 11, 2026 22:05
mahaloz added a commit that referenced this pull request Aug 11, 2026
… from guardarm

#283 was branched on top of #281, whose commit is now on main as a squash. The
child was replayed with `git rebase --onto origin/main ee14e46`, so only
loopcondhoist's own commit applies -- but the shared counters still needed
re-deriving from the BUILT artifact rather than by arithmetic. Two auto-merged
at the wrong value with no conflict marker, both because main and the branch had
made the identical N -> N+1 edit: catalog_bytecompat.rs (98, must be 99) and
xml.rs's corpus file count (196, must be 197). Live catalog 99, phases.toml 99,
tiers (20, 44, 35), `},\n` 98, phase_catalog.json 99 (recaptured),
docs/options.md 99 (regenerated), kuna-catalog.xml structure-recovery 25.
Stages baseline re-recorded on the merged base: 416 -> 419.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
mahaloz added a commit that referenced this pull request Aug 11, 2026
…it scan so ruleBlockWhileDo keeps the head test (+669 GED-perfect / 32,339 O0 functions) (#283)

* [AUTOMATED] feat(p8): loopcondhoist - let the deferred ifNoExit scan pass over a loop head so ruleBlockWhileDo keeps the head test (+669 GED-perfect over 32,339 O0 functions)

`CollapseStructure::collapse_internal`'s deferred `ruleBlockIfNoExit` scan walks
the live components in order and folds the FIRST one whose terminal arm
qualifies, then restarts the cascade. A head-tested loop whose exit arm is a
`return` is such a component -- once ActionReturnSplit/returndup has given that
return in-degree 1 the rule matches the loop head -- and the head sits ahead of
its own body in component order. So the head folds to `if (!C) return X;` and
drops to ONE out-edge, after which `rule_block_while_do` can never match it and
the loop is emitted as `while( true ) { if (!C) return X; BODY; }` where the
source and IDA write `while (C) { BODY }`.

The `KUNA_RS_DEBUG` trace added in the parent commit shows the alternative is
already in the same scan: on coreutils od read_char and libacl getfacl
walk_tree_visited the loop head is the first candidate and the body block
carrying the `break` is the second. Folding the body first is strictly better --
its clause is the loop follower, which the rule ALREADY requires be reached from
nowhere else (`size_in() == 1`), so absorbing it into the break arm is
semantics-preserving by the rule's own precondition; the body then collapses to
a single back-edge clause and the head test hoists into the `while`/`for` header
on the next cascade pass, with no new machinery and no relocated statement.

`option loopcondhoist on` (default-off opt-in) gives the non-heads one pass of
the scan, falling back to the unrestricted upstream pass when it finds nothing.
A function with no loop-head candidate is byte-identical, and the scan still
terminates on the same fixpoint.

Measured bidirectionally over the whole decbench O0 slice (32,339 scored
functions, 265 slices): 2,281 bodies change, GED 230,291 -> 224,670 (-5,621),
perfect 14,785 -> 15,454 (+669), 680 moved to perfect against 11 moved off
(61.8:1, McNemar z = +25.5). Every architecture gains: x86-64 +542, ARM
Cortex-M +123, i386 PE +4. Over the changed set `while( true )` goes 2,087 ->
223, gotos 621 -> 409, labels 399 -> 251, `// no-return` unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH

* [AUTOMATED] chore(p0): re-derive the shared counters after unstacking from guardarm

#283 was branched on top of #281, whose commit is now on main as a squash. The
child was replayed with `git rebase --onto origin/main ee14e46`, so only
loopcondhoist's own commit applies -- but the shared counters still needed
re-deriving from the BUILT artifact rather than by arithmetic. Two auto-merged
at the wrong value with no conflict marker, both because main and the branch had
made the identical N -> N+1 edit: catalog_bytecompat.rs (98, must be 99) and
xml.rs's corpus file count (196, must be 197). Live catalog 99, phases.toml 99,
tiers (20, 44, 35), `},\n` 98, phase_catalog.json 99 (recaptured),
docs/options.md 99 (regenerated), kuna-catalog.xml structure-recovery 25.
Stages baseline re-recorded on the merged base: 416 -> 419.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
mahaloz added a commit that referenced this pull request Aug 11, 2026
…e stages baseline

Rebased onto #278/#281/#283. Every shared counter re-derived from the BUILT
artifact rather than by arithmetic: live catalog 100, phases.toml 100,
catalog_bytecompat.rs 100, kuna_phases/tests.rs 100 / tiers (21, 44, 35) /
`},\n` 99, phase_catalog.json 100 (recaptured), docs/options.md 100
(regenerated), xml.rs corpus 198. `kuna-catalog.xml` structure-recovery stays 25
because calloverlap is a correctness-fix, not a structure-recovery row. Stages
baseline re-recorded on the merged base: 419 -> 425, +6 CALLOVERLAP keys, none
removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
mahaloz added a commit that referenced this pull request Aug 11, 2026
…call-overlap guards (GH-275) (#284)

* [AUTOMATED] fix(p3): calloverlap - complete the two stubbed upstream call-overlap guards (GH-275)

Squashed from the branch's four commits (the fix, the two catalog-count bumps,
the flip-guidance prose and the post-funcboundflow reconciliation) so the merge
train replays one commit instead of four rounds of the same shared-counter edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH

* [AUTOMATED] chore(p0): re-derive the shared counters and re-record the stages baseline

Rebased onto #278/#281/#283. Every shared counter re-derived from the BUILT
artifact rather than by arithmetic: live catalog 100, phases.toml 100,
catalog_bytecompat.rs 100, kuna_phases/tests.rs 100 / tiers (21, 44, 35) /
`},\n` 99, phase_catalog.json 100 (recaptured), docs/options.md 100
(regenerated), xml.rs corpus 198. `kuna-catalog.xml` structure-recovery stays 25
because calloverlap is a correctness-fix, not a structure-recovery row. Stages
baseline re-recorded on the merged base: 419 -> 425, +6 CALLOVERLAP keys, none
removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
mahaloz added a commit that referenced this pull request Aug 11, 2026
…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
mahaloz added a commit that referenced this pull request Aug 11, 2026
…chain of a short-circuit (DIV-69) (#285)

* [AUTOMATED] feat(p8): orchain - returndup must not split the operand 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

* [AUTOMATED] chore(p0): re-derive the shared counters and re-record the 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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant