fix(optimizer): optimized-path block/br_if branch lands mid-instruction (#483, #242) - #497
Merged
Merged
Conversation
…struction (#483, #242) On the optimized (non-relocatable) ARM path, a forward `block` + `br_if` lowered to a conditional branch whose target landed in the MIDDLE of an instruction and did not skip the block, silently miscompiling any such function (the `--relocatable` direct path was unaffected). Two root causes: 1. **Label id mismatch.** A `br`/`br_if` exiting a forward `block` targets the id pushed when the `Block` opened (the `Block` arm emits only a `Nop`). But the matching `End` emitted its label with the End's OWN id, so the branch resolved against an id no label carried and was left as the unpatched offset-0 placeholder. Fix: `End` now labels with the id of the block it closes (loop ends and the function-end keep their own id — a loop's target label is already at its start, and the function end is just the structural trailing label). 2. **Byte-size estimate gap.** The `byte_offsets` table that drives branch displacement sized `Strh`/`Strb`/`Ldrh`/`Ldrsh`/`Ldrb`/`Ldrsb` as the default 2 bytes, but the optimized path always addresses memory off a high base register (`ip`/r11), so the encoder emits the 4-byte `.w` form. Once the branch resolved (fix 1), the i32.store16 in the repro drifted the target by 2. Fix: size these as 4 (the optimized path never emits the 16-bit T1 form). Validation: new CI-gated unicorn differential (`block_brif_483_differential.py`) runs the forward-block repro AND a nested-block + `br` case through the optimized path and diffs linear memory vs wasmtime across both branch directions; fails without the fix (init_branch(0) wrongly executes the guarded stores). Frozen byte gate stays bit-identical (direct path untouched); full workspace suite green. The size-estimator also lacks correct arms for other variable-width ops (Cmp/Cmn/ Adds/Subs/Popcnt) that could drift a branch spanning them — filed separately as a table-completeness follow-up; not triggered by this repro. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
avrabe
added a commit
that referenced
this pull request
Jun 25, 2026
… + the optimizer_bridge control-flow frontier (#242) (#501) VCR-ORACLE-001's running log was stale: it ended with "#490 blocks the non-leaf prologue lever / the lever stays unmerged until #490 lands". Both that fix and a companion control-flow fix have since landed this arc — record the resolution and the broader frontier the work surfaced, so the #242 roadmap reflects reality and prioritizes the next gated step. Appended (behavior-frozen, doc-only) to VCR-ORACLE-001: - #490 LANDED (PR #495): optimized path now wraps a body that touches r4-r8 in push {r4-r8,lr}/pop {r4-r8,pc}, decided on the post-realloc body (no spurious push; >4-param fns decline to direct so stack-arg offsets are untouched), CI oracle callee_saved_490_differential.py. The non-leaf prologue lever it blocked is now unblocked. - #483 LANDED (PR #497): forward block+br_if mid-instruction landing — End labelled with its own id not the closed block's, plus Strh/Strb/Ldrh/... sized 2 vs the 4-byte .w the high-base optimized path emits; CI oracle block_brif_483_differential.py. - The multi-modal control-flow sweep established these are one cluster of optimizer_bridge control-flow/ABI defects — the concrete measured evidence for VCR-SEL-001's "collapse the two/three-selector accretion" motivation — and the next gated frontier: #500 (br_if still unresolved for if/else + sequential blocks; seqblocks the minimal repro; Pattern-3 ruled out), #499 (spill frame not deallocated under control flow; #490's pop epilogue made the SP leak fatal, #215-class), #498 (byte-size estimator incomplete for Cmp/Cmn/Adds/Subs/Popcnt; structural mirror drift), #496 (register-exhausting folds silently miscompiled). All four are separate gated steps, never idle-tick increments; none in shipped firmware (gale ships --relocatable/direct). rivet validate profile unchanged (49 errors / 75 warnings / 0 broken cross-refs, all pre-existing xref/coverage gaps; NON_XREF=0); coverage exit 0. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
avrabe
added a commit
that referenced
this pull request
Jul 15, 2026
…itional branches landed mid-shape Root cause (NOT the direct selector's label resolution, which computes the halfword displacement correctly): the Thumb-2 encoder's 32-bit B<cond>.W (encoding T3) arm packed `halfword_offset >> 1` into imm6:imm11. Per ARMv7-M the byte offset is SignExtend(S:J2:J1:imm6:imm11:'0') — the 20-bit field S:J2:J1:imm6:imm11 IS the signed halfword offset — so every conditional branch spanning > 254 bytes jumped to HALF its intended displacement. gust_poll's loop-head empty-budget `br_if 1 (;@2;)` (the only wide conditional in the function) landed after the first transition call: spurious state+0x34 write + spurious calls on a round that must not touch memory. Narrow (16-bit) B<cond> was unaffected, which is why all short-range CF differentials (#483/#497/#500/#508/#509) stayed green. Fix packs the halfword offset directly (mirroring the T4 unconditional arm) and loud-declines offsets outside the signed 20-bit T3 range instead of truncating. Bytes cross-checked against llvm (`bne.w #0x224` = f040 8112). Evidence: - scripts/repro/brif_outer_740_differential.py — minimal loop-inside-block shape with wide exits on the direct path, RED pre-fix (5/6), GREEN now; anti-vacuous (fails if `poll` stops containing a T3 conditional); CI-wired. - gust_spill_fwd_390_differential.py zero-state rows: the anti-vacuous #740 xfail fired (FIXD) and is PROMOTED to strict — return AND state window now match wasmtime in all three lever configs. - encoder unit test test_encode_thumb_bcond_wide_t3_halfword_offset_740 (forward/backward wide, narrow byte-identical, out-of-range loud Err). - Frozen anchors 10/10 bit-identical (no wide conditionals in fixtures); block_brif_483 / br_table_507 / br_table_value_509 / cf_shapes_500 / control_step differentials all PASS. Closes #740. Part of the #242 Track-C validation lane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe
added a commit
that referenced
this pull request
Jul 15, 2026
…ditional branches landed mid-shape (#748) * wip: session-limit salvage snapshot (agent died mid-lane; resume from here) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#740): B<cond>.W (T3) packed HALF the halfword offset — wide conditional branches landed mid-shape Root cause (NOT the direct selector's label resolution, which computes the halfword displacement correctly): the Thumb-2 encoder's 32-bit B<cond>.W (encoding T3) arm packed `halfword_offset >> 1` into imm6:imm11. Per ARMv7-M the byte offset is SignExtend(S:J2:J1:imm6:imm11:'0') — the 20-bit field S:J2:J1:imm6:imm11 IS the signed halfword offset — so every conditional branch spanning > 254 bytes jumped to HALF its intended displacement. gust_poll's loop-head empty-budget `br_if 1 (;@2;)` (the only wide conditional in the function) landed after the first transition call: spurious state+0x34 write + spurious calls on a round that must not touch memory. Narrow (16-bit) B<cond> was unaffected, which is why all short-range CF differentials (#483/#497/#500/#508/#509) stayed green. Fix packs the halfword offset directly (mirroring the T4 unconditional arm) and loud-declines offsets outside the signed 20-bit T3 range instead of truncating. Bytes cross-checked against llvm (`bne.w #0x224` = f040 8112). Evidence: - scripts/repro/brif_outer_740_differential.py — minimal loop-inside-block shape with wide exits on the direct path, RED pre-fix (5/6), GREEN now; anti-vacuous (fails if `poll` stops containing a T3 conditional); CI-wired. - gust_spill_fwd_390_differential.py zero-state rows: the anti-vacuous #740 xfail fired (FIXD) and is PROMOTED to strict — return AND state window now match wasmtime in all three lever configs. - encoder unit test test_encode_thumb_bcond_wide_t3_halfword_offset_740 (forward/backward wide, narrow byte-identical, out-of-range loud Err). - Frozen anchors 10/10 bit-identical (no wide conditionals in fixtures); block_brif_483 / br_table_507 / br_table_value_509 / cf_shapes_500 / control_step differentials all PASS. Closes #740. Part of the #242 Track-C validation lane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
On the optimized (non-
--relocatable) ARM path, a forwardblock+br_iflowered to a conditional branch whose target landed mid-instruction and did not skip the block — silently miscompiling any such function (#483). The--relocatabledirect path was unaffected.Two root causes:
Label id mismatch. A
br/br_ifexiting a forwardblocktargets the id pushed when theBlockopened (theBlockarm emits only aNop, no label). But the matchingEndemitted its label with the End's own id, so the branch resolved against an id no label carried and was left as the unpatched offset-0 placeholder. Fix:Endlabels with the id of the block it closes (loop ends / function-end keep their own id).Byte-size estimate gap. The
byte_offsetstable driving branch displacement sizedStrh/Strb/Ldrh/Ldrsh/Ldrb/Ldrsbas the default 2 bytes, but the optimized path always addresses memory off a high base register (ip/r11) → the encoder emits the 4-byte.wform. Once branches resolved (fix 1), thei32.store16in the repro drifted the target by 2. Fix: size these as 4.Validation
block_brif_483_differential.py): runs the forward-block repro and a nested-block + unconditionalbrcase through the optimized path, diffing linear memory vs wasmtime across both branch directions. Fails without the fix (init_branch(0)wrongly executes the guarded stores, writing{0:33, 12:44}).Scope / follow-ups
Cmp/Cmn/Adds/Subs/Popcnt) that could drift a branch spanning them — not triggered by this repro; filing a table-completeness follow-up.sub sp,#Nframe not deallocated before the epiloguepop) — independent of this fix; filing separately.🤖 Generated with Claude Code