fix(encoder): A32 i64 mul/shift/rotate/compare + tripwire — no silent-NOP arm survives (#615)#620
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-NOP arm survives (#615) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
avrabe
added a commit
that referenced
this pull request
Jul 7, 2026
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.
Closes #615.
Root cause
The A32 (
--target cortex-r5/ armv7r,IsaVariant::Arm32) arm ofArmEncoder::encodestill carried verification-era "encode as NOP for now" arms: every i64 op (mul, shifts, rotates, all ten comparisons, eqz, clz/ctz/popcnt, div/rem, const, load/store, extends, wrap) plus the i32SetCond/SelectMove/Popcntpseudo-ops encoded to the literal NOP word0xE1A00000. Once cortex-r5 made ARM mode user-reachable, the operation silently vanished — functions returned whatever garbage sat in the result registers, with no diagnostic (validate_instructionsonly gates FPU/MVE).Class history: #594 (A32
call_indirect→ NOP, call never happened) → #610/#613 (Thumb i64 rot/div silently zero) → #615 (every A32 i64 op → NOP).What this PR does
1. A32 expansion helper (
encode_arm_expanded), wired as an early return inencode_arm— mirrors each Thumb-2 twin's register contract exactly:SetCond,SelectMoveMOV<c>(A32 conditional execution replaces IT blocks)I64SetCond(10 conds),I64SetCondZ,I64Eq..I64GeU,I64EqzCMP lo,lo+SBCS hi,hiidiom w/ operand swap;ORRSfor eqzI64MulMUL/MLAcross products into R12 +UMULL+ADDI64Shl/ShrU/ShrSSUBS/BPLsmall/large splitI64Rotl/RotrI64DivU/DivS/RemU/RemSORRS R2,R3+UDF #0zero-divisor trap, 64-round shift-subtract core, signed variants negate/re-signI64Clz/CtzCLZ/RBIT+ conditional executionI64Const/Ldr/Str/Extend*/I32WrapI64MOVW/MOVT, word pairs w/ imm12 range check (Err on overflow),SXTB/SXTH+ASR #31I64Add/Sub/And/Or/XorADDS+ADC/SUBS+SBC/ word pairsPopcnt,I64PopcntNew free helpers
emit_a32_i64_fixed_abi_entry/exit+emit_a32_i64_divisor_zero_trapare A32 twins of the #610 Thumb wrappers (same marshal contract,STMDB SP!/LDR [SP],#4encodings, same swapped-pair Err).2. No silent NOP survives. The former NOP arms are now:
unreachable!("handled by encode_arm_expanded (#615)")for everything the helper covers (the call_indirect on the A32 path (--target cortex-r5) compiles to a NOP — silently returns wrong result instead of calling the target #594 CallIndirect pattern), andErrfor ops with no A32 encoding: the verification-only pseudo-ops (Select,LocalGet/Set/Tee,GlobalGet/Set,BrTable,Call— modeled by synth-verify's ArmSemantics, never constructed by any codegen path;Errrather thanunreachable!keeps the encoder total per theencoder_no_panicOk-or-Err contract) and the 41 MVE ops (Helium is Thumb-2-only; the selector only emits MVE for M55). Loud-Err'd, not implemented, because no A32 target can legally reach them — and if one ever does, it now fails with a message instead of shipping wrong code.3. Tripwire test (
a32_no_silent_nop_615.rs) — the structural payoff: an exhaustive no-wildcardmatchover all 221ArmOpvariants (adding a variant fails compilation until classified) plus one non-degenerate representative instance per variant, asserting the A32 encoding is never the bare NOP word — except the documented allowlist (ArmOp::Nop,Mov rd,rd,I32WrapI64 rd==rnlo,Lsl rd,rd,#0, all genuine no-ops). Err/unreachable-covered ops count as PASS (they are loud). The class cannot silently regrow.Verification
Clang cross-check (#544 pattern): 24 representative encodings (
SBCS,UMULL,CLZNE, register-controlledLSL,RBIT,MOVT,ORR lsr#31,STR [SP,#-4]!,LDR [SP],#4,MUL,MLA,SBC,ADC,MOVW,AND #63,RSB #32,SXTB,MVN,ADDS #1,ORRS,CMP,SUBS #1,PUSH,POP) assembled withclang -target armv7r-none-eabi— all 24 match bit-for-bit.Branch-offset audit: hardcoded A32 branch words re-derived against instruction counts (target = PC+8+imm24*4): shift
BPL +5skips the 6-insn small path onto.large,B +1skips the 2-insn large path; rotBPL/B +7skip the 8-insn halves; div-loopBHI +2/BLO +4/BLO +2land on subtract/decrement; trapBNE +0skips exactly theUDF. All consistent (and execution-verified below).Execution differential (
scripts/repro/a32_i64_615_differential.py): unicornUC_MODE_ARM(not Thumb) vs the wasmtime oracle, symbols from the ELF symtab (#489), module compiled--target cortex-r5 --all-exports --relocatable --no-optimize. 40 exported functions covering every op above; vectors include0x8000000000000000,-1,0,1, lo/hi straddles, shift/rot amounts {0,1,31,32,33,63,65}:Red→green: the same script against a pre-fix build (origin/main encoder, same harness):
Pre-fix ground truth (issue evidence, reconfirmed):
mul64body containedmov r0, r0where the multiply belongs. Post-fix:mul ip,r4,r3; mla ip,r5,r2,ip; umull r6,r7,r4,r2; add r7,r7,ip.Frozen anchors:
cargo test -p synth-cli --test frozen_codegen_bytes— 9/9 pass, Thumb bytes byte-identical (this change only touches the A32 path).Suites:
cargo test -p synth-backend214+ green (one unit test updated:test_encode_mve_arm32_noppinned the old silent-NOP MVE behavior and now asserts the loud Err); full workspace green (--exclude synth-verify, local z3 quirk); fmt + clippy clean.🤖 Generated with Claude Code