fix(#946): selector wildcard classification — the wasm_stack_effect wildcard was a LIVE executed miscompile (RQ-58-WILDCARD) - #1003
Open
avrabe wants to merge 2 commits into
Open
Conversation
…l args never left the width stack The RQ-58-WILDCARD classification pass found the live hole the lane brief predicted, and it is worse than latent: an EXECUTED cross-shape miscompile. wasm_stack_effect listed If | BrIf | BrTable as (0, 0) although each pops one i32 (condition / table index), let the `_ => (0, 0)` wildcard absorb MemoryCopy/MemoryFill (which pop 3), and its Call row (0, 1) never popped call arguments. Every one of those stale entries shifts infer_i64_locals' width stack, so an i64 local set past one of these shapes was inferred i32, given a 4-byte slot and a single-word STR/LDR — hi half silently dropped. The inference is shared with the RV32 selector (#312), so the drift was cross-backend. Executed proof (unicorn vs wasmtime, cortex-m4 --relocatable --no-optimize), all returning (0x1_0000_0005 >> 32), expected 1: br_if shape -> 32 if shape -> 32 br_table shape -> 32 memory.copy shape -> 0 All four return 1 after the fix. Fix: * wasm_stack_effect: If | BrIf | BrTable -> (1, 0); MemoryCopy | MemoryFill -> (3, 0); the trailing `_ => (0, 0)` wildcard is GONE — the match is fully enumerated over all 279 WasmOp variants (SIMD rows stated per shape), so a new variant fails to COMPILE until its stack effect is stated (#615 expand-or-loud-reject pattern). * infer_i64_locals: pops call ARGUMENTS against the real arg-count tables (func_arg_counts / type_arg_counts, one slot per value — the decoder's unit) and call_indirect's table-index operand; empty tables degrade to the pre-#946 zero-arg approximation, matching lower_call's own #195 fallback. Signature threaded through compute_local_layout, both ARM call sites, and the RV32 selector (which has no type table; call_indirect is Unsupported there, so the empty slice cannot under-pop on anything that compiles). * Approximation residuals STATED, not hidden, at the structural row: post-Br/Return/Unreachable polymorphic tracking and the Else join (an if-with-result double-counts its result) need a control-frame-aware walk — named follow-up. Gates: * tests/i64_width_vstack_946.rs — 7 shape pins red on the pre-fix behavior (verified by temporary revert), 2 controls (i64-returning call #311, i32-stays-i32) green on both. * tests/selector_stack_effect_no_wildcard_946.rs — the #615-style tripwire (same scanner set as wcet_sp_no_wildcard_946.rs): no catch-all arm may regrow, every WasmOp variant must be NAMED in the body, count pinned 279, negative controls + verified red on an injected `_ =>` in the real function. * scripts/repro/i64_width_vstack_946.wat — the four executable shapes, picked up by the wired #973 ARM corpus sweep Phase B (ran locally: 4 exports x 12 vectors compared, 0 mismatches, sweep PASS with the EXPECTED_DECLINES exact-set unchanged); i64_width_vstack_mem_946.wat carries the memory.copy/fill shapes (memory section excludes it from Phase B — Phase A compile + unit pins cover it). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…dcard ratchet 56 -> 55 Classification-driven follow-through on the RQ-58-WILDCARD lane (the fix itself is the previous commit): * 8 silent-map refinements converted to expand-or-loud-reject: the six float inner matches whose `_ =>` silently lowered as the LAST variant (F32Div/F32Ge/F64Div/F64Sqrt/F64Ge/F64Nearest — a widened outer arm would have become a silent wrong lowering) now name the final variant and panic on drift; the two access-size fallbacks that silently widened any unexpected width to a WORD load/store now panic. Byte-identical by construction (whole workspace incl. all frozen anchors green, rc=0). Deliberately count-neutral: `_ => unreachable!()` still matches the ratchet regex, and swapping it for a bare-binding arm to game the count is exactly what the tripwire scanners flag. * claims.yaml: selector_wildcard_arms_code 56 -> 55 banked (value + baseline; the wasm_stack_effect wildcard is gone), arms_total 91 -> 90, lines_code 17,897 -> 17,991 with a bound waiver (the 279-variant enumeration + call-arg threading: lines traded for exhaustiveness, the trade this release wants), lines_total 28,495 -> 28,599. The remaining 55 arms' classification is recorded at the pin. status.json regenerated via --emit-status. * Ledger re-grades: RQ-58-WILDCARD proposed -> implemented (this lane); RQ-58-SHIPVERIFY proposed -> implemented (PR #1002 merged 2026-08-19 — the ledger had drifted behind the merge). * CLAUDE.md: the pin-rationale sentence kept v0.57 numbers as if live; now dated at pin creation with the live figures delegated to the pins. Gates: cargo test --workspace rc=0, clippy -D warnings clean, fmt --check clean, claim_check 49/49. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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.
RQ-58-WILDCARD (#946, epic #242) — rank first, convert the miscompile-risk set
Re-derived baselines on main after #999:
selector_lines_code = 17,897,selector_wildcard_arms_code = 56(both match the banked pins).The live hole — found, executed, fixed
The classification pass found the hole the lane brief predicted, and it is live and executed, not latent.
wasm_stack_effect— the (pops, pushes) table feedinginfer_i64_locals, the i64 local width inference shared with the RV32 selector (#312) — carried wrong rows behind the wildcard and beside it:If/BrIf/BrTable(0, 0)"no value stack effect"MemoryCopy/MemoryFill_ => (0, 0)Call/CallIndirect(0, 1)"approximate"Every stale entry shifts the width stack, so an i64 local set past one of those shapes was inferred i32, given a 4-byte slot and a single-word STR/LDR — hi half silently dropped. Executed proof (unicorn vs wasmtime, cortex-m4
--relocatable --no-optimize, all computing0x1_0000_0005 >> 32, expected 1):br_ifshape → 32 ·ifshape → 32 ·br_tableshape → 32 ·memory.copyshape → 0local.setof the i64 emitted a single-wordstr r0, [sp](static)All return 1 after the fix. Plain WASM-validating modules; no exotic features.
The conversion + tripwire
wasm_stack_effectis now a fully-enumerated, no-wildcard match over all 279WasmOpvariants — a new variant fails to compile until its stack effect is stated (A32 encoder silently NOPs i64 mul/shl/shr/rotl/rotr/compare/eqz (--target cortex-r5) — returns garbage instead of computing or rejecting #615 expand-or-loud-reject).infer_i64_localspops call arguments against the realfunc_arg_counts/type_arg_countstables (one slot per value — the decoder's unit) plus call_indirect's index operand; empty tables degrade to the pre-existing zero-arg approximation, matchinglower_call's own arm selector: call arguments not marshalled into r0-r3 (AAPCS) — blocks z_impl_k_sem_give #195 fallback. Threaded throughcompute_local_layout, both ARM call sites, and the RV32 selector (no type table there;call_indirectisUnsupportedon RV32, so the empty slice cannot under-pop on anything that compiles).tests/selector_stack_effect_no_wildcard_946.rs— same scanner set and style as the existing doc-vs-source sweep: 30 mechanically-verified disagreements, 4 of them possible defects (not doc staleness) #946writes_sptripwire (wcet_sp_no_wildcard_946.rs): no catch-all (_or bare-binding) arm may regrow, everyWasmOpvariant must be named in the body, population pinned at 279, negative controls prove the scanner can go red, and it was demonstrated red on an injected_ =>in the real function.tests/i64_width_vstack_946.rs: 7 shape tests verified failing on the pre-fix behavior (temporary revert), 2 controls (v0.11.35 arm: i64 unpack miscompile — mask constants materialize into the live u64 pair (r0/r1); silent wrong-code in k_sem_give #311 i64-returning call, i32-stays-i32) green both sides.scripts/repro/i64_width_vstack_946.wat(4 exports) rides the wired ARM: select on an i64-comparison condition with COMPUTED arms always returns the then-arm — reload clobbers the live else-arm register #973 ARM corpus sweep Phase B — ran locally end-to-end: 4 exports x 12 vectors compared, 0 mismatches, sweep PASS with theEXPECTED_DECLINESexact-set unchanged (no new ARM declines, so no cross-lane redness).i64_width_vstack_mem_946.watcarries the memory.copy/fill shapes (a memory section excludes a module from Phase B; Phase-A compile + unit pins cover it).The denominator, stated honestly (56 arms classified)
wasm_stack_effectwildcard (the live hole)._ =>silently lowered as the last variant (F32Div/F32Ge/F64Div/F64Sqrt/F64Ge/F64Nearest: a widened outer arm would have become a silent wrong lowering) and the two access-size fallbacks that silently widened any unexpected width to a word load/store. All nowunreachable!()— which still matches the ratchet regex, deliberately: replacing it with a bare-binding arm to game the count is exactly what the tripwire scanners flag.unreachable!()refinements — already loud, outer arm pins the set;WasmOpwalks — onlyLocalGet/Set/Teecarry local indices, onlyCall/CallIndirectcarry arg counts, onlyBlock/Loop/If/Endaffect nesting;vfp_op_demand, whose doc pins the safe direction: an under-estimate re-raises the exhaustion decline, never a miscompile);None/false/no-op semantics (incl.set_branch_offset, whose only callerresolve_brancheshas zero in-tree call sites — production branch resolution is arm_backend's own pass);index % Nmaps;Err(if/else-with-result lowering has no result-register reconciliation — arms agree only by register-pressure luck #313) — it is the expand-or-loud-reject pattern.Ratchet
selector_wildcard_arms_code56 -> 55, value + baseline banked;arms_total91 -> 90.selector_lines_code17,897 -> 17,991 (+94): waived with a written reason — the 279-variant enumeration + call-arg threading is lines traded for exhaustiveness, the exact trade this release wants.lines_total28,495 -> 28,599.Frozen anchors
Unmoved. The entire workspace suite (incl. all frozen-anchor differentials) is green with rc=0 — no pinned fixture contains the drift shape, so the semantic fix is byte-invisible on all anchors, and the 8 loud-conversions are byte-identical by construction. Byte movement is confined to modules with an i64 local set past a cond-consuming / bulk-memory / arg-taking-call shape — i.e. exactly the shapes proven wrong before and proven right after by execution.
Also folded in
RQ-58-SHIPVERIFYre-gradedproposed -> implemented(PR feat(#1000): ship 'synth verify' in released artifacts + non-vacuity smoke gate (RQ-58-SHIPVERIFY) #1002 merged 2026-08-19; verified viagh pr viewbefore re-grading — the ledger had drifted behind the merge).RQ-58-WILDCARDre-gradedimplementedwith the finding recorded.Honest residuals (named, not hidden)
Br/Return/Unreachablepolymorphic tracking and theElsejoin (anif-with-result double-counts its result across arms) need a control-frame-aware walk — stated in the code at the structural row; named follow-up.Callresult push is unconditional, matching the ARM lowering's own unconditional result push); RV32'sfunc_result_countsmachinery is the natural donor when this is picked up.Gates (each run without a pipe)
cargo test --workspacerc=0 (145 suites, 0 failures) ·cargo clippy --workspace --all-targets -- -D warningsclean ·cargo fmt --checkclean ·python3 scripts/claim_check.py claims.yaml49/49 · #973 corpus sweep PASS (2327 compared, EXPECTED_DECLINES unchanged).🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L