Skip to content

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
mainfrom
fix/selector-wildcard-arms-946
Open

fix(#946): selector wildcard classification — the wasm_stack_effect wildcard was a LIVE executed miscompile (RQ-58-WILDCARD)#1003
avrabe wants to merge 2 commits into
mainfrom
fix/selector-wildcard-arms-946

Conversation

@avrabe

@avrabe avrabe commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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 feeding infer_i64_locals, the i64 local width inference shared with the RV32 selector (#312) — carried wrong rows behind the wildcard and beside it:

row table said reality
If / BrIf / BrTable (0, 0) "no value stack effect" each pops one i32 (condition / index)
MemoryCopy / MemoryFill absorbed by _ => (0, 0) each pops 3
Call / CallIndirect (0, 1) "approximate" args + call_indirect's table index are popped

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 computing 0x1_0000_0005 >> 32, expected 1):

  • br_if shape → 32 · if shape → 32 · br_table shape → 32 · memory.copy shape → 0
  • call-args shape: local.set of the i64 emitted a single-word str r0, [sp] (static)

All return 1 after the fix. Plain WASM-validating modules; no exotic features.

The conversion + tripwire

The denominator, stated honestly (56 arms classified)

  • Converted (9):
    • 1 eliminated — the wasm_stack_effect wildcard (the live hole).
    • 8 silent-to-loud, count-neutral by design — six float inner maps whose _ => 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 now unreachable!() — 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.
  • Deliberately left (47), each with a reason (recorded at the pin in claims.yaml):
    • 16 guarded-unreachable!() refinements — already loud, outer arm pins the set;
    • 7 closed-subspace WasmOp walks — only LocalGet/Set/Tee carry local indices, only Call/CallIndirect carry arg counts, only Block/Loop/If/End affect nesting;
    • 6 dispatch defaults whose fall-through is handled-elsewhere / loud-decline (incl. vfp_op_demand, whose doc pins the safe direction: an under-estimate re-raises the exhaustion decline, never a miscompile);
    • 4 lookback peepholes — the wildcard declines an optimization, conservative by direction;
    • 10 internal-enum arms with meaningful None/false/no-op semantics (incl. set_branch_offset, whose only caller resolve_branches has zero in-tree call sites — production branch resolution is arm_backend's own pass);
    • 3 genuinely total index % N maps;
    • 1 already-loud 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_code 56 -> 55, value + baseline banked; arms_total 91 -> 90.
  • selector_lines_code 17,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_total 28,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

Honest residuals (named, not hidden)

  • The width walk is still a linear approximation: post-Br/Return/Unreachable polymorphic tracking and the Else join (an if-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.
  • A void callee still pushes one phantom width entry (Call result push is unconditional, matching the ARM lowering's own unconditional result push); RV32's func_result_counts machinery is the natural donor when this is picked up.

Gates (each run without a pipe)

cargo test --workspace rc=0 (145 suites, 0 failures) · cargo clippy --workspace --all-targets -- -D warnings clean · cargo fmt --check clean · python3 scripts/claim_check.py claims.yaml 49/49 · #973 corpus sweep PASS (2327 compared, EXPECTED_DECLINES unchanged).

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 2 commits August 19, 2026 09:02
…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

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.27907% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-synthesis/src/instruction_selector.rs 63.75% 29 Missing ⚠️

📢 Thoughts on this report? Let us know!

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