Skip to content

feat(vcr-sel): cmp→select → IT-block predication fusion, flag-off (VCR-SEL-004, #242, #428) - #444

Merged
avrabe merged 1 commit into
mainfrom
feat/vcr-sel-004-cmp-select-predication
Jun 23, 2026
Merged

feat(vcr-sel): cmp→select → IT-block predication fusion, flag-off (VCR-SEL-004, #242, #428)#444
avrabe merged 1 commit into
mainfrom
feat/vcr-sel-004-cmp-select-predication

Conversation

@avrabe

@avrabe avrabe commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What

Adds the late peephole fuse_cmp_select (VCR-SEL-004, epic #242) — the cmp→select half of gale's #1 hot-path size/cycle lever (#428).

The selector lowers a select whose condition is a comparison to a materialize-then-re-test sequence:

cmp a,b ; SetCond D,c ; cmp D,#0 ; movne dst,v1 ; moveq dst,v2

The SetCond and the select's cmp D,#0 are pure overhead — the comparison already set NZCV, and each SelectMove is an independent flag-preserving IT;MOV. The peephole collapses the pair onto the comparison's own flags (delete SetCond + cmp D,#0, retarget the moves to c / invert(c)), yielding the textbook predicated clamp:

cmp a,b ; movc dst,v1 ; mov{!c} dst,v2     (−2 instructions / fused select)

Real-fixture objdump (control_step), flag-on:

flag-off: cmp r0,#19 ; movcc.w r8,#1 ; movcs.w r8,#0 ; cmp.w r8,#0 ; movne r5,r6   (5 insns)
flag-on:  cmp r0,#19 ; movcc r5,r6                                                  (2 insns)

Scope — select half only

The artifact title is "select/br_if". This PR lands the selectmovCC half. The br_if→predicated-branch half is not done (branch consumers are unmodeled by reg_effect and never match) — deferred follow-up. Artifact note scoped accordingly.

Sound by construction (each guard has a negative unit test)

  • flags reused only when nothing clobbers them in the SetCondcmp window (clobbers_flags — tight ldr/str/push/pop/nop allowlist);
  • the boolean deleted only when provably dead — a downstream redefinition before any read / unmodeled op / branch / end (reg_dead_by_redef, so an R0 return value is never mistaken for dead);
  • [i-1] must be the flag-setting compare; dst != D.

Plus Condition::invert() (exhaustive over all 10 conditions).

Frozen-safe / gated

Wired into arm_backend.rs after range-realloc (final register identities for the dead-D proof) and before encode, behind SYNTH_CMP_SELECT_FUSE=1. Default-off ⇒ a literal no-op ⇒ the three frozen differentials (control_step 0x00210A55 / flat+inlined flight_algo 0x07FDF307 / divseam) stay bit-identical, un-rerun.

Validation (this step)

  • 8 liveness unit tests — positive incl. the gust_mix ×2 in-place clamp; 5 negative guards (D live, flag-clobber in window, predecessor not a compare, dst aliases D, no redef);
  • 3 encoder byte-tests — all 10 IT <cond>; MOV conditions, exact bytes for the LT/GE + LO/HS pairs fusion newly produces;
  • rewrite-count > 0 on real fixtures (control_step 3, flight_algo/controller 5–6, filter_axis 0 — correctly nothing to fuse);
  • arm-none-eabi-objdump diff confirms the predicated clamp replaces the 5-insn materialize+re-test, and unsafe sites (boolean reused) correctly decline.

Deliberately deferred (owed by the default-on flip step — NOT done here)

Refs #242, #428. Baseline v0.12.0.

🤖 Generated with Claude Code

…R-SEL-004, #242, #428)

The selector lowers a `select` whose condition is a comparison to a
*materialize-then-re-test* sequence: `cmp a,b; SetCond D,c; cmp D,#0;
movne dst,v1; moveq dst,v2`. The `SetCond` and the select's `cmp D,#0`
are pure overhead — the comparison already set NZCV, and each
`SelectMove` is an independent flag-preserving `IT;MOV`. This adds a LATE
peephole `fuse_cmp_select` that collapses the pair onto the comparison's
own flags (delete `SetCond` + `cmp D,#0`, retarget the moves to `c` /
`invert(c)`), yielding the textbook predicated clamp `cmp a,b; movc dst,v1;
mov{!c} dst,v2` — −2 instructions per fused select. gale #428 re-ranked
this the #1 hot-path size/cycle lever on the gust_mix clamp chain.

Sound by construction (each guard has a negative unit test):
- flags reused only when nothing clobbers them in the `SetCond`→`cmp`
  window (`clobbers_flags`, a tight load/store/push/pop/nop allowlist);
- the boolean deleted only when provably dead — a downstream redefinition
  before any read / unmodeled op / branch / end (`reg_dead_by_redef`,
  so an R0 return value is never mistaken for dead);
- `[i-1]` must be the flag-setting compare; `dst != D`.

Also adds `Condition::invert()` (exhaustive over all 10 conditions).

Wired into arm_backend.rs AFTER range-realloc (final register identities
for the dead-D proof) and before encode, BEHIND `SYNTH_CMP_SELECT_FUSE=1`.
Default-off ⇒ a literal no-op ⇒ the three frozen differentials
(control_step 0x00210A55 / flat+inlined flight_algo 0x07FDF307 / divseam)
stay bit-identical, un-rerun. The default-on flip is the held byte-changing
step, gated on gale's `gust_codegen_bench` (G474RE) + the VCR-ORACLE-001
execution differential — DEFERRED, not done here.

Validation: 8 liveness unit tests (positive incl. the gust_mix ×2 in-place
clamp; 5 negative guards), 3 encoder byte-tests (all 10 IT conditions),
rewrite-count>0 on real fixtures (control_step 3, flight_algo/controller
5-6), and an arm-none-eabi-objdump diff confirming `cmp r0,#19; movcc r5,r6`
replaces the 5-insn materialize+re-test while unsafe sites correctly decline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.38202% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-synthesis/src/liveness.rs 95.95% 10 Missing ⚠️
crates/synth-backend/src/arm_backend.rs 28.57% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit ef97f86 into main Jun 23, 2026
14 checks passed
@avrabe
avrabe deleted the feat/vcr-sel-004-cmp-select-predication branch June 23, 2026 04:49
avrabe added a commit that referenced this pull request Jun 23, 2026
…use gale's clamp-#2 decline (VCR-SEL-004, #428, #242) (#448)

gale's gust_codegen_bench (qemu -icount) confirmed cmp→select (#444) is
correctness-identical over [0,2047] and a monotonic win (1.05→0.95 ticks,
2.63×→2.375×, 132→124 B), but surfaced two observations: the fusion fired one
gust_mix clamp and declined its sibling, and the two-move moveq→mov{invert(c)}
arm never executed end-to-end.

Investigation (objdump of the synthetic two-move fixture) found a SINGLE root
cause — not gale's "narrow dst-match" guess: reg_dead_by_redef requires an
explicit downstream redefinition of the boolean before deleting the SetCond. The
real selector ABANDONS the boolean temp after the select (used once, not
live-out, never rewritten), so the guard conservatively declines. This one
mechanism unifies BOTH the clamp-#2 decline AND the two-move-arm-never-fires gap:
the two-move arm is effectively unreachable through the real selector today.

Adds:
- scripts/repro/cmp_select_two_move.wat — forces the selector's two-move form
  (val2 a live param); compiles, and (verified by objdump) currently declines.
- cmp_select_two_move_coverage.rs — characterization test: the two-move fixture
  fuses 0 sites while in-place control_step fuses >0 (non-vacuous). Flips to >=1
  when the deadness guard is fixed.
- VCR-SEL-004 note: the finding, the unified follow-on (teach reg_dead_by_redef
  that "abandoned + not-live-out" is dead — closes clamp #2 AND makes the
  two-move arm reachable), and the two flip preconditions (two-move exercised
  end-to-end + gale G474RE DWT no-regression).

Frozen-safe: test + fixture + docs only, zero production change. fmt + clippy +
rivet (0 non-xref) clean.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Jun 23, 2026
…wo-move arm reachable (VCR-SEL-004 #7, #428, #242)

#444's deadness guard deleted the SetCond only when the boolean was explicitly
REDEFINED downstream. The real selector abandons the boolean temp after the
select (used once, not live-out, never rewritten), so the guard declined — both
gale's gust_mix clamp #2 and EVERY two-move shape, leaving the two-move
moveq→mov{invert(c)} arm unreachable through the real selector.

Fix: reg_dead_by_redef now recognizes the function RETURN terminator (`bx lr`,
or `pop {…,pc}`). At a return the only live-out registers are the ABI result
regs {R0,R1} (i32→R0, i64→R0:R1; R2/R3 are i64 operand inputs, never results),
so an abandoned boolean in R2..R8 is proven dead and the SetCond can be deleted.

Soundness: the forward scan still bails (declines) on every Label and branch —
reg_effect returns None for them — so it never walks past a join point where the
boolean could be live on another edge. The `d ∉ {R0,R1}` guard is load-bearing.
Locked by unit tests: positive (abandoned boolean before pop/bx-lr ⇒ fuse, with
movlt/movge — the invert arm) and negatives (boolean in a result reg, a branch in
the tail, and bx-to-non-LR all still decline).

Verified end-to-end: scripts/repro/cmp_select_two_move.wat now lowers to
`cmp r0,r1; movlt r4,r0; movge r4,r2` — the mov{invert(c)} arm firing for the
first time, semantically identical to the select. Characterization test flipped
to assert reachability (fuses 1 / in-place control_step fuses >0).

Frozen-safe: reg_dead_by_redef is reached ONLY via fuse_cmp_select, which runs
only behind SYNTH_CMP_SELECT_FUSE (off by default) — so the shipped default path
is byte-identical; frozen byte gates #445/#446 stay GREEN flag-off (verified).

NOT claimed (owed by the flip, the separate gated step): the two-move arm is
reachable + objdump/IR-correct but NOT execution-validated — gale's next
gust_codegen_bench (flag-on) exercises it for the first time. "Closes clamp #2"
is by the same mechanism but unverified locally (no gust_mix.wasm); gale's bench
confirms. No flip, no re-freeze, no tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Jun 23, 2026
…wo-move arm reachable (VCR-SEL-004 #7, #428, #242)

#444's deadness guard deleted the SetCond only when the boolean was explicitly
REDEFINED downstream. The real selector abandons the boolean temp after the
select (used once, not live-out, never rewritten), so the guard declined — both
gale's gust_mix clamp #2 and EVERY two-move shape, leaving the two-move
moveq→mov{invert(c)} arm unreachable through the real selector.

Fix: reg_dead_by_redef now recognizes the function RETURN terminator (`bx lr`,
or `pop {…,pc}`). At a return the only live-out registers are the ABI result
regs {R0,R1} (i32→R0, i64→R0:R1; R2/R3 are i64 operand inputs, never results),
so an abandoned boolean in R2..R8 is proven dead and the SetCond can be deleted.

Soundness: the forward scan still bails (declines) on every Label and branch —
reg_effect returns None for them — so it never walks past a join point where the
boolean could be live on another edge. The `d ∉ {R0,R1}` guard is load-bearing.
Locked by unit tests: positive (abandoned boolean before pop/bx-lr ⇒ fuse, with
movlt/movge — the invert arm) and negatives (boolean in a result reg, a branch in
the tail, and bx-to-non-LR all still decline).

Verified end-to-end: scripts/repro/cmp_select_two_move.wat now lowers to
`cmp r0,r1; movlt r4,r0; movge r4,r2` — the mov{invert(c)} arm firing for the
first time, semantically identical to the select. Characterization test flipped
to assert reachability (fuses 1 / in-place control_step fuses >0).

Frozen-safe: reg_dead_by_redef is reached ONLY via fuse_cmp_select, which runs
only behind SYNTH_CMP_SELECT_FUSE (off by default) — so the shipped default path
is byte-identical; frozen byte gates #445/#446 stay GREEN flag-off (verified).

NOT claimed (owed by the flip, the separate gated step): the two-move arm is
reachable + objdump/IR-correct but NOT execution-validated — gale's next
gust_codegen_bench (flag-on) exercises it for the first time. "Closes clamp #2"
is by the same mechanism but unverified locally (no gust_mix.wasm); gale's bench
confirms. No flip, no re-freeze, no tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Jun 23, 2026
…wo-move arm reachable (VCR-SEL-004 #7, #428, #242) (#449)

#444's deadness guard deleted the SetCond only when the boolean was explicitly
REDEFINED downstream. The real selector abandons the boolean temp after the
select (used once, not live-out, never rewritten), so the guard declined — both
gale's gust_mix clamp #2 and EVERY two-move shape, leaving the two-move
moveq→mov{invert(c)} arm unreachable through the real selector.

Fix: reg_dead_by_redef now recognizes the function RETURN terminator (`bx lr`,
or `pop {…,pc}`). At a return the only live-out registers are the ABI result
regs {R0,R1} (i32→R0, i64→R0:R1; R2/R3 are i64 operand inputs, never results),
so an abandoned boolean in R2..R8 is proven dead and the SetCond can be deleted.

Soundness: the forward scan still bails (declines) on every Label and branch —
reg_effect returns None for them — so it never walks past a join point where the
boolean could be live on another edge. The `d ∉ {R0,R1}` guard is load-bearing.
Locked by unit tests: positive (abandoned boolean before pop/bx-lr ⇒ fuse, with
movlt/movge — the invert arm) and negatives (boolean in a result reg, a branch in
the tail, and bx-to-non-LR all still decline).

Verified end-to-end: scripts/repro/cmp_select_two_move.wat now lowers to
`cmp r0,r1; movlt r4,r0; movge r4,r2` — the mov{invert(c)} arm firing for the
first time, semantically identical to the select. Characterization test flipped
to assert reachability (fuses 1 / in-place control_step fuses >0).

Frozen-safe: reg_dead_by_redef is reached ONLY via fuse_cmp_select, which runs
only behind SYNTH_CMP_SELECT_FUSE (off by default) — so the shipped default path
is byte-identical; frozen byte gates #445/#446 stay GREEN flag-off (verified).

NOT claimed (owed by the flip, the separate gated step): the two-move arm is
reachable + objdump/IR-correct but NOT execution-validated — gale's next
gust_codegen_bench (flag-on) exercises it for the first time. "Closes clamp #2"
is by the same mechanism but unverified locally (no gust_mix.wasm); gale's bench
confirms. No flip, no re-freeze, no tag.

Co-authored-by: Claude Opus 4.8 <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