Skip to main content

synth_core/
wasm_decoder.rs

1//! WASM Binary Decoder - Converts wasmparser operators to WasmOp sequences
2//!
3//! This module bridges the gap between parsed WASM binaries and any backend.
4//! It extracts function bodies and converts wasmparser operators to our internal WasmOp format.
5
6use crate::wasm_op::WasmOp;
7use anyhow::{Context, Result};
8use std::collections::HashMap;
9use wasmparser::{ExternalKind, Parser, Payload};
10
11/// Kind of a WASM import
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ImportKind {
14    /// Imported function with type index
15    Function(u32),
16    /// Imported memory
17    Memory,
18    /// Imported table
19    Table,
20    /// Imported global
21    Global,
22}
23
24/// A WASM import entry with full metadata
25#[derive(Debug, Clone)]
26pub struct ImportEntry {
27    /// Module name (e.g., "wasi:cli/stdout" or "env")
28    pub module: String,
29    /// Field name (e.g., "write" or "memory")
30    pub name: String,
31    /// Import kind and associated data
32    pub kind: ImportKind,
33    /// Index of this import within its kind (e.g., function import index)
34    pub index: u32,
35}
36
37/// WASM linear memory specification
38#[derive(Debug, Clone)]
39pub struct WasmMemory {
40    /// Memory index
41    pub index: u32,
42    /// Initial size in pages (64KB each)
43    pub initial_pages: u32,
44    /// Maximum size in pages (if specified)
45    pub max_pages: Option<u32>,
46    /// Whether memory is shared (requires threads proposal)
47    pub shared: bool,
48}
49
50/// A captured constant global initializer (#649). Only INTEGER `t.const` init
51/// exprs are captured: `f32.const`/`f64.const` inits deliberately decode to
52/// `None` — float-typed global ACCESS is the GI-FPU-001 (#369) loud-skip lane,
53/// and fabricating a bit-pattern here must not quietly unskip it. Non-const
54/// init exprs (e.g. `global.get` of an import) are not statically known and
55/// also decode to `None`.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum GlobalInit {
58    /// A leading `i32.const` initializer.
59    I32(i32),
60    /// A leading `i64.const` initializer — BOTH words must reach the emitted
61    /// global slot (#649: `init_i32`-shaped capture silently zeroed these).
62    I64(i64),
63}
64
65/// A WASM global's declaration — its initial value and mutability (#237).
66/// Needed so the native-pointer ABI can recognize a global whose initializer is
67/// a linear-memory address (e.g. `$__stack_pointer = 65536`) and make it
68/// `__synth_wasm_data`-relative, rather than reading it from an R9 globals table
69/// the self-contained drop-in object can't rely on.
70#[derive(Debug, Clone)]
71pub struct WasmGlobal {
72    /// Global index (defined globals; imported globals are not counted here).
73    pub index: u32,
74    /// The captured constant initializer (#237/#649): `i32.const` or
75    /// `i64.const`. Float/non-const init exprs decode to `None` — see
76    /// [`GlobalInit`].
77    pub init: Option<GlobalInit>,
78    /// Whether the global is mutable.
79    pub mutable: bool,
80    /// #643: byte width of the global's storage slot, from its declared value
81    /// type — 4 for i32/f32, 8 for i64/f64, 16 for v128. The globals table is
82    /// laid out by SUMMING these widths (not `index * 4`): an i64 global needs
83    /// room for both words, and every later global's offset shifts with it.
84    pub slot_bytes: u32,
85}
86
87impl WasmMemory {
88    /// Get initial size in bytes
89    pub fn initial_bytes(&self) -> u32 {
90        self.initial_pages * 65536
91    }
92
93    /// Get maximum size in bytes (or initial if not specified)
94    pub fn max_bytes(&self) -> u32 {
95        self.max_pages.unwrap_or(self.initial_pages) * 65536
96    }
97}
98
99/// #642: one element segment's statically-decoded shape — see
100/// [`DecodedModule::elem_segments`].
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ElemSegmentInfo {
103    /// #650: the table this ACTIVE segment initializes (0 for the pre-#650
104    /// single-table form). Meaningless when `offset` is `None`.
105    pub table_index: u32,
106    /// Const i32 offset of an ACTIVE segment into its table; `None` =
107    /// placement not statically verifiable (passive/declared segment or a
108    /// non-const offset expression).
109    pub offset: Option<u32>,
110    /// The segment's function indices in slot order; `None` = contents not
111    /// statically verifiable (an entry was not a plain `ref.func`).
112    pub funcs: Option<Vec<u32>>,
113}
114
115/// #642/#650: one table's `call_indirect` guard inputs — see
116/// [`CallIndirectGuards`] for the layout contract and soundness argument.
117#[derive(Debug, Clone, Default, PartialEq, Eq)]
118pub struct TableGuards {
119    /// Compile-time size of this table (entries); `None` = no sound bound
120    /// known (an imported table with growable limits).
121    pub table_size: Option<u32>,
122    /// #650: byte offset of this table's base within the contiguous R11
123    /// region — `sum(size(0..N)) * 4`, a compile-time constant. `None` when
124    /// any PRECEDING table's size is unknown (the base is then not a
125    /// compile-time constant and the lowering declines).
126    pub base_byte_offset: Option<u32>,
127    /// Per expected-type index: `None` = closed-world type property VERIFIED
128    /// against THIS table; `Some(reason)` = not verifiable (the lowering
129    /// declines).
130    pub type_reject: Vec<Option<String>>,
131    /// #664: whether this table's image contains at least one uninitialized
132    /// (null funcref) slot. WASM Core §4.4.8 requires a `call_indirect`
133    /// reaching a null slot to TRAP — the closed-world type check verifies
134    /// the INITIALIZED slots only, and the lowering must emit a runtime
135    /// null check (pointer == 0 → trap) before the indirect branch when
136    /// this is set. `false` for a fully-initialized table keeps today's
137    /// exact dispatch bytes (no null check) BY CONSTRUCTION. Only
138    /// meaningful when the type verdict is `None` (verified); reject paths
139    /// decline before it is consulted.
140    pub has_null_slots: bool,
141    /// #676: this table's image is statically known but HETEROGENEOUS — its
142    /// initialized slots span at least two distinct STRUCTURAL signature
143    /// classes, so no expected type's closed world can hold
144    /// (`type_reject[t]` is `Some` for every `t`) — yet the mismatch trap
145    /// (WASM Core §4.4.8) IS dischargeable at runtime: the type-id sidecar
146    /// (see [`CallIndirectGuards`]) carries each slot's structural class id,
147    /// and the dispatch compares the indexed slot's id against the expected
148    /// type's class id (a compile-time immediate), trapping on inequality.
149    /// When set (and [`CallIndirectGuards::type_ids_byte_offset`] is known),
150    /// the lowering emits that runtime check INSTEAD of declining. `false`
151    /// keeps the pre-#676 behavior: verified tables dispatch unchecked
152    /// (byte-identical), unverifiable tables decline.
153    pub runtime_type_check: bool,
154}
155
156/// #642/#650: everything the `call_indirect` lowering needs to emit its
157/// guards — computed once per module by
158/// [`DecodedModule::call_indirect_guards`] and threaded to the instruction
159/// selector via `CompileConfig`.
160///
161/// ## The R11 multi-table layout contract (#650)
162///
163/// The runtime/harness links every funcref table as ONE contiguous region of
164/// raw 4-byte code pointers based at R11, in declaration order (imported
165/// tables first): table 0 at `R11 + 0`, table N at
166/// `R11 + sum(size(0..N)) * 4`. The offsets are compile-time constants
167/// because tables are provably fixed-size (`table.grow`/`table.set` are
168/// unsupported ops whose functions loud-skip at decode — #642). A
169/// single-table module degenerates to the pre-#650 contract (table 0 at
170/// R11, offset 0) BY CONSTRUCTION, keeping its emitted bytes identical.
171///
172/// WASM Core §4.4.8 requires `call_indirect` to trap when `index >=
173/// table.size` and when the callee's type does not match the instruction's
174/// expected type. The region stores no size fields and no type ids, so, per
175/// table:
176///  - the BOUNDS check is emitted at runtime against THAT table's
177///    compile-time `table_size` immediate (sound: fixed-size, see above), and
178///  - the TYPE check is discharged at COMPILE time: for expected type `t`,
179///    `tables[n].type_reject[t]` is `None` only when every INITIALIZED slot
180///    of table `n` verifiably holds a function whose signature structurally
181///    equals type `t` (the closed-world property — no runtime mismatch is
182///    then possible). Otherwise it holds the reason, and the lowering
183///    declines LOUDLY rather than emit an unchecked indirect branch, and
184///  - a NULL (uninitialized) slot traps at RUNTIME (#664): the layout
185///    contract requires the runtime/harness to link every uninitialized
186///    slot as a ZERO word (null funcref has no code address; 0 is never a
187///    valid function pointer in the region), and when `has_null_slots` is
188///    set the dispatch emits a null check on the loaded pointer
189///    (`CMP #0` → trap) between the bounds guard and the indirect branch.
190///    A fully-initialized table (`has_null_slots == false`) keeps the
191///    pre-#664 dispatch bytes identical BY CONSTRUCTION, and
192///  - a HETEROGENEOUS table (mixed signatures — the closed-world property
193///    cannot hold for ANY expected type) is dispatched through a runtime
194///    type check against the **type-id sidecar** (#676): a parallel `u32`
195///    array the layout contract places at `R11 + type_ids_byte_offset`
196///    (immediately after the LAST table's pointer words, i.e. at
197///    `sum(size(0..num_tables)) * 4`), mirroring the pointer region slot
198///    for slot — table N's type-ids start at
199///    `R11 + type_ids_byte_offset + base_byte_offset(N)`. Each word is the
200///    slot's STRUCTURAL signature class id: structurally-equal function
201///    types share one dense id (1-based, first-occurrence order over the
202///    type section); id **0 is reserved for null slots**, so the type
203///    compare (expected ids are always >= 1) subsumes the #664 null trap
204///    in the same `CMP`. The dispatch loads `type_id[idx]`, compares it
205///    against the expected type's class id (compile-time immediate) and
206///    traps (`UDF`) on mismatch — WASM Core §4.4.8's runtime type check —
207///    before the pointer load and `BLX`. The sidecar words are emitted
208///    into the relocatable object as the `.synth.table_type_ids` section
209///    (non-ALLOC metadata, like `.meld_import_table`): the runtime/harness
210///    that links the pointer region copies them to
211///    `R11 + type_ids_byte_offset` verbatim — it never re-derives ids. A
212///    module with NO heterogeneous table emits no sidecar and no runtime
213///    type check anywhere: homogeneous dispatch bytes stay identical BY
214///    CONSTRUCTION (the #650 offset-0 / #664 `null_check: false` trick).
215///
216///    pre-#664 dispatch bytes identical BY CONSTRUCTION.
217///
218/// ## Companion: the self-contained SRAM layout contract (#687)
219///
220/// The R11 register above is ALSO the linear-memory base register, whose
221/// placement inside SRAM is governed by the self-contained image's stack
222/// layout: `--stack-layout=high` (default) keeps linmem at the SRAM start
223/// with the stack growing down from the top; `--stack-layout=low` reserves
224/// the stack at the SRAM BOTTOM and shifts linmem/globals (and the optimized
225/// path's absolute `0x2000_0100` base) up by the stack size, so an overflow
226/// BusFaults below SRAM instead of silently corrupting them. The full layout
227/// tables live on `build_multi_func_cortex_m_elf` in `synth-cli` (the builder
228/// that owns the addresses). Relocatable/host-linked objects are NOT covered
229/// — their linker script owns the layout, and the flag is refused there.
230#[derive(Debug, Clone, Default, PartialEq, Eq)]
231pub struct CallIndirectGuards {
232    /// Per-table guard inputs, indexed by table index (imports first). The
233    /// default (empty — no module context) DECLINES every `call_indirect`.
234    pub tables: Vec<TableGuards>,
235    /// #676: byte offset of the type-id sidecar within the R11 region — the
236    /// total pointer-region size, `sum(size(0..num_tables)) * 4`. `Some`
237    /// only when a sidecar exists: at least one table is heterogeneous
238    /// (see [`TableGuards::runtime_type_check`]) AND every table's size is
239    /// compile-time known (otherwise the sidecar base is not a constant and
240    /// heterogeneous dispatches keep declining). `None` = no sidecar.
241    pub type_ids_byte_offset: Option<u32>,
242    /// #676: the sidecar image — one `u32` structural class id per slot
243    /// across ALL tables in region order (0 = null slot). Emitted into the
244    /// object as `.synth.table_type_ids`; empty exactly when
245    /// `type_ids_byte_offset` is `None`. A table whose image is not
246    /// statically known contributes ZERO words (it declines at the
247    /// lowering, and 0 never equals an expected class id, so even a rogue
248    /// dispatch would trap, not branch).
249    pub type_ids_image: Vec<u32>,
250    /// #676: per module type index, that type's structural class id
251    /// (1-based, dense; structurally-equal duplicate types share an id).
252    /// The expected-type immediate the dispatch compares against. Empty
253    /// when `type_ids_byte_offset` is `None` (no sidecar — never consulted).
254    pub type_class_ids: Vec<u32>,
255}
256
257impl CallIndirectGuards {
258    /// Single-table (table 0 at R11 offset 0) guards — the pre-#650 shape,
259    /// used by tests and single-table call sites.
260    pub fn single_table(table_size: Option<u32>, type_reject: Vec<Option<String>>) -> Self {
261        Self {
262            tables: vec![TableGuards {
263                table_size,
264                base_byte_offset: Some(0),
265                type_reject,
266                has_null_slots: false,
267                runtime_type_check: false,
268            }],
269            ..Self::default()
270        }
271    }
272}
273
274/// Decoded WASM module with functions and memory
275#[derive(Debug, Clone)]
276pub struct DecodedModule {
277    /// Decoded functions
278    pub functions: Vec<FunctionOps>,
279    /// Linear memories
280    pub memories: Vec<WasmMemory>,
281    /// Data segments (offset, data) for memory initialization.
282    ///
283    /// MEMORY 0 ONLY — the legacy single-memory field every existing consumer
284    /// reads; its shape and contents are unchanged by multi-memory (#406).
285    /// Segments targeting memory > 0 live in
286    /// [`Self::extra_memory_data_segments`].
287    pub data_segments: Vec<(u32, Vec<u8>)>,
288    /// VCR-MEM-002 phase 1 (#406): active const-offset data segments on
289    /// NON-DEFAULT memories, as `(memory_index, offset, bytes)` with
290    /// `memory_index > 0`. Previously these were silently dropped (memory k
291    /// shipped uninitialized while its loads compiled). Declaration order.
292    pub extra_memory_data_segments: Vec<(u32, u32, Vec<u8>)>,
293    /// VCR-MEM-002 phase 1 (#406): `Some(reason)` when the module contains a
294    /// multi-memory shape decode cannot lower (e.g. an active data segment on
295    /// memory > 0 with a non-constant offset). The multi-memory compile path
296    /// must decline LOUDLY with this reason; single-memory modules never set
297    /// it.
298    pub multi_memory_decline: Option<String>,
299    /// Import entries (module name, field name, kind)
300    pub imports: Vec<ImportEntry>,
301    /// Number of imported functions (for distinguishing import calls from local calls)
302    pub num_imported_funcs: u32,
303    /// AAPCS integer-argument count per function, indexed by the *full* WASM
304    /// function index (imported functions first, then locally-defined ones).
305    /// Used by the backend to marshal call arguments into R0–R3 (issue #195).
306    /// Counts every parameter as one slot (i64/f64 over-counted — see the
307    /// backend's `set_func_arg_counts` scope note).
308    pub func_arg_counts: Vec<u32>,
309    /// AAPCS integer-argument count per *function type*, indexed by type index.
310    /// Used by `call_indirect`, whose callee arg count comes from the static
311    /// type index (issue #195).
312    pub type_arg_counts: Vec<u32>,
313    /// #311: whether each *function* (full index, imports first) returns i64 —
314    /// the call lowering must tag the result as a register PAIR (r0:r1) or the
315    /// hi half is invisible to liveness and the next constant clobbers it.
316    pub func_ret_i64: Vec<bool>,
317    /// #311: whether each *function type* returns i64 (for `call_indirect`).
318    pub type_ret_i64: Vec<bool>,
319    /// #359: declared parameter widths per *function* (full index, imports
320    /// first): `func_params_i64[f][k]` is true when param `k` is i64/f64. The
321    /// AAPCS stack-argument path needs the declared widths — op-stream inference
322    /// can't see an unused i64 param that still shifts the incoming-stack layout.
323    pub func_params_i64: Vec<Vec<bool>>,
324    /// GI-FPU-002 (#619/#369): declared f32-param mask per *function* (full
325    /// index, imports first): `func_params_f32[f][k]` is true when param `k` is
326    /// f32. The direct selector homes hard-float f32 args in S0..S15 (AAPCS-VFP),
327    /// which op-stream inference cannot recover for a pure-passthrough f32 param.
328    pub func_params_f32: Vec<Vec<bool>>,
329    /// GI-FPU-002 phase 2 (#369): declared f64-param mask per *function*
330    /// (full index, imports first): `func_params_f64[f][k]` is true when param
331    /// `k` is f64. Hard-float targets decline such functions loudly (the
332    /// legacy width inference treats the param as an i64 CORE pair — wrong
333    /// registers under AAPCS-VFP). Distinct from `func_params_i64`, which
334    /// deliberately lumps i64 and f64 for frame-layout purposes.
335    pub func_params_f64: Vec<Vec<bool>>,
336    /// GI-FPU-002 phase 2 (#719/#369): whether each *function* (full index,
337    /// imports first) returns f32. The direct selector's epilogue homes an f32
338    /// result in S0 (AAPCS-VFP); when the result value transited a core register
339    /// (e.g. it came from a call that returned f32 as an integer-tagged R0), the
340    /// epilogue must loudly decline rather than emit the integer R0 return (a
341    /// silent miscompile — the caller reads S0). Op-stream inference cannot see a
342    /// pure-passthrough f32 return, so it is carried from the declared signature.
343    pub func_ret_f32: Vec<bool>,
344    /// GI-FPU-002 phase 2 (#719/#369): whether each *function* returns f64 (D0
345    /// under AAPCS-VFP). Same epilogue-soundness role as `func_ret_f32`.
346    pub func_ret_f64: Vec<bool>,
347    /// GI-FPU-002 phase 2 (#719/#369): whether each *function type* returns
348    /// f32 / f64 — the `call_indirect` analogue of `func_ret_f32`/`func_ret_f64`
349    /// (the selector loudly declines an indirect call whose static type returns
350    /// a float this increment does not marshal, rather than tag S0/D0 as R0).
351    pub type_ret_f32: Vec<bool>,
352    /// See [`Self::type_ret_f32`].
353    pub type_ret_f64: Vec<bool>,
354    /// Defined globals with their initializers (#237). Empty if the module has
355    /// no global section. Used by the native-pointer ABI to make a global whose
356    /// initializer is a linear-memory address (e.g. `$__stack_pointer`)
357    /// self-contained rather than table-relative.
358    pub globals: Vec<WasmGlobal>,
359    /// Function indices that populate any table via an element segment (#275).
360    /// These are the possible `call_indirect` targets — a function reached only
361    /// through the table is invisible to direct-`call` reachability, so the
362    /// whole-graph closure must treat every table entry as reachable once any
363    /// reachable function performs a `call_indirect`. Empty for modules with no
364    /// element section (every leaf/direct-call module), keeping output identical.
365    pub elem_func_indices: Vec<u32>,
366    /// #642: compile-time size (in entries) of table 0 — `table_sizes[0]`,
367    /// kept as a convenience accessor. See [`Self::table_sizes`].
368    pub table_size: Option<u32>,
369    /// #650: compile-time size (in entries) per table, indexed by table index
370    /// (imported tables first, then the table section, in declaration order).
371    /// A DEFINED table's size is exact: `table.grow`/`table.set` are
372    /// unsupported ops (their functions loud-skip at decode), so nothing
373    /// synth compiles can resize or retype a table. An imported table only
374    /// yields a sound bound when its limits pin the size (`max == initial`);
375    /// otherwise its entry is `None` and the `call_indirect` lowering
376    /// declines (for that table AND for any later table, whose base offset
377    /// within the contiguous R11 region is then unknown).
378    pub table_sizes: Vec<Option<u32>>,
379    /// #642: per element segment, everything the closed-world `call_indirect`
380    /// type check needs. `offset` is the const i32 placement of an ACTIVE
381    /// segment into table `table_index` (`None` = passive/declared/non-const
382    /// offset — statically unverifiable placement); `funcs` are the segment's
383    /// function indices in slot order (`None` = an entry was not a plain
384    /// `ref.func`, e.g. `ref.null` — statically unverifiable contents).
385    pub elem_segments: Vec<ElemSegmentInfo>,
386    /// #642: type index per function, indexed by the FULL function index
387    /// (imports first, then locally-defined ones).
388    pub func_type_indices: Vec<u32>,
389    /// #851: result (return-value) count per function, indexed by the FULL
390    /// function index (imports first). `0` = void, `1` = single result, etc.
391    /// (saturated at 255 for pathological signatures). The aarch64 direct-`call`
392    /// lowering needs the 0-vs-1 distinction — `func_ret_i64/f32/f64` carry the
393    /// result TYPE but conflate void and i32 (both all-false), so they cannot say
394    /// whether a value is pushed back after the call.
395    pub func_result_counts: Vec<u32>,
396    /// #642: canonical structural signature per type index (params/results
397    /// rendered as a string) — used for the closed-world `call_indirect` type
398    /// check, which must compare SIGNATURES, not raw type indices (a module
399    /// may carry structurally-identical duplicate types).
400    pub type_signatures: Vec<String>,
401    /// VCR-PERF-002 Phase 1 (#494): proven invariants from loom's `wsc.facts`
402    /// custom section, keyed by `(function index, value id)` — see
403    /// `docs/design/wsc-facts-encoding.md` (schema v1) and
404    /// [`crate::wsc_facts::parse_wsc_facts`]. FAIL-SAFE by contract (loom#231
405    /// Q4): a missing/unparseable section or unknown version yields the empty
406    /// vec, unknown fact kinds are skipped — never a decode error. Phase 1 is
407    /// ingestion only: NO codegen path consumes these yet, so emitted bytes
408    /// are unchanged whether or not a module carries the section.
409    pub wsc_facts: Vec<crate::wsc_facts::WscFact>,
410}
411
412impl DecodedModule {
413    /// #642/#650: compute the `call_indirect` guard inputs — per table, the
414    /// compile-time size for the runtime bounds check, the base byte offset
415    /// within the contiguous R11 region, and the per-expected-type
416    /// closed-world verdict that discharges the type check at compile time.
417    /// See [`CallIndirectGuards`] for the layout contract and soundness
418    /// argument.
419    pub fn call_indirect_guards(&self) -> CallIndirectGuards {
420        let n_types = self.type_signatures.len();
421
422        // A segment whose PLACEMENT is not statically attributable
423        // (passive/declared segment, non-const offset, or a table index the
424        // module does not declare) poisons EVERY table: `table.init` (itself
425        // an unsupported op) or a computed offset could land its entries
426        // anywhere, so no table's image is verifiable.
427        let global_poison: Option<&'static str> = self
428            .elem_segments
429            .iter()
430            .any(|seg| seg.offset.is_none() || seg.table_index as usize >= self.table_sizes.len())
431            .then_some(
432                "element segment is not statically verifiable (passive/declared \
433                 segment, non-const offset, out-of-range table, or non-`ref.func` \
434                 entry)",
435            );
436
437        // #676: structural signature classes — structurally-equal types share
438        // one dense 1-based id (first-occurrence order over the type section);
439        // id 0 is reserved for null slots. These feed the type-id sidecar and
440        // the expected-type compare immediate of the runtime type check.
441        let mut class_of_sig: std::collections::HashMap<&str, u32> =
442            std::collections::HashMap::new();
443        let mut type_class_ids: Vec<u32> = Vec::with_capacity(n_types);
444        for sig in &self.type_signatures {
445            let next = class_of_sig.len() as u32 + 1;
446            type_class_ids.push(*class_of_sig.entry(sig.as_str()).or_insert(next));
447        }
448
449        let mut tables = Vec::with_capacity(self.table_sizes.len());
450        // #676: per table, the slot class ids (None = image not statically
451        // known) — concatenated into the sidecar image below.
452        let mut per_table_slot_ids: Vec<Option<Vec<u32>>> =
453            Vec::with_capacity(self.table_sizes.len());
454        // Running word offset of the next table's base within the R11 region;
455        // `None` once a table of unknown size is passed (every later base is
456        // then not a compile-time constant).
457        let mut base_words: Option<u32> = Some(0);
458        for (n, &size) in self.table_sizes.iter().enumerate() {
459            let base_byte_offset = base_words.and_then(|w| w.checked_mul(4));
460            let (type_reject, has_null_slots, slot_class_ids) =
461                self.table_type_reject(n as u32, size, global_poison, n_types, &type_class_ids);
462            // #676: heterogeneous = the image is statically known and its
463            // INITIALIZED slots span >= 2 distinct structural classes (null
464            // slots — id 0 — don't count; a sparse homogeneous table stays
465            // on the #664 verified-plus-null-check path, bytes identical).
466            let runtime_type_check = slot_class_ids.as_ref().is_some_and(|ids| {
467                let mut distinct: Vec<u32> = ids.iter().copied().filter(|&c| c != 0).collect();
468                distinct.sort_unstable();
469                distinct.dedup();
470                distinct.len() >= 2
471            });
472            per_table_slot_ids.push(slot_class_ids);
473            tables.push(TableGuards {
474                table_size: size,
475                base_byte_offset,
476                type_reject,
477                has_null_slots,
478                runtime_type_check,
479            });
480            base_words = match (base_words, size) {
481                (Some(w), Some(s)) => w.checked_add(s),
482                _ => None,
483            };
484        }
485
486        // #676: the sidecar exists only when some table actually needs the
487        // runtime check AND the whole pointer region's size is compile-time
488        // known (`base_words` survived every table) — otherwise the sidecar
489        // base is not a constant and heterogeneous dispatches keep declining
490        // (their `runtime_type_check` flag is cleared so the lowering sees a
491        // plain reject).
492        let any_hetero = tables.iter().any(|t| t.runtime_type_check);
493        let type_ids_byte_offset = base_words
494            .filter(|_| any_hetero)
495            .and_then(|w| w.checked_mul(4));
496        let type_ids_image = if type_ids_byte_offset.is_some() {
497            self.table_sizes
498                .iter()
499                .zip(&per_table_slot_ids)
500                .flat_map(|(&size, ids)| match ids {
501                    Some(ids) => ids.clone(),
502                    // Image not statically known: zero words (id 0 never
503                    // matches an expected class id >= 1 — trap, not branch).
504                    None => vec![0u32; size.unwrap_or(0) as usize],
505                })
506                .collect()
507        } else {
508            for t in &mut tables {
509                t.runtime_type_check = false;
510            }
511            Vec::new()
512        };
513        CallIndirectGuards {
514            tables,
515            type_ids_byte_offset,
516            type_ids_image,
517            type_class_ids: if type_ids_byte_offset.is_some() {
518                type_class_ids
519            } else {
520                Vec::new()
521            },
522        }
523    }
524
525    /// #275: the STATIC image of the contiguous funcref region — one slot per
526    /// table entry across ALL tables in declaration order (the exact layout
527    /// [`CallIndirectGuards`]' `base_byte_offset` contract describes), each
528    /// `Some(full_function_index)` for a statically-known initialized slot
529    /// and `None` for a null (or not statically attributable) slot. The
530    /// self-contained image builder resolves each `Some` to the laid-out
531    /// function address (Thumb bit set) and links every `None` as a ZERO
532    /// word, which the dispatch's #664 null check / #676 id-0 compare traps.
533    ///
534    /// Mirrors the reconstruction in [`Self::call_indirect_guards`]:
535    ///  - stops at the first table with no compile-time size (later tables
536    ///    have no constant base offset, so no dispatch can reach them);
537    ///  - a table whose segments are not statically verifiable (non-const
538    ///    offset, non-`ref.func` entry, out-of-range write) contributes
539    ///    all-`None` slots — every dispatch into it declines at the lowering
540    ///    anyway, and a rogue read traps on the zero word rather than branch.
541    pub fn funcref_region_slots(&self) -> Vec<Option<u32>> {
542        let mut region: Vec<Option<u32>> = Vec::new();
543        for (n, &size) in self.table_sizes.iter().enumerate() {
544            let Some(size) = size else { break };
545            let mut slots: Vec<Option<u32>> = vec![None; size as usize];
546            let mut verifiable = true;
547            for seg in self
548                .elem_segments
549                .iter()
550                .filter(|s| s.table_index == n as u32)
551            {
552                let (Some(off), Some(funcs)) = (seg.offset, seg.funcs.as_ref()) else {
553                    verifiable = false;
554                    break;
555                };
556                for (k, &f) in funcs.iter().enumerate() {
557                    match slots.get_mut(off as usize + k) {
558                        Some(slot) => *slot = Some(f),
559                        None => {
560                            // Writes past the declared size: the guards
561                            // reject this table; ship all-null slots.
562                            verifiable = false;
563                            break;
564                        }
565                    }
566                }
567                if !verifiable {
568                    break;
569                }
570            }
571            if !verifiable {
572                slots = vec![None; size as usize];
573            }
574            region.extend(slots);
575        }
576        region
577    }
578
579    /// #642/#650: the closed-world type verdicts for ONE table — `None` per
580    /// expected type when every INITIALIZED slot of table `n` verifiably
581    /// holds a function of that exact structural signature; `Some(reason)`
582    /// otherwise. The second component is `has_null_slots` (#664): whether
583    /// the table image left any slot uninitialized — a `call_indirect`
584    /// reaching one must TRAP at runtime (null check on the loaded pointer),
585    /// which the lowering emits only when this is set. Reject paths return
586    /// `false` (the verdict declines before the flag is consulted). The
587    /// third component (#676) is the table's slot class ids — per slot, the
588    /// structural signature class of the initializing function (0 for a
589    /// null slot) — `Some` exactly when the table image is statically
590    /// known; it feeds the type-id sidecar and the heterogeneity verdict.
591    fn table_type_reject(
592        &self,
593        n: u32,
594        size: Option<u32>,
595        global_poison: Option<&str>,
596        n_types: usize,
597        type_class_ids: &[u32],
598    ) -> (Vec<Option<String>>, bool, Option<Vec<u32>>) {
599        let reject_all = |reason: String| (vec![Some(reason); n_types], false, None);
600
601        if let Some(reason) = global_poison {
602            return reject_all(reason.to_string());
603        }
604        let Some(size) = size else {
605            return reject_all(format!(
606                "table {n} has no compile-time-fixed size (imported table with \
607                 growable limits)"
608            ));
609        };
610
611        // Reconstruct the table image: slot -> initializing function index.
612        let mut slots: Vec<Option<u32>> = vec![None; size as usize];
613        for seg in self.elem_segments.iter().filter(|s| s.table_index == n) {
614            let (Some(off), Some(funcs)) = (seg.offset, seg.funcs.as_ref()) else {
615                // Placement is known (global_poison ruled `offset: None` out),
616                // so this is an unverifiable CONTENTS case — it poisons only
617                // the table it targets.
618                return reject_all(format!(
619                    "element segment targeting table {n} is not statically \
620                     verifiable (non-`ref.func` entry)"
621                ));
622            };
623            for (k, &f) in funcs.iter().enumerate() {
624                let Some(slot) = slots.get_mut(off as usize + k) else {
625                    return reject_all(format!(
626                        "element segment (offset {off}, {} entries) writes past \
627                         table {n}'s declared size {size}",
628                        funcs.len()
629                    ));
630                };
631                *slot = Some(f);
632            }
633        }
634        // #664: an uninitialized slot is a null funcref — calling it must
635        // trap (WASM Core §4.4.8). It no longer poisons the closed world
636        // (pre-#664 it rejected EVERY type): the layout contract requires
637        // null slots to be linked as ZERO words, so the lowering discharges
638        // the trap at RUNTIME with a null check on the loaded pointer. The
639        // type check below therefore covers the INITIALIZED slots only —
640        // a null slot can never produce a live callee of the wrong type,
641        // because the null check traps before the branch.
642        let has_null_slots = slots.iter().any(|s| s.is_none());
643
644        let rejects = (0..n_types)
645            .map(|t| {
646                for f in slots.iter().flatten() {
647                    let Some(&fty) = self.func_type_indices.get(*f as usize) else {
648                        return Some(format!(
649                            "table {n} entry references function {f} with no known type"
650                        ));
651                    };
652                    if self.type_signatures.get(fty as usize) != self.type_signatures.get(t) {
653                        return Some(format!(
654                            "table {n} entry (function {f}, type {fty}) has a different \
655                             signature than expected type {t}"
656                        ));
657                    }
658                }
659                None
660            })
661            .collect();
662        // #676: per-slot structural class ids (0 = null). `None` as soon as
663        // any initializing function's type is unknown — the image is then
664        // not statically classifiable and the table can neither verify nor
665        // carry the runtime check (the rejects above already name it).
666        let slot_class_ids: Option<Vec<u32>> = slots
667            .iter()
668            .map(|s| match s {
669                None => Some(0u32),
670                Some(f) => self
671                    .func_type_indices
672                    .get(*f as usize)
673                    .and_then(|&fty| type_class_ids.get(fty as usize).copied()),
674            })
675            .collect();
676        (rejects, has_null_slots, slot_class_ids)
677    }
678}
679
680/// Decode a WASM binary and extract functions, memory, and data segments
681pub fn decode_wasm_module(wasm_bytes: &[u8]) -> Result<DecodedModule> {
682    let mut functions = Vec::new();
683    let mut memories = Vec::new();
684    let mut data_segments = Vec::new();
685    // VCR-MEM-002 phase 1 (#406): (memory_index, offset, bytes) for active
686    // const-offset data segments on memory > 0, and the first decode-level
687    // reason multi-memory lowering must be declined (if any).
688    let mut extra_memory_data_segments: Vec<(u32, u32, Vec<u8>)> = Vec::new();
689    let mut multi_memory_decline: Option<String> = None;
690    let mut globals: Vec<WasmGlobal> = Vec::new();
691    let mut imports = Vec::new();
692    let mut func_index = 0u32;
693    let mut num_imported_funcs = 0u32;
694    let mut export_names: HashMap<u32, String> = HashMap::new();
695    // #195: per-type AAPCS arg count (indexed by type index) and per-function
696    // arg count (indexed by full function index: imports first, then locals).
697    let mut type_arg_counts: Vec<u32> = Vec::new();
698    let mut func_arg_counts: Vec<u32> = Vec::new();
699    let mut type_ret_i64: Vec<bool> = Vec::new();
700    let mut func_ret_i64: Vec<bool> = Vec::new();
701    // GI-FPU-002 phase 2 (#719/#369): per-type / per-function f32/f64 return
702    // flags, so the direct selector's epilogue can loudly decline an f32/f64
703    // result that reaches it in a core register (never a silent R0 return).
704    let mut type_ret_f32: Vec<bool> = Vec::new();
705    let mut func_ret_f32: Vec<bool> = Vec::new();
706    let mut type_ret_f64: Vec<bool> = Vec::new();
707    let mut func_ret_f64: Vec<bool> = Vec::new();
708    // #359: declared param widths per type / per function (full index).
709    let mut type_params_i64: Vec<Vec<bool>> = Vec::new();
710    let mut func_params_i64: Vec<Vec<bool>> = Vec::new();
711    // GI-FPU-002 (#619/#369): per-type / per-function declared f32-param mask,
712    // so the direct selector can home hard-float (AAPCS-VFP) f32 args in S0..S15
713    // instead of the core-register (R0..R3) integer path. Independent of
714    // `params_i64` (which lumps f64 with i64): an f32 param is neither.
715    let mut type_params_f32: Vec<Vec<bool>> = Vec::new();
716    let mut func_params_f32: Vec<Vec<bool>> = Vec::new();
717    // GI-FPU-002 phase 2 (#369): per-type / per-function f64-param mask —
718    // hard-float targets decline f64 params loudly (D-register homing is a
719    // later increment; the legacy i64-pair treatment reads wrong registers).
720    let mut type_params_f64: Vec<Vec<bool>> = Vec::new();
721    let mut func_params_f64: Vec<Vec<bool>> = Vec::new();
722    // #509: (param_count, result_count) per type index, for FuncType blocktypes.
723    let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
724    let mut elem_func_indices: Vec<u32> = Vec::new();
725    // #642/#650: call_indirect guard inputs — per-table fixed sizes (imports
726    // first, then the table section, in declaration order), per-segment
727    // static shapes, per-function type index, per-type canonical signature.
728    let mut table_sizes: Vec<Option<u32>> = Vec::new();
729    let mut elem_segments: Vec<ElemSegmentInfo> = Vec::new();
730    let mut func_type_indices: Vec<u32> = Vec::new();
731    let mut type_signatures: Vec<String> = Vec::new();
732    // #394 Tier-1.x: function index → developer-facing name from the wasm
733    // `name` custom section (function-names subsection). Applied to
734    // `FunctionOps.debug_name` after the parse loop — the custom section
735    // conventionally trails the code section, so the entries are not yet
736    // available when each `CodeSectionEntry` is decoded.
737    let mut name_section_names: HashMap<u32, String> = HashMap::new();
738    // VCR-PERF-002 Phase 1 (#494): facts from loom's `wsc.facts` custom
739    // section. `None` until (and unless) the first such section is seen —
740    // duplicates are ignored (one prover, one section; encoding doc rule).
741    let mut wsc_facts: Option<Vec<crate::wsc_facts::WscFact>> = None;
742    // GI-FPU-001 (#369): f32/f64-typed globals in the FULL global index space
743    // (imports first, then defined). `global.get`/`global.set` decode fine
744    // (they are type-agnostic ops), but there is no float lowering: the
745    // f32.const/f64.const initializer is silently dropped (`init_i32: None`
746    // → slot zeroed), so a read returns 0.0 instead of the init — a silent
747    // wrong value. Functions touching a float global loud-skip instead.
748    let mut num_imported_globals = 0u32;
749    let mut float_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
750    // #680: v128-typed globals (same index space) — a SIMD access has no
751    // lowering on any target, so touching one must loud-skip the function.
752    let mut v128_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
753    // #680: per-type "params/results contain v128" and its per-defined-function
754    // projection — a v128 param/result is expressible with ZERO SIMD-proposal
755    // operators in the body (`local.get 0` passthrough), so the operator-level
756    // catch alone would miss it.
757    let mut type_has_v128: Vec<bool> = Vec::new();
758    let mut func_sig_has_v128: Vec<bool> = Vec::new();
759
760    for payload in Parser::new(0).parse_all(wasm_bytes) {
761        let payload = payload.context("Failed to parse WASM payload")?;
762
763        match payload {
764            Payload::TypeSection(reader) => {
765                // Record the parameter count of each function type so calls can
766                // marshal the right number of arguments (issue #195).
767                for rec_group in reader {
768                    let rec_group = rec_group.context("Failed to parse type")?;
769                    for sub_ty in rec_group.types() {
770                        // #509: blocktype arity per type index (saturated u8 —
771                        // >255 params/results is far beyond anything the
772                        // selector supports anyway, and the selector declines
773                        // rather than trusting a saturated count).
774                        type_block_arity.push(match &sub_ty.composite_type.inner {
775                            wasmparser::CompositeInnerType::Func(f) => (
776                                u8::try_from(f.params().len()).unwrap_or(u8::MAX),
777                                u8::try_from(f.results().len()).unwrap_or(u8::MAX),
778                            ),
779                            _ => (u8::MAX, u8::MAX),
780                        });
781                        let (count, ret_i64, params_i64) = match &sub_ty.composite_type.inner {
782                            wasmparser::CompositeInnerType::Func(func_ty) => (
783                                func_ty.params().len() as u32,
784                                func_ty
785                                    .results()
786                                    .first()
787                                    .is_some_and(|t| *t == wasmparser::ValType::I64),
788                                // #359: i64/f64 params occupy 8 bytes / a register
789                                // pair under AAPCS. f32/f64 are not in scope for the
790                                // stack-arg path (refused), but mark both 64-bit
791                                // float and i64 so the guard catches them.
792                                func_ty
793                                    .params()
794                                    .iter()
795                                    .map(|t| {
796                                        matches!(
797                                            t,
798                                            wasmparser::ValType::I64 | wasmparser::ValType::F64
799                                        )
800                                    })
801                                    .collect::<Vec<bool>>(),
802                            ),
803                            _ => (0, false, Vec::new()),
804                        };
805                        // GI-FPU-002: declared f32-param mask for this type.
806                        let params_f32 = match &sub_ty.composite_type.inner {
807                            wasmparser::CompositeInnerType::Func(func_ty) => func_ty
808                                .params()
809                                .iter()
810                                .map(|t| matches!(t, wasmparser::ValType::F32))
811                                .collect::<Vec<bool>>(),
812                            _ => Vec::new(),
813                        };
814                        // GI-FPU-002 phase 2: declared f64-param mask.
815                        let params_f64 = match &sub_ty.composite_type.inner {
816                            wasmparser::CompositeInnerType::Func(func_ty) => func_ty
817                                .params()
818                                .iter()
819                                .map(|t| matches!(t, wasmparser::ValType::F64))
820                                .collect::<Vec<bool>>(),
821                            _ => Vec::new(),
822                        };
823                        // GI-FPU-002 phase 2: f32/f64 return flags for this type.
824                        let (ret_f32, ret_f64) = match &sub_ty.composite_type.inner {
825                            wasmparser::CompositeInnerType::Func(func_ty) => (
826                                func_ty
827                                    .results()
828                                    .first()
829                                    .is_some_and(|t| *t == wasmparser::ValType::F32),
830                                func_ty
831                                    .results()
832                                    .first()
833                                    .is_some_and(|t| *t == wasmparser::ValType::F64),
834                            ),
835                            _ => (false, false),
836                        };
837                        type_arg_counts.push(count);
838                        type_ret_i64.push(ret_i64);
839                        type_ret_f32.push(ret_f32);
840                        type_ret_f64.push(ret_f64);
841                        type_params_i64.push(params_i64);
842                        type_params_f32.push(params_f32);
843                        type_params_f64.push(params_f64);
844                        // #680: v128 anywhere in the signature.
845                        type_has_v128.push(match &sub_ty.composite_type.inner {
846                            wasmparser::CompositeInnerType::Func(f) => f
847                                .params()
848                                .iter()
849                                .chain(f.results())
850                                .any(|t| *t == wasmparser::ValType::V128),
851                            _ => false,
852                        });
853                        // #642: canonical structural signature for the
854                        // closed-world call_indirect type check (compares
855                        // SIGNATURES so duplicate types stay interchangeable).
856                        type_signatures.push(match &sub_ty.composite_type.inner {
857                            wasmparser::CompositeInnerType::Func(f) => {
858                                format!("{:?}->{:?}", f.params(), f.results())
859                            }
860                            other => format!("non-func:{other:?}"),
861                        });
862                    }
863                }
864            }
865            Payload::ImportSection(reader) => {
866                // wasmparser 0.221+ groups imports (the "compact imports"
867                // proposal): the section reader yields `Imports` groups, each of
868                // which may expand to several `Import`s. `into_imports()`
869                // flattens groups back to individual `Import`s (preserving the
870                // module/name/ty fields), keeping the per-import loop intact.
871                for import in reader.into_imports() {
872                    let import = import.context("Failed to parse import")?;
873                    let (kind, idx) = match import.ty {
874                        wasmparser::TypeRef::Func(type_idx) => {
875                            let idx = num_imported_funcs;
876                            num_imported_funcs += 1;
877                            // Record the imported function's arg count at its
878                            // full function index (imports come first).
879                            func_type_indices.push(type_idx); // #642
880                            func_arg_counts
881                                .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
882                            func_ret_i64.push(
883                                type_ret_i64
884                                    .get(type_idx as usize)
885                                    .copied()
886                                    .unwrap_or(false),
887                            );
888                            func_ret_f32.push(
889                                type_ret_f32
890                                    .get(type_idx as usize)
891                                    .copied()
892                                    .unwrap_or(false),
893                            );
894                            func_ret_f64.push(
895                                type_ret_f64
896                                    .get(type_idx as usize)
897                                    .copied()
898                                    .unwrap_or(false),
899                            );
900                            func_params_i64.push(
901                                type_params_i64
902                                    .get(type_idx as usize)
903                                    .cloned()
904                                    .unwrap_or_default(),
905                            );
906                            func_params_f32.push(
907                                type_params_f32
908                                    .get(type_idx as usize)
909                                    .cloned()
910                                    .unwrap_or_default(),
911                            );
912                            func_params_f64.push(
913                                type_params_f64
914                                    .get(type_idx as usize)
915                                    .cloned()
916                                    .unwrap_or_default(),
917                            );
918                            (ImportKind::Function(type_idx), idx)
919                        }
920                        wasmparser::TypeRef::Memory(_) => (ImportKind::Memory, 0),
921                        wasmparser::TypeRef::Table(t) => {
922                            // #642: an imported table only yields a SOUND
923                            // compile-time bound when its limits pin the size
924                            // exactly (max == initial) — a growable import
925                            // could be larger at runtime, and a bounds guard
926                            // against `initial` would trap spec-valid calls.
927                            // #650: imported tables take the leading table
928                            // indices, in declaration order.
929                            table_sizes.push(match (u32::try_from(t.initial), t.maximum) {
930                                (Ok(init), Some(max)) if u64::from(init) == max => Some(init),
931                                _ => None,
932                            });
933                            (ImportKind::Table, 0)
934                        }
935                        wasmparser::TypeRef::Global(g) => {
936                            // GI-FPU-001 (#369): imported globals come first in
937                            // the global index space — record float-typed ones
938                            // so accesses loud-skip their function.
939                            if matches!(
940                                g.content_type,
941                                wasmparser::ValType::F32 | wasmparser::ValType::F64
942                            ) {
943                                float_globals.insert(num_imported_globals);
944                            }
945                            // #680: v128-typed imported globals — same lane.
946                            if g.content_type == wasmparser::ValType::V128 {
947                                v128_globals.insert(num_imported_globals);
948                            }
949                            num_imported_globals += 1;
950                            (ImportKind::Global, 0)
951                        }
952                        _ => continue,
953                    };
954                    imports.push(ImportEntry {
955                        module: import.module.to_string(),
956                        name: import.name.to_string(),
957                        kind,
958                        index: idx,
959                    });
960                }
961            }
962            Payload::FunctionSection(reader) => {
963                // Each entry gives the type index of a locally-defined function,
964                // in order. Their full function indices follow the imports, so
965                // appending to `func_arg_counts` keeps it indexed by full index
966                // (issue #195).
967                for ty in reader {
968                    let type_idx = ty.context("Failed to parse function type index")?;
969                    func_type_indices.push(type_idx); // #642
970                    func_arg_counts
971                        .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
972                    func_ret_i64.push(
973                        type_ret_i64
974                            .get(type_idx as usize)
975                            .copied()
976                            .unwrap_or(false),
977                    );
978                    func_ret_f32.push(
979                        type_ret_f32
980                            .get(type_idx as usize)
981                            .copied()
982                            .unwrap_or(false),
983                    );
984                    func_ret_f64.push(
985                        type_ret_f64
986                            .get(type_idx as usize)
987                            .copied()
988                            .unwrap_or(false),
989                    );
990                    func_params_i64.push(
991                        type_params_i64
992                            .get(type_idx as usize)
993                            .cloned()
994                            .unwrap_or_default(),
995                    );
996                    func_params_f32.push(
997                        type_params_f32
998                            .get(type_idx as usize)
999                            .cloned()
1000                            .unwrap_or_default(),
1001                    );
1002                    func_params_f64.push(
1003                        type_params_f64
1004                            .get(type_idx as usize)
1005                            .cloned()
1006                            .unwrap_or_default(),
1007                    );
1008                    // #680: defined-function order matches code-entry order.
1009                    func_sig_has_v128.push(
1010                        type_has_v128
1011                            .get(type_idx as usize)
1012                            .copied()
1013                            .unwrap_or(false),
1014                    );
1015                }
1016            }
1017            Payload::TableSection(reader) => {
1018                // #642: a DEFINED table's compile-time size is exact — its
1019                // initial size is its permanent size, because nothing synth
1020                // compiles can resize it (`table.grow` is an unsupported op
1021                // whose function loud-skips at decode). #650: EVERY table is
1022                // recorded — the contiguous R11 region places table N at
1023                // byte offset `sum(size(0..N)) * 4`.
1024                for table in reader {
1025                    let table = table.context("Failed to parse table")?;
1026                    table_sizes.push(u32::try_from(table.ty.initial).ok());
1027                }
1028            }
1029            Payload::MemorySection(reader) => {
1030                for (idx, memory) in reader.into_iter().enumerate() {
1031                    let mem = memory.context("Failed to parse memory")?;
1032                    memories.push(WasmMemory {
1033                        index: idx as u32,
1034                        initial_pages: mem.initial as u32,
1035                        max_pages: mem.maximum.map(|m| m as u32),
1036                        shared: mem.shared,
1037                    });
1038                }
1039            }
1040            Payload::GlobalSection(reader) => {
1041                // #237/#649: capture each defined global's constant initializer
1042                // + mutability. The init is a const expr; we decode a leading
1043                // `i32.const` (the `$__stack_pointer`/data-layout shape) or
1044                // `i64.const` (#649: capturing only i32 silently ZEROED every
1045                // nonzero i64 init). f32/f64 inits stay `None` on purpose —
1046                // float global access is the GI-FPU-001 (#369) loud-skip lane —
1047                // as do non-const init exprs (`global.get` of an import).
1048                for (idx, global) in reader.into_iter().enumerate() {
1049                    let global = global.context("Failed to parse global")?;
1050                    let mut ops = global.init_expr.get_operators_reader();
1051                    let init = match ops.read() {
1052                        Ok(wasmparser::Operator::I32Const { value }) => {
1053                            Some(GlobalInit::I32(value))
1054                        }
1055                        Ok(wasmparser::Operator::I64Const { value }) => {
1056                            Some(GlobalInit::I64(value))
1057                        }
1058                        _ => None,
1059                    };
1060                    // #643: record the slot width from the DECLARED value type.
1061                    // i64/f64 globals occupy 8 bytes (a register pair on the
1062                    // 32-bit targets), v128 sixteen; laying every global out at
1063                    // `index * 4` silently dropped the high word of every i64.
1064                    let slot_bytes = match global.ty.content_type {
1065                        wasmparser::ValType::I64 | wasmparser::ValType::F64 => 8,
1066                        wasmparser::ValType::V128 => 16,
1067                        _ => 4,
1068                    };
1069                    // GI-FPU-001 (#369): a float-typed global's initializer is
1070                    // NOT captured (`init_i32` only decodes `i32.const`), so
1071                    // its slot would be silently zeroed — record it so any
1072                    // function accessing it loud-skips instead of reading a
1073                    // silently-wrong 0.0.
1074                    if matches!(
1075                        global.ty.content_type,
1076                        wasmparser::ValType::F32 | wasmparser::ValType::F64
1077                    ) {
1078                        float_globals.insert(num_imported_globals + idx as u32);
1079                    }
1080                    // #680: a v128-typed global's `v128.const` initializer is
1081                    // not captured either (slot zeroed) and an access moves 4
1082                    // of the 16 bytes — record it so accesses loud-skip.
1083                    if global.ty.content_type == wasmparser::ValType::V128 {
1084                        v128_globals.insert(num_imported_globals + idx as u32);
1085                    }
1086                    globals.push(WasmGlobal {
1087                        index: idx as u32,
1088                        init,
1089                        mutable: global.ty.mutable,
1090                        slot_bytes,
1091                    });
1092                }
1093            }
1094            Payload::DataSection(reader) => {
1095                for data in reader {
1096                    let data = data.context("Failed to parse data segment")?;
1097                    if let wasmparser::DataKind::Active {
1098                        memory_index,
1099                        offset_expr,
1100                    } = data.kind
1101                    {
1102                        let mut ops = offset_expr.get_operators_reader();
1103                        let const_off = match ops.read() {
1104                            Ok(wasmparser::Operator::I32Const { value }) => Some(value as u32),
1105                            _ => None,
1106                        };
1107                        if memory_index == 0 {
1108                            // Memory-0 behavior unchanged (frozen): a const-
1109                            // offset segment is captured, anything else keeps
1110                            // the legacy drop.
1111                            if let Some(off) = const_off {
1112                                data_segments.push((off, data.data.to_vec()));
1113                            }
1114                        } else if let Some(off) = const_off {
1115                            // VCR-MEM-002 phase 1 (#406): capture non-default-
1116                            // memory segments — previously they were silently
1117                            // DROPPED (memory k's init data never shipped).
1118                            extra_memory_data_segments.push((
1119                                memory_index,
1120                                off,
1121                                data.data.to_vec(),
1122                            ));
1123                        } else {
1124                            // A non-const offset on a non-default memory cannot
1125                            // be placed at compile time — record it so the
1126                            // multi-memory compile path declines LOUDLY instead
1127                            // of shipping memory k uninitialized.
1128                            multi_memory_decline.get_or_insert(format!(
1129                                "active data segment on memory {memory_index} has a \
1130                                 non-constant offset expression — cannot be placed \
1131                                 at compile time (multi-memory phase 1, #406)"
1132                            ));
1133                        }
1134                    }
1135                }
1136            }
1137            Payload::ElementSection(reader) => {
1138                // #275: collect every function index that initializes a table.
1139                // These are the `call_indirect` targets the direct-call closure
1140                // cannot see; `reachable_from_exports` unions them in when a
1141                // reachable function does a `call_indirect`. Both element forms
1142                // are handled: a flat function-index list, and the const-expr
1143                // form whose `ref.func` entries name the functions.
1144                for elem in reader {
1145                    let elem = elem.context("Failed to parse element segment")?;
1146                    // #642/#650: the segment's static placement — a const i32
1147                    // offset of an ACTIVE segment into its target table (any
1148                    // table index: the R11 region is contiguous, #650);
1149                    // anything else is unverifiable and poisons the
1150                    // closed-world type check.
1151                    let (seg_table, seg_offset): (u32, Option<u32>) = match &elem.kind {
1152                        wasmparser::ElementKind::Active {
1153                            table_index,
1154                            offset_expr,
1155                        } => {
1156                            let mut ops = offset_expr.get_operators_reader();
1157                            let off = match ops.read() {
1158                                Ok(wasmparser::Operator::I32Const { value }) => {
1159                                    u32::try_from(value).ok()
1160                                }
1161                                _ => None,
1162                            };
1163                            (table_index.unwrap_or(0), off)
1164                        }
1165                        _ => (0, None),
1166                    };
1167                    let mut seg_funcs: Option<Vec<u32>> = Some(Vec::new());
1168                    match elem.items {
1169                        wasmparser::ElementItems::Functions(funcs) => {
1170                            for f in funcs {
1171                                let f = f.context("Failed to parse element func index")?;
1172                                elem_func_indices.push(f);
1173                                if let Some(v) = seg_funcs.as_mut() {
1174                                    v.push(f);
1175                                }
1176                            }
1177                        }
1178                        wasmparser::ElementItems::Expressions(_, exprs) => {
1179                            for expr in exprs {
1180                                let expr = expr.context("Failed to parse element expr")?;
1181                                // #642: an entry is verifiable only when it is
1182                                // a single plain `ref.func` (reader yields the
1183                                // op + the implicit `end`). `ref.null` or any
1184                                // computed entry poisons the segment.
1185                                let mut entry_func: Option<u32> = None;
1186                                let mut plain = true;
1187                                for (k, op) in expr.get_operators_reader().into_iter().enumerate() {
1188                                    match (k, op.context("Failed to parse element op")?) {
1189                                        (0, wasmparser::Operator::RefFunc { function_index }) => {
1190                                            elem_func_indices.push(function_index);
1191                                            entry_func = Some(function_index);
1192                                        }
1193                                        (_, wasmparser::Operator::End) => {}
1194                                        (_, wasmparser::Operator::RefFunc { function_index }) => {
1195                                            // Keep the pre-#642 reachability
1196                                            // behaviour: every ref.func seen
1197                                            // anywhere is a possible target.
1198                                            elem_func_indices.push(function_index);
1199                                            plain = false;
1200                                        }
1201                                        _ => plain = false,
1202                                    }
1203                                }
1204                                match (plain, entry_func, seg_funcs.as_mut()) {
1205                                    (true, Some(f), Some(v)) => v.push(f),
1206                                    _ => seg_funcs = None,
1207                                }
1208                            }
1209                        }
1210                    }
1211                    elem_segments.push(ElemSegmentInfo {
1212                        table_index: seg_table,
1213                        offset: seg_offset,
1214                        funcs: seg_funcs,
1215                    });
1216                }
1217            }
1218            Payload::ExportSection(exports) => {
1219                for export in exports {
1220                    let export = export.context("Failed to parse export")?;
1221                    if export.kind == ExternalKind::Func {
1222                        export_names.insert(export.index, export.name.to_string());
1223                    }
1224                }
1225            }
1226            Payload::CodeSectionEntry(body) => {
1227                let (ops, op_offsets, block_arity, mut unsupported) =
1228                    decode_function_body(&body, &type_block_arity, &float_globals, &v128_globals)?;
1229                // #680: a v128 param/result reaches the body only through
1230                // type-agnostic ops (a `local.get 0` passthrough compiles to
1231                // a 4-byte `mov`), so flag the SIGNATURE even when the body
1232                // contains no SIMD-proposal operator.
1233                if unsupported.is_none()
1234                    && func_sig_has_v128
1235                        .get(func_index as usize)
1236                        .copied()
1237                        .unwrap_or(false)
1238                {
1239                    unsupported = Some(
1240                        "signature has a v128 param/result — no SIMD lowering \
1241                         for this target (#680)"
1242                            .to_string(),
1243                    );
1244                }
1245                let actual_index = num_imported_funcs + func_index;
1246                let export_name = export_names.get(&actual_index).cloned();
1247
1248                functions.push(FunctionOps {
1249                    index: actual_index,
1250                    export_name,
1251                    debug_name: None, // filled from the `name` section after the loop
1252                    ops,
1253                    op_offsets,
1254                    unsupported,
1255                    block_arity,
1256                });
1257                func_index += 1;
1258            }
1259            Payload::CustomSection(c) => {
1260                // #394 Tier-1.x: the wasm `name` custom section.
1261                if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
1262                    parse_name_section_func_names(reader, &mut name_section_names);
1263                }
1264                // VCR-PERF-002 Phase 1 (#494): loom's `wsc.facts` section.
1265                // `parse_wsc_facts` is TOTAL (fail-safe skew, loom#231 Q4):
1266                // any malformed payload decodes to the empty fact list WITH a
1267                // stderr diagnostic, never an error — facts are optional
1268                // accelerators and must not be able to change a compilation
1269                // outcome. First section wins.
1270                if c.name() == crate::wsc_facts::WSC_FACTS_SECTION_NAME && wsc_facts.is_none() {
1271                    let parsed = crate::wsc_facts::parse_wsc_facts(c.data());
1272                    if let Some(reason) = &parsed.section_ignored {
1273                        eprintln!(
1274                            "warning: ignoring unparseable `wsc.facts` custom section \
1275                             ({reason}) — facts are optional accelerators, compilation \
1276                             is unaffected (#494 fail-safe skew rule)"
1277                        );
1278                    } else if parsed.records_skipped > 0 {
1279                        eprintln!(
1280                            "warning: skipped {} unknown/undecodable `wsc.facts` \
1281                             record(s) (likely a newer loom emitter); {} known fact(s) \
1282                             kept, compilation is unaffected (#494 fail-safe skew rule)",
1283                            parsed.records_skipped,
1284                            parsed.facts.len()
1285                        );
1286                    }
1287                    wsc_facts = Some(parsed.facts);
1288                }
1289            }
1290            _ => {}
1291        }
1292    }
1293
1294    apply_name_section(&mut functions, &name_section_names);
1295
1296    Ok(DecodedModule {
1297        functions,
1298        memories,
1299        data_segments,
1300        extra_memory_data_segments,
1301        multi_memory_decline,
1302        imports,
1303        num_imported_funcs,
1304        func_arg_counts,
1305        type_arg_counts,
1306        func_ret_i64,
1307        type_ret_i64,
1308        func_params_i64,
1309        func_params_f32,
1310        func_params_f64,
1311        func_ret_f32,
1312        func_ret_f64,
1313        type_ret_f32,
1314        type_ret_f64,
1315        globals,
1316        elem_func_indices,
1317        table_size: table_sizes.first().copied().flatten(),
1318        table_sizes,
1319        elem_segments,
1320        func_result_counts: func_type_indices
1321            .iter()
1322            .map(|&ti| {
1323                type_block_arity
1324                    .get(ti as usize)
1325                    .map(|&(_p, r)| r as u32)
1326                    .unwrap_or(0)
1327            })
1328            .collect(),
1329        func_type_indices,
1330        type_signatures,
1331        wsc_facts: wsc_facts.unwrap_or_default(),
1332    })
1333}
1334
1335/// Parse the function-names subsection of a wasm `name` custom section into
1336/// `out` (function index → developer-facing name, e.g.
1337/// `core::panicking::panic_fmt::h...`). Best-effort by design: the section is
1338/// DEBUG METADATA only, so a malformed entry is skipped rather than failing the
1339/// compile — no codegen path depends on it (#394 Tier-1.x).
1340fn parse_name_section_func_names(
1341    reader: wasmparser::NameSectionReader<'_>,
1342    out: &mut HashMap<u32, String>,
1343) {
1344    for subsection in reader.into_iter().flatten() {
1345        if let wasmparser::Name::Function(map) = subsection {
1346            for naming in map.into_iter().flatten() {
1347                out.insert(naming.index, naming.name.to_string());
1348            }
1349        }
1350    }
1351}
1352
1353/// Fill each function's `debug_name` from the `name`-section map (keyed by the
1354/// FULL function index, imports first — the same index space `FunctionOps.index`
1355/// uses). A function without an entry keeps `None` (⇒ `func_N` downstream).
1356fn apply_name_section(functions: &mut [FunctionOps], names: &HashMap<u32, String>) {
1357    if names.is_empty() {
1358        return;
1359    }
1360    for f in functions {
1361        f.debug_name = names.get(&f.index).cloned();
1362    }
1363}
1364
1365/// Decode a WASM binary and extract all function bodies as WasmOp sequences
1366pub fn decode_wasm_functions(wasm_bytes: &[u8]) -> Result<Vec<FunctionOps>> {
1367    let mut functions = Vec::new();
1368    let mut func_index = 0u32;
1369    let mut num_imported_funcs = 0u32;
1370    let mut export_names: HashMap<u32, String> = HashMap::new();
1371    let mut name_section_names: HashMap<u32, String> = HashMap::new();
1372    // #509: (param_count, result_count) per type index, for FuncType blocktypes.
1373    let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
1374    // GI-FPU-001 (#369): float-typed globals (full index space, imports first)
1375    // whose accesses must loud-skip — see `decode_wasm_module`.
1376    let mut num_imported_globals = 0u32;
1377    let mut float_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
1378    // #680: v128-typed globals + v128 params/results — see `decode_wasm_module`.
1379    let mut v128_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
1380    let mut type_has_v128: Vec<bool> = Vec::new();
1381    let mut func_sig_has_v128: Vec<bool> = Vec::new();
1382
1383    for payload in Parser::new(0).parse_all(wasm_bytes) {
1384        let payload = payload.context("Failed to parse WASM payload")?;
1385
1386        match payload {
1387            Payload::TypeSection(reader) => {
1388                // #509: the blocktype-arity side-table needs the type section
1389                // for `BlockType::FuncType(i)` lookups (the wasm binary format
1390                // places types before code, so the table is complete before any
1391                // `CodeSectionEntry` is decoded).
1392                for rec_group in reader {
1393                    let rec_group = rec_group.context("Failed to parse type")?;
1394                    for sub_ty in rec_group.types() {
1395                        type_block_arity.push(match &sub_ty.composite_type.inner {
1396                            wasmparser::CompositeInnerType::Func(f) => (
1397                                u8::try_from(f.params().len()).unwrap_or(u8::MAX),
1398                                u8::try_from(f.results().len()).unwrap_or(u8::MAX),
1399                            ),
1400                            _ => (u8::MAX, u8::MAX),
1401                        });
1402                        // #680: v128 anywhere in the signature.
1403                        type_has_v128.push(match &sub_ty.composite_type.inner {
1404                            wasmparser::CompositeInnerType::Func(f) => f
1405                                .params()
1406                                .iter()
1407                                .chain(f.results())
1408                                .any(|t| *t == wasmparser::ValType::V128),
1409                            _ => false,
1410                        });
1411                    }
1412                }
1413            }
1414            Payload::ImportSection(imports) => {
1415                // wasmparser 0.221+ compact-imports grouping — flatten groups
1416                // to individual imports (see the ImportSection handler above).
1417                for import in imports.into_imports() {
1418                    let import = import.context("Failed to parse import")?;
1419                    match import.ty {
1420                        wasmparser::TypeRef::Func(_) => num_imported_funcs += 1,
1421                        wasmparser::TypeRef::Global(g) => {
1422                            // GI-FPU-001 (#369): see `decode_wasm_module` —
1423                            // float-typed global accesses must loud-skip.
1424                            if matches!(
1425                                g.content_type,
1426                                wasmparser::ValType::F32 | wasmparser::ValType::F64
1427                            ) {
1428                                float_globals.insert(num_imported_globals);
1429                            }
1430                            // #680: v128-typed imported globals — same lane.
1431                            if g.content_type == wasmparser::ValType::V128 {
1432                                v128_globals.insert(num_imported_globals);
1433                            }
1434                            num_imported_globals += 1;
1435                        }
1436                        _ => {}
1437                    }
1438                }
1439            }
1440            Payload::FunctionSection(reader) => {
1441                // #680: defined-function type indices, in order — the per-
1442                // function v128-signature flag (`decode_wasm_module` gets this
1443                // from its existing FunctionSection handling).
1444                for ty in reader {
1445                    let type_idx = ty.context("Failed to parse function type index")?;
1446                    func_sig_has_v128.push(
1447                        type_has_v128
1448                            .get(type_idx as usize)
1449                            .copied()
1450                            .unwrap_or(false),
1451                    );
1452                }
1453            }
1454            Payload::GlobalSection(reader) => {
1455                // GI-FPU-001 (#369): record f32/f64-typed defined globals so
1456                // `decode_function_body` flags accesses (their initializer is
1457                // dropped on this path too — same silent-zero hazard).
1458                for (idx, global) in reader.into_iter().enumerate() {
1459                    let global = global.context("Failed to parse global")?;
1460                    if matches!(
1461                        global.ty.content_type,
1462                        wasmparser::ValType::F32 | wasmparser::ValType::F64
1463                    ) {
1464                        float_globals.insert(num_imported_globals + idx as u32);
1465                    }
1466                    // #680: v128-typed defined globals — same lane.
1467                    if global.ty.content_type == wasmparser::ValType::V128 {
1468                        v128_globals.insert(num_imported_globals + idx as u32);
1469                    }
1470                }
1471            }
1472            Payload::ExportSection(exports) => {
1473                for export in exports {
1474                    let export = export.context("Failed to parse export")?;
1475                    if export.kind == ExternalKind::Func {
1476                        export_names.insert(export.index, export.name.to_string());
1477                    }
1478                }
1479            }
1480            Payload::CodeSectionEntry(body) => {
1481                let (ops, op_offsets, block_arity, mut unsupported) =
1482                    decode_function_body(&body, &type_block_arity, &float_globals, &v128_globals)?;
1483                // #680: v128 param/result — see `decode_wasm_module`.
1484                if unsupported.is_none()
1485                    && func_sig_has_v128
1486                        .get(func_index as usize)
1487                        .copied()
1488                        .unwrap_or(false)
1489                {
1490                    unsupported = Some(
1491                        "signature has a v128 param/result — no SIMD lowering \
1492                         for this target (#680)"
1493                            .to_string(),
1494                    );
1495                }
1496                let actual_index = num_imported_funcs + func_index;
1497                let export_name = export_names.get(&actual_index).cloned();
1498
1499                functions.push(FunctionOps {
1500                    index: actual_index,
1501                    export_name,
1502                    debug_name: None, // filled from the `name` section after the loop
1503                    ops,
1504                    op_offsets,
1505                    unsupported,
1506                    block_arity,
1507                });
1508                func_index += 1;
1509            }
1510            Payload::CustomSection(c) => {
1511                // #394 Tier-1.x: the wasm `name` custom section.
1512                if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
1513                    parse_name_section_func_names(reader, &mut name_section_names);
1514                }
1515            }
1516            _ => {}
1517        }
1518    }
1519
1520    apply_name_section(&mut functions, &name_section_names);
1521
1522    Ok(functions)
1523}
1524
1525/// Decoded function with its WasmOp sequence
1526#[derive(Debug, Clone)]
1527pub struct FunctionOps {
1528    /// Function index in the module (includes imported functions)
1529    pub index: u32,
1530    /// Export name if this function is exported
1531    pub export_name: Option<String>,
1532    /// #394 Tier-1.x: the function's developer-facing name from the wasm `name`
1533    /// custom section (function-names subsection), e.g.
1534    /// `core::panicking::panic_fmt::h6651313c3e2c6c2f` — present for INTERNAL
1535    /// (non-exported) functions too, unlike `export_name`. DEBUG METADATA only:
1536    /// consumed by the `--debug-line` `DW_TAG_subprogram` emit (name priority:
1537    /// name-section > export name > `func_N`); no codegen or symbol-table path
1538    /// reads it, so emitted `.text`/`.symtab` are unchanged (frozen-safe).
1539    /// `None` when the module has no `name` section or no entry for this index.
1540    pub debug_name: Option<String>,
1541    /// The WASM operations in this function body
1542    pub ops: Vec<WasmOp>,
1543    /// VCR-DBG-001 step 1 (#394): module-relative wasm byte offset of each op in
1544    /// `ops` (same index → same op). This is the address space DWARF-for-wasm
1545    /// `.debug_line` keys on, so it is the bridge from synth's op-index
1546    /// `source_line` to the input wasm's DWARF (wasm-offset → source). PURELY
1547    /// ADDITIVE metadata: no codegen path reads it, so emitted `.text` is
1548    /// unchanged and the frozen fixtures stay bit-identical. Empty until consumed
1549    /// by the DWARF emitter (Tier 1).
1550    pub op_offsets: Vec<u32>,
1551    /// `Some(reason)` when the body contained a value-affecting operator the
1552    /// decoder cannot lower (e.g. scalar f32/f64 — #369, bulk-memory
1553    /// memory.copy/fill). Such an op would otherwise be silently *dropped*
1554    /// (`convert_operator` → `None`), leaving the operand stack wrong and the
1555    /// function a silent miscompile. The compile path LOUD-SKIPS a flagged
1556    /// function (diagnostic + symbol absent → link error names it) instead —
1557    /// the #180/#185 "unsupported op must Err, never silently continue"
1558    /// contract. `None` once every op decoded or was intentionally ignorable
1559    /// (Nop).
1560    pub unsupported: Option<String>,
1561    /// #509: blocktype arity side-table — `(param_count, result_count)` of the
1562    /// k-th `Block`/`Loop`/`If` op in `ops`, in order of appearance.
1563    /// ORDINAL-keyed, not op-index-keyed, on purpose: the backend may rewrite
1564    /// the op stream before selection (e.g. the #539 `i32.const 0; memory.grow`
1565    /// → `memory.size` fold), which shifts op indices but never adds/removes
1566    /// control ops, so the ordinal stays aligned. `BlockType::Empty → (0,0)`,
1567    /// `ValType → (0,1)`, `FuncType(i) →` counts from the type section
1568    /// (saturated to u8; an unresolvable type index records `(u8::MAX,
1569    /// u8::MAX)` so the selector declines loudly instead of miscompiling).
1570    /// This is what lets the direct selector land a value carried by
1571    /// `br`/`br_if`/`br_table` in the target block's designated result
1572    /// register instead of dropping it — `WasmOp::Block/Loop/If` stay bare
1573    /// unit variants (zero ripple through the backends' match sites), and an
1574    /// empty table (hand-built op streams in unit tests) keeps the legacy
1575    /// void-block lowering.
1576    pub block_arity: Vec<(u8, u8)>,
1577}
1578
1579/// #509: `(param_count, result_count)` of a wasm blocktype, for the
1580/// [`FunctionOps::block_arity`] side-table. `type_block_arity` is the type
1581/// section's per-type-index counts (needed for the `FuncType` form); a missing
1582/// entry saturates to `(u8::MAX, u8::MAX)` so downstream declines loudly.
1583fn blocktype_arity(bt: &wasmparser::BlockType, type_block_arity: &[(u8, u8)]) -> (u8, u8) {
1584    match bt {
1585        wasmparser::BlockType::Empty => (0, 0),
1586        wasmparser::BlockType::Type(_) => (0, 1),
1587        wasmparser::BlockType::FuncType(i) => type_block_arity
1588            .get(*i as usize)
1589            .copied()
1590            .unwrap_or((u8::MAX, u8::MAX)),
1591    }
1592}
1593
1594/// The per-function payload [`decode_function_body`] extracts: `(ops,
1595/// op_offsets, block_arity, unsupported)` — see the matching
1596/// [`FunctionOps`] fields for each component's contract.
1597type DecodedBody = (Vec<WasmOp>, Vec<u32>, Vec<(u8, u8)>, Option<String>);
1598
1599/// Decode a single function body to WasmOp sequence.
1600///
1601/// Returns the ops plus `Some(reason)` if any operator was a value-affecting
1602/// op the decoder cannot lower (so the function must be loud-skipped, #369 —
1603/// not silently miscompiled by dropping the op).
1604fn decode_function_body(
1605    body: &wasmparser::FunctionBody,
1606    type_block_arity: &[(u8, u8)],
1607    float_globals: &std::collections::HashSet<u32>,
1608    v128_globals: &std::collections::HashSet<u32>,
1609) -> Result<DecodedBody> {
1610    let mut ops = Vec::new();
1611    // VCR-DBG-001 step 1: parallel to `ops` — the module-relative wasm byte
1612    // offset of each emitted op (the DWARF-for-wasm address space). Captured via
1613    // the offset-aware reader; pushed only when an op is pushed, so indices stay
1614    // aligned with `ops`. Additive metadata, no codegen consumer ⇒ frozen-safe.
1615    let mut op_offsets = Vec::new();
1616    // #509: ordinal blocktype-arity side-table — one entry per Block/Loop/If in
1617    // `ops` order (see `FunctionOps::block_arity`).
1618    let mut block_arity: Vec<(u8, u8)> = Vec::new();
1619    let mut unsupported: Option<String> = None;
1620
1621    // #680: a v128-typed LOCAL is expressible with zero SIMD-proposal
1622    // operators (`local.get`/`local.set`/`local.tee` are type-agnostic), but
1623    // every selector lowers those as 4-byte (or 8-byte i64) register moves —
1624    // silently truncating the 16-byte value. Flag the declaration up front.
1625    for local in body.get_locals_reader()? {
1626        let (count, ty) = local.context("Failed to read local declaration")?;
1627        if unsupported.is_none() && count > 0 && ty == wasmparser::ValType::V128 {
1628            unsupported = Some(
1629                "declares a v128-typed local — no SIMD lowering for this \
1630                 target, accesses would silently truncate the 16-byte value \
1631                 (#680)"
1632                    .to_string(),
1633            );
1634        }
1635    }
1636
1637    let ops_reader = body.get_operators_reader()?;
1638    for item in ops_reader.into_iter_with_offsets() {
1639        let (op, offset) = item.context("Failed to read operator")?;
1640
1641        // #680: SIMD (v128) category-level honesty guard. Some SIMD ops decode
1642        // into `WasmOp` v128 variants that only a dead, never-wired Helium/MVE
1643        // prototype can select (`has_helium` is set by tests alone), so on
1644        // every real target they were silently dropped at selection —
1645        // `i32x4.add` compiled to an operand passthrough and `v128.store`
1646        // left memory unwritten. Catch the ENTIRE SIMD + relaxed-SIMD
1647        // proposal space here (macro-generated from wasmparser's operator
1648        // table — no hand-kept list to fall out of date) and route the
1649        // function through the same loud-skip/honest-bail lane as scalar
1650        // floats (GI-FPU-001). Targets with real SIMD hardware (Helium/MVE on
1651        // cortex-m55) can lift this once a lowering is actually wired.
1652        if unsupported.is_none() && is_simd_operator(&op) {
1653            unsupported = Some(format!(
1654                "{op:?}: no SIMD lowering for this target — the op would be \
1655                 silently dropped to a no-op (WASM SIMD proposal, #680)"
1656            ));
1657        }
1658
1659        if let Some(wasm_op) = convert_operator(&op) {
1660            // #509: capture the blocktype arity BEFORE the enum flattens it away
1661            // (`WasmOp::Block/Loop/If` are unit variants by design).
1662            if let wasmparser::Operator::Block { blockty }
1663            | wasmparser::Operator::Loop { blockty }
1664            | wasmparser::Operator::If { blockty } = &op
1665            {
1666                block_arity.push(blocktype_arity(blockty, type_block_arity));
1667            }
1668            // GI-FPU-001 (#369): `global.get`/`global.set` decode fine (the
1669            // ops are type-agnostic), but an f32/f64-typed global has no
1670            // float lowering — its const initializer is dropped (slot zeroed),
1671            // so a read returns a silently-wrong 0.0. Flag the function for
1672            // the same loud-skip the scalar float ops get.
1673            if unsupported.is_none()
1674                && let WasmOp::GlobalGet(i) | WasmOp::GlobalSet(i) = &wasm_op
1675                && float_globals.contains(i)
1676            {
1677                unsupported = Some(format!(
1678                    "{wasm_op:?} on an f32/f64-typed global — float globals \
1679                     have no lowering, the initializer would be silently \
1680                     zeroed (GI-FPU-001)"
1681                ));
1682            }
1683            // #680: same hazard for v128-typed globals — `global.get`/
1684            // `global.set` decode fine, but there is no SIMD lowering: the
1685            // access would move 4 of the 16 bytes and the `v128.const`
1686            // initializer is never captured (slot zeroed).
1687            if unsupported.is_none()
1688                && let WasmOp::GlobalGet(i) | WasmOp::GlobalSet(i) = &wasm_op
1689                && v128_globals.contains(i)
1690            {
1691                unsupported = Some(format!(
1692                    "{wasm_op:?} on a v128-typed global — no SIMD lowering \
1693                     for this target (#680)"
1694                ));
1695            }
1696            // VCR-MEM-002 phase 1 (#406): a load/store whose memarg targets a
1697            // NON-DEFAULT memory must carry its index — dropping it silently
1698            // aliased every memory onto the one R11 base (a store to memory
1699            // `$b` clobbered memory `$a`). memidx 0 stays the bare variant, so
1700            // single-memory streams are bit-identical by construction.
1701            let wasm_op = match memarg_memory_index(&op) {
1702                Some(mem) if mem > 0 => WasmOp::MultiMemory {
1703                    memory: mem,
1704                    op: Box::new(wasm_op),
1705                },
1706                _ => wasm_op,
1707            };
1708            ops.push(wasm_op);
1709            op_offsets.push(offset as u32);
1710        } else if unsupported.is_none() && !is_intentionally_ignored(&op) {
1711            // #406 phase 1: bulk-memory ops on a non-default memory (including
1712            // the cross-memory `memory.copy` dst_mem != src_mem form) have no
1713            // lowering yet — name the decline precisely instead of the generic
1714            // dropped-op message.
1715            unsupported = match &op {
1716                wasmparser::Operator::MemoryCopy { dst_mem, src_mem }
1717                    if *dst_mem != 0 || *src_mem != 0 =>
1718                {
1719                    Some(format!(
1720                        "memory.copy dst_mem={dst_mem} src_mem={src_mem}: \
1721                         cross-/non-default-memory memory.copy is not lowered \
1722                         in multi-memory phase 1 (#406) — only memory-0 \
1723                         memory.copy is supported"
1724                    ))
1725                }
1726                wasmparser::Operator::MemoryFill { mem } if *mem != 0 => Some(format!(
1727                    "memory.fill mem={mem}: non-default-memory memory.fill is \
1728                     not lowered in multi-memory phase 1 (#406) — only \
1729                     memory-0 memory.fill is supported"
1730                )),
1731                // The op was DROPPED by `convert_operator` (`_ => None`) and is
1732                // not an intentional no-op (Nop) — record it so the function is
1733                // loud-skipped rather than silently miscompiled (#369).
1734                _ => Some(format!("{op:?}")),
1735            };
1736        }
1737    }
1738
1739    Ok((ops, op_offsets, block_arity, unsupported))
1740}
1741
1742/// Operators that `convert_operator` returns `None` for *on purpose* — they
1743/// carry no value-affecting semantics for our backend, so dropping them is
1744/// correct (NOT a silent miscompile). Everything else that decodes to `None`
1745/// is an unsupported op that must loud-skip its function (#369).
1746///
1747/// #665: `Unreachable` is NOT on this list — it traps (WASM §4.4.5), so it
1748/// decodes to `WasmOp::Unreachable` and every backend lowers it to a trap
1749/// instruction (or loud-declines). Only `Nop` is genuinely ignorable.
1750fn is_intentionally_ignored(op: &wasmparser::Operator) -> bool {
1751    use wasmparser::Operator::*;
1752    matches!(op, Nop)
1753}
1754
1755/// #680: is `op` from the WASM SIMD or relaxed-SIMD proposal?
1756///
1757/// CATEGORY-LEVEL by construction: the match is macro-generated from
1758/// `wasmparser::for_each_operator!`'s own proposal markers (`@simd` /
1759/// `@relaxed_simd`), so it covers the entire SIMD operator space of the
1760/// pinned wasmparser — there is no hand-kept op list that a new lane op,
1761/// load/store variant, or relaxed-SIMD instruction can silently fall out of.
1762/// Used to loud-skip functions with SIMD ops: no target has a SIMD lowering
1763/// wired today (the Helium/MVE selector arms are gated on a `has_helium`
1764/// flag only tests set), so a decoded v128 `WasmOp` was silently dropped at
1765/// selection — the #554-class miscompile this predicate closes.
1766fn is_simd_operator(op: &wasmparser::Operator) -> bool {
1767    macro_rules! define_match_operator {
1768        ($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => {
1769            match op {
1770                $(
1771                    wasmparser::Operator::$op { .. } => {
1772                        define_match_operator!(impl_one @$proposal)
1773                    }
1774                )*
1775                // `Operator` is non-exhaustive; an operator outside the
1776                // pinned wasmparser's own table cannot be produced by it.
1777                _ => false,
1778            }
1779        };
1780        (impl_one @simd) => { true };
1781        (impl_one @relaxed_simd) => { true };
1782        (impl_one @$proposal:ident) => { false };
1783    }
1784    wasmparser::for_each_operator!(define_match_operator)
1785}
1786
1787/// VCR-MEM-002 phase 1 (#406): the `memarg.memory` index of a load/store
1788/// operator that [`convert_operator`] lowers, `None` for every other op.
1789///
1790/// MIRROR PIN: this list must cover exactly the memarg-carrying arms of
1791/// `convert_operator` (every `{ memarg }` load/store it returns `Some` for).
1792/// A memarg op missing HERE but lowered THERE would silently drop a non-zero
1793/// memory index again — the pre-#406 aliasing bug. Ops `convert_operator`
1794/// drops (`_ => None`) loud-skip their function regardless, so they need no
1795/// entry. `memory.size`/`grow`/`copy`/`fill` carry their indices in their own
1796/// `WasmOp` variants / decode-time declines, not via this helper.
1797fn memarg_memory_index(op: &wasmparser::Operator) -> Option<u32> {
1798    use wasmparser::Operator::*;
1799    match op {
1800        I32Load { memarg }
1801        | I32Store { memarg }
1802        | I64Load { memarg }
1803        | I64Store { memarg }
1804        | I32Load8S { memarg }
1805        | I32Load8U { memarg }
1806        | I32Load16S { memarg }
1807        | I32Load16U { memarg }
1808        | I32Store8 { memarg }
1809        | I32Store16 { memarg }
1810        | I64Load8S { memarg }
1811        | I64Load8U { memarg }
1812        | I64Load16S { memarg }
1813        | I64Load16U { memarg }
1814        | I64Load32S { memarg }
1815        | I64Load32U { memarg }
1816        | I64Store8 { memarg }
1817        | I64Store16 { memarg }
1818        | I64Store32 { memarg }
1819        | F32Load { memarg }
1820        | F32Store { memarg }
1821        | F64Load { memarg }
1822        | F64Store { memarg }
1823        | V128Load { memarg }
1824        | V128Store { memarg } => Some(memarg.memory),
1825        _ => None,
1826    }
1827}
1828
1829/// Convert a wasmparser Operator to our WasmOp enum
1830fn convert_operator(op: &wasmparser::Operator) -> Option<WasmOp> {
1831    use wasmparser::Operator::*;
1832
1833    match op {
1834        // Constants
1835        I32Const { value } => Some(WasmOp::I32Const(*value)),
1836
1837        // i32 Arithmetic
1838        I32Add => Some(WasmOp::I32Add),
1839        I32Sub => Some(WasmOp::I32Sub),
1840        I32Mul => Some(WasmOp::I32Mul),
1841        I32DivS => Some(WasmOp::I32DivS),
1842        I32DivU => Some(WasmOp::I32DivU),
1843        I32RemS => Some(WasmOp::I32RemS),
1844        I32RemU => Some(WasmOp::I32RemU),
1845
1846        // i64 Constants
1847        I64Const { value } => Some(WasmOp::I64Const(*value)),
1848
1849        // i64 Arithmetic
1850        I64Add => Some(WasmOp::I64Add),
1851        I64Sub => Some(WasmOp::I64Sub),
1852        I64Mul => Some(WasmOp::I64Mul),
1853        I64DivS => Some(WasmOp::I64DivS),
1854        I64DivU => Some(WasmOp::I64DivU),
1855        I64RemS => Some(WasmOp::I64RemS),
1856        I64RemU => Some(WasmOp::I64RemU),
1857
1858        // i64 Bitwise
1859        I64And => Some(WasmOp::I64And),
1860        I64Or => Some(WasmOp::I64Or),
1861        I64Xor => Some(WasmOp::I64Xor),
1862        I64Shl => Some(WasmOp::I64Shl),
1863        I64ShrS => Some(WasmOp::I64ShrS),
1864        I64ShrU => Some(WasmOp::I64ShrU),
1865        I64Rotl => Some(WasmOp::I64Rotl),
1866        I64Rotr => Some(WasmOp::I64Rotr),
1867        I64Clz => Some(WasmOp::I64Clz),
1868        I64Ctz => Some(WasmOp::I64Ctz),
1869        I64Popcnt => Some(WasmOp::I64Popcnt),
1870        I64Extend8S => Some(WasmOp::I64Extend8S),
1871        I64Extend16S => Some(WasmOp::I64Extend16S),
1872        I64Extend32S => Some(WasmOp::I64Extend32S),
1873        // i32<->i64 width conversions. Previously UNMAPPED → silently dropped,
1874        // which left an i32 value as a 64-bit operand with a garbage high half
1875        // (harmless when a following `i64.shl 32` discards it, but a latent
1876        // miscompile for extend-then-arithmetic, and it breaks width-correct
1877        // register allocation). (#204)
1878        I64ExtendI32U => Some(WasmOp::I64ExtendI32U),
1879        I64ExtendI32S => Some(WasmOp::I64ExtendI32S),
1880        I32WrapI64 => Some(WasmOp::I32WrapI64),
1881
1882        // i64 Comparison
1883        I64Eqz => Some(WasmOp::I64Eqz),
1884        I64Eq => Some(WasmOp::I64Eq),
1885        I64Ne => Some(WasmOp::I64Ne),
1886        I64LtS => Some(WasmOp::I64LtS),
1887        I64LtU => Some(WasmOp::I64LtU),
1888        I64LeS => Some(WasmOp::I64LeS),
1889        I64LeU => Some(WasmOp::I64LeU),
1890        I64GtS => Some(WasmOp::I64GtS),
1891        I64GtU => Some(WasmOp::I64GtU),
1892        I64GeS => Some(WasmOp::I64GeS),
1893        I64GeU => Some(WasmOp::I64GeU),
1894
1895        // Bitwise
1896        I32And => Some(WasmOp::I32And),
1897        I32Or => Some(WasmOp::I32Or),
1898        I32Xor => Some(WasmOp::I32Xor),
1899        I32Shl => Some(WasmOp::I32Shl),
1900        I32ShrS => Some(WasmOp::I32ShrS),
1901        I32ShrU => Some(WasmOp::I32ShrU),
1902        I32Rotl => Some(WasmOp::I32Rotl),
1903        I32Rotr => Some(WasmOp::I32Rotr),
1904        I32Clz => Some(WasmOp::I32Clz),
1905        I32Ctz => Some(WasmOp::I32Ctz),
1906        I32Popcnt => Some(WasmOp::I32Popcnt),
1907        I32Extend8S => Some(WasmOp::I32Extend8S),
1908        I32Extend16S => Some(WasmOp::I32Extend16S),
1909
1910        // Comparison
1911        I32Eqz => Some(WasmOp::I32Eqz),
1912        I32Eq => Some(WasmOp::I32Eq),
1913        I32Ne => Some(WasmOp::I32Ne),
1914        I32LtS => Some(WasmOp::I32LtS),
1915        I32LtU => Some(WasmOp::I32LtU),
1916        I32LeS => Some(WasmOp::I32LeS),
1917        I32LeU => Some(WasmOp::I32LeU),
1918        I32GtS => Some(WasmOp::I32GtS),
1919        I32GtU => Some(WasmOp::I32GtU),
1920        I32GeS => Some(WasmOp::I32GeS),
1921        I32GeU => Some(WasmOp::I32GeU),
1922
1923        // Memory
1924        I32Load { memarg } => Some(WasmOp::I32Load {
1925            offset: memarg.offset as u32,
1926            align: memarg.align as u32,
1927        }),
1928        I32Store { memarg } => Some(WasmOp::I32Store {
1929            offset: memarg.offset as u32,
1930            align: memarg.align as u32,
1931        }),
1932        // #372: full-width i64 load/store. The selector already lowers these to
1933        // a lo/hi i32 register-pair access (`generate_i64_load/store_with_bounds_check`,
1934        // reusing the #171 pair regalloc) — only the decoder arm was missing, so
1935        // `i64.load`/`i64.store` fell through `_ => None` and (since v0.11.46)
1936        // loud-skipped their function. The narrow forms (I64Load8.. / I64Store32)
1937        // were already decoded below.
1938        I64Load { memarg } => Some(WasmOp::I64Load {
1939            offset: memarg.offset as u32,
1940            align: memarg.align as u32,
1941        }),
1942        I64Store { memarg } => Some(WasmOp::I64Store {
1943            offset: memarg.offset as u32,
1944            align: memarg.align as u32,
1945        }),
1946
1947        // Sub-word loads (i32)
1948        I32Load8S { memarg } => Some(WasmOp::I32Load8S {
1949            offset: memarg.offset as u32,
1950            align: memarg.align as u32,
1951        }),
1952        I32Load8U { memarg } => Some(WasmOp::I32Load8U {
1953            offset: memarg.offset as u32,
1954            align: memarg.align as u32,
1955        }),
1956        I32Load16S { memarg } => Some(WasmOp::I32Load16S {
1957            offset: memarg.offset as u32,
1958            align: memarg.align as u32,
1959        }),
1960        I32Load16U { memarg } => Some(WasmOp::I32Load16U {
1961            offset: memarg.offset as u32,
1962            align: memarg.align as u32,
1963        }),
1964
1965        // Sub-word stores (i32)
1966        I32Store8 { memarg } => Some(WasmOp::I32Store8 {
1967            offset: memarg.offset as u32,
1968            align: memarg.align as u32,
1969        }),
1970        I32Store16 { memarg } => Some(WasmOp::I32Store16 {
1971            offset: memarg.offset as u32,
1972            align: memarg.align as u32,
1973        }),
1974
1975        // Local/Global
1976        LocalGet { local_index } => Some(WasmOp::LocalGet(*local_index)),
1977        LocalSet { local_index } => Some(WasmOp::LocalSet(*local_index)),
1978        LocalTee { local_index } => Some(WasmOp::LocalTee(*local_index)),
1979        GlobalGet { global_index } => Some(WasmOp::GlobalGet(*global_index)),
1980        GlobalSet { global_index } => Some(WasmOp::GlobalSet(*global_index)),
1981
1982        // Control flow
1983        Block { .. } => Some(WasmOp::Block),
1984        Loop { .. } => Some(WasmOp::Loop),
1985        Br { relative_depth } => Some(WasmOp::Br(*relative_depth)),
1986        BrIf { relative_depth } => Some(WasmOp::BrIf(*relative_depth)),
1987        // br_table: indexed multi-way branch. Previously UNMAPPED → silently
1988        // dropped, so the selector never emitted the index dispatch and control
1989        // fell straight into the first table arm — every br_table behaved as if
1990        // it always took target 0 (gale's binary-sem WAKE path never fired). The
1991        // jump-table relative depths + default depth are preserved in order.
1992        BrTable { targets } => {
1993            let default = targets.default();
1994            let tgts: Vec<u32> = targets.targets().filter_map(Result::ok).collect();
1995            Some(WasmOp::BrTable {
1996                targets: tgts,
1997                default,
1998            })
1999        }
2000        Return => Some(WasmOp::Return),
2001        Call { function_index } => Some(WasmOp::Call(*function_index)),
2002        CallIndirect {
2003            type_index,
2004            table_index,
2005            ..
2006        } => Some(WasmOp::CallIndirect {
2007            type_index: *type_index,
2008            table_index: *table_index,
2009        }),
2010
2011        // End is needed for control flow pattern matching
2012        End => Some(WasmOp::End),
2013
2014        // #665: `unreachable` MUST reach the backends — WASM Core §4.4.5
2015        // requires it to trap unconditionally. It was previously dropped here
2016        // (treated like Nop), so every backend compiled it to a no-op and
2017        // control FELL THROUGH panic!/abort/unreachable-default guards with
2018        // undefined register state. The selector arms (ARM: UDF #0, RV32:
2019        // ebreak) already existed; they just never received the op.
2020        Unreachable => Some(WasmOp::Unreachable),
2021
2022        // Nop - skip (genuinely no semantics)
2023        Nop => None,
2024
2025        // Drop is needed for br_if pattern matching
2026        Drop => Some(WasmOp::Drop),
2027
2028        // Select
2029        Select => Some(WasmOp::Select),
2030
2031        // If/Else - simplified handling
2032        If { .. } => Some(WasmOp::If),
2033        Else => Some(WasmOp::Else),
2034
2035        // i64 sub-word loads
2036        I64Load8S { memarg } => Some(WasmOp::I64Load8S {
2037            offset: memarg.offset as u32,
2038            align: memarg.align as u32,
2039        }),
2040        I64Load8U { memarg } => Some(WasmOp::I64Load8U {
2041            offset: memarg.offset as u32,
2042            align: memarg.align as u32,
2043        }),
2044        I64Load16S { memarg } => Some(WasmOp::I64Load16S {
2045            offset: memarg.offset as u32,
2046            align: memarg.align as u32,
2047        }),
2048        I64Load16U { memarg } => Some(WasmOp::I64Load16U {
2049            offset: memarg.offset as u32,
2050            align: memarg.align as u32,
2051        }),
2052        I64Load32S { memarg } => Some(WasmOp::I64Load32S {
2053            offset: memarg.offset as u32,
2054            align: memarg.align as u32,
2055        }),
2056        I64Load32U { memarg } => Some(WasmOp::I64Load32U {
2057            offset: memarg.offset as u32,
2058            align: memarg.align as u32,
2059        }),
2060
2061        // i64 sub-word stores
2062        I64Store8 { memarg } => Some(WasmOp::I64Store8 {
2063            offset: memarg.offset as u32,
2064            align: memarg.align as u32,
2065        }),
2066        I64Store16 { memarg } => Some(WasmOp::I64Store16 {
2067            offset: memarg.offset as u32,
2068            align: memarg.align as u32,
2069        }),
2070        I64Store32 { memarg } => Some(WasmOp::I64Store32 {
2071            offset: memarg.offset as u32,
2072            align: memarg.align as u32,
2073        }),
2074
2075        // Memory management
2076        MemorySize { mem, .. } => Some(WasmOp::MemorySize(*mem)),
2077        MemoryGrow { mem, .. } => Some(WasmOp::MemoryGrow(*mem)),
2078
2079        // Bulk memory (#374). The backend supports a single linear memory
2080        // (memory 0); any non-zero memory index falls through to `_ => None` and
2081        // loud-skips the function (GI-FPU-001 honesty contract) rather than
2082        // miscompiling a multi-memory copy. memory.copy reads dst/src memories;
2083        // memory.fill one. The selector lowers these to a bounds-checked byte
2084        // loop (see select_with_stack).
2085        MemoryCopy {
2086            dst_mem: 0,
2087            src_mem: 0,
2088        } => Some(WasmOp::MemoryCopy),
2089        MemoryFill { mem: 0 } => Some(WasmOp::MemoryFill),
2090
2091        // ========================================================================
2092        // v128 SIMD operations (WASM SIMD proposal, 0xFD prefix)
2093        // ========================================================================
2094        V128Const { value } => {
2095            let mut bytes = [0u8; 16];
2096            bytes.copy_from_slice(value.bytes());
2097            Some(WasmOp::V128Const(bytes))
2098        }
2099        V128Load { memarg } => Some(WasmOp::V128Load {
2100            offset: memarg.offset as u32,
2101            align: memarg.align as u32,
2102        }),
2103        V128Store { memarg } => Some(WasmOp::V128Store {
2104            offset: memarg.offset as u32,
2105            align: memarg.align as u32,
2106        }),
2107
2108        // v128 bitwise
2109        V128And => Some(WasmOp::V128And),
2110        V128Or => Some(WasmOp::V128Or),
2111        V128Xor => Some(WasmOp::V128Xor),
2112        V128Not => Some(WasmOp::V128Not),
2113        V128AndNot => Some(WasmOp::V128AndNot),
2114
2115        // i8x16
2116        I8x16Add => Some(WasmOp::I8x16Add),
2117        I8x16Sub => Some(WasmOp::I8x16Sub),
2118        I8x16Neg => Some(WasmOp::I8x16Neg),
2119        I8x16Eq => Some(WasmOp::I8x16Eq),
2120        I8x16Ne => Some(WasmOp::I8x16Ne),
2121        I8x16LtS => Some(WasmOp::I8x16LtS),
2122        I8x16LtU => Some(WasmOp::I8x16LtU),
2123        I8x16GtS => Some(WasmOp::I8x16GtS),
2124        I8x16GtU => Some(WasmOp::I8x16GtU),
2125        I8x16LeS => Some(WasmOp::I8x16LeS),
2126        I8x16LeU => Some(WasmOp::I8x16LeU),
2127        I8x16GeS => Some(WasmOp::I8x16GeS),
2128        I8x16GeU => Some(WasmOp::I8x16GeU),
2129        I8x16Splat => Some(WasmOp::I8x16Splat),
2130        I8x16ExtractLaneS { lane } => Some(WasmOp::I8x16ExtractLaneS(*lane)),
2131        I8x16ExtractLaneU { lane } => Some(WasmOp::I8x16ExtractLaneU(*lane)),
2132        I8x16ReplaceLane { lane } => Some(WasmOp::I8x16ReplaceLane(*lane)),
2133        I8x16Shuffle { lanes } => Some(WasmOp::I8x16Shuffle(*lanes)),
2134        I8x16Swizzle => Some(WasmOp::I8x16Swizzle),
2135
2136        // i16x8
2137        I16x8Add => Some(WasmOp::I16x8Add),
2138        I16x8Sub => Some(WasmOp::I16x8Sub),
2139        I16x8Mul => Some(WasmOp::I16x8Mul),
2140        I16x8Neg => Some(WasmOp::I16x8Neg),
2141        I16x8Eq => Some(WasmOp::I16x8Eq),
2142        I16x8Ne => Some(WasmOp::I16x8Ne),
2143        I16x8LtS => Some(WasmOp::I16x8LtS),
2144        I16x8LtU => Some(WasmOp::I16x8LtU),
2145        I16x8GtS => Some(WasmOp::I16x8GtS),
2146        I16x8GtU => Some(WasmOp::I16x8GtU),
2147        I16x8LeS => Some(WasmOp::I16x8LeS),
2148        I16x8LeU => Some(WasmOp::I16x8LeU),
2149        I16x8GeS => Some(WasmOp::I16x8GeS),
2150        I16x8GeU => Some(WasmOp::I16x8GeU),
2151        I16x8Splat => Some(WasmOp::I16x8Splat),
2152        I16x8ExtractLaneS { lane } => Some(WasmOp::I16x8ExtractLaneS(*lane)),
2153        I16x8ExtractLaneU { lane } => Some(WasmOp::I16x8ExtractLaneU(*lane)),
2154        I16x8ReplaceLane { lane } => Some(WasmOp::I16x8ReplaceLane(*lane)),
2155
2156        // i32x4
2157        I32x4Add => Some(WasmOp::I32x4Add),
2158        I32x4Sub => Some(WasmOp::I32x4Sub),
2159        I32x4Mul => Some(WasmOp::I32x4Mul),
2160        I32x4Neg => Some(WasmOp::I32x4Neg),
2161        I32x4Eq => Some(WasmOp::I32x4Eq),
2162        I32x4Ne => Some(WasmOp::I32x4Ne),
2163        I32x4LtS => Some(WasmOp::I32x4LtS),
2164        I32x4LtU => Some(WasmOp::I32x4LtU),
2165        I32x4GtS => Some(WasmOp::I32x4GtS),
2166        I32x4GtU => Some(WasmOp::I32x4GtU),
2167        I32x4LeS => Some(WasmOp::I32x4LeS),
2168        I32x4LeU => Some(WasmOp::I32x4LeU),
2169        I32x4GeS => Some(WasmOp::I32x4GeS),
2170        I32x4GeU => Some(WasmOp::I32x4GeU),
2171        I32x4Splat => Some(WasmOp::I32x4Splat),
2172        I32x4ExtractLane { lane } => Some(WasmOp::I32x4ExtractLane(*lane)),
2173        I32x4ReplaceLane { lane } => Some(WasmOp::I32x4ReplaceLane(*lane)),
2174
2175        // i64x2
2176        I64x2Add => Some(WasmOp::I64x2Add),
2177        I64x2Sub => Some(WasmOp::I64x2Sub),
2178        I64x2Mul => Some(WasmOp::I64x2Mul),
2179        I64x2Neg => Some(WasmOp::I64x2Neg),
2180        I64x2Eq => Some(WasmOp::I64x2Eq),
2181        I64x2Ne => Some(WasmOp::I64x2Ne),
2182        I64x2LtS => Some(WasmOp::I64x2LtS),
2183        I64x2GtS => Some(WasmOp::I64x2GtS),
2184        I64x2LeS => Some(WasmOp::I64x2LeS),
2185        I64x2GeS => Some(WasmOp::I64x2GeS),
2186        I64x2Splat => Some(WasmOp::I64x2Splat),
2187        I64x2ExtractLane { lane } => Some(WasmOp::I64x2ExtractLane(*lane)),
2188        I64x2ReplaceLane { lane } => Some(WasmOp::I64x2ReplaceLane(*lane)),
2189
2190        // === Scalar f32 (GI-FPU-002 phase 1, #619/#369) ===
2191        // Un-dropped so the FPU targets (cortex-m4f/m7/m7dp) can route these to
2192        // the VFP selector arms in `select_with_stack`. The FPU gate lives at
2193        // the selector/validate layer (`requires_fpu()` + `set_target`): on a
2194        // non-FPU target (m0/m3/r5) these still honest-reject. f64 stays dropped
2195        // (phase 2 — M7DP D-registers). Only the wired scope is un-dropped; the
2196        // rest of the scalar f32 surface (abs/neg/sqrt/min/max/…) still falls to
2197        // `_ => None` and loud-skips its function until phase 1b wires it.
2198        F32Add => Some(WasmOp::F32Add),
2199        F32Sub => Some(WasmOp::F32Sub),
2200        F32Mul => Some(WasmOp::F32Mul),
2201        F32Div => Some(WasmOp::F32Div),
2202        F32Eq => Some(WasmOp::F32Eq),
2203        F32Ne => Some(WasmOp::F32Ne),
2204        F32Lt => Some(WasmOp::F32Lt),
2205        F32Le => Some(WasmOp::F32Le),
2206        F32Gt => Some(WasmOp::F32Gt),
2207        F32Ge => Some(WasmOp::F32Ge),
2208        F32Const { value } => Some(WasmOp::F32Const(f32::from_bits(value.bits()))),
2209        // #708 (phase 1b): `f32.load` un-dropped. The selector lowers it as the
2210        // proven `i32.load` address sequence (`[R11,idx]`→absolute-base rewrite +
2211        // bounds guard) into a core register, then a bit-exact `VMOV Sd,Rd`
2212        // (reinterpret) — a VLDR loads the same 4 bytes, so the bit pattern is
2213        // identical.
2214        F32Load { memarg } => Some(WasmOp::F32Load {
2215            offset: memarg.offset as u32,
2216            align: memarg.align as u32,
2217        }),
2218        // #719 (phase 1b): `f32.store` — the VFP-store twin of `f32.load`. The
2219        // selector moves the S-register value into a core register (`VMOV Rn,Sn`,
2220        // a reinterpret) and reuses the PROVEN `i32.store` address path; a VSTR
2221        // would write the same 4 bytes, so the stored word is bit-exact. (falcon
2222        // has 10 f32.store functions, #719.)
2223        F32Store { memarg } => Some(WasmOp::F32Store {
2224            offset: memarg.offset as u32,
2225            align: memarg.align as u32,
2226        }),
2227        // #719 (phase 1b): scalar f32 sign-family math — `VABS.F32` / `VNEG.F32`
2228        // and the `copysign` sign-bit splice. Pure single-precision VFP, no
2229        // numeric approximation; bit-exact across ±0.0 / NaN-sign / ±inf.
2230        F32Abs => Some(WasmOp::F32Abs),
2231        F32Neg => Some(WasmOp::F32Neg),
2232        F32Copysign => Some(WasmOp::F32Copysign),
2233        // #538 m4: f32.sqrt / f32.min / f32.max un-dropped. sqrt is a single
2234        // IEEE-754 VSQRT/FSQRT everywhere (sqrt of a negative ⇒ quiet NaN,
2235        // never traps — exactly WASM). min/max lower on aarch64 (A64 FMIN/FMAX
2236        // = IEEE 754-2019 minimum/maximum ≡ WASM NaN-propagation + -0<+0);
2237        // ARM32 LOUD-declines them (its legacy compare-select pseudo-op is
2238        // NaN/±0-wrong — see the selector's F32Min/F32Max reject arm) and
2239        // RV32 loud-declines all floats. The f32 rounding ops stay at
2240        // `_ => None` until a later increment.
2241        F32Sqrt => Some(WasmOp::F32Sqrt),
2242        F32Min => Some(WasmOp::F32Min),
2243        F32Max => Some(WasmOp::F32Max),
2244        // #708 (phase 1b): the f32<->i32 bit-casts. Pure `VMOV` between a core
2245        // register and a single-precision S-register — no numeric conversion.
2246        F32ReinterpretI32 => Some(WasmOp::F32ReinterpretI32),
2247        I32ReinterpretF32 => Some(WasmOp::I32ReinterpretF32),
2248        // #851 (GI-FPU-001): the f64<->i64 bit-casts, the 64-bit twins of the
2249        // pair above. The WasmOp variants already existed and every backend
2250        // that lowers f64 has a lowering (ARM: `ArmOp::{F64ReinterpretI64,
2251        // I64ReinterpretF64}` VFP moves; aarch64: `fmov d,x` / `fmov x,d`);
2252        // they were merely never DECODED, so they globally declined. Backends
2253        // without an f64 lowering (RV32) still loud-decline downstream — this
2254        // only un-drops the op at decode.
2255        F64ReinterpretI64 => Some(WasmOp::F64ReinterpretI64),
2256        I64ReinterpretF64 => Some(WasmOp::I64ReinterpretF64),
2257        F32ConvertI32S => Some(WasmOp::F32ConvertI32S),
2258        F32ConvertI32U => Some(WasmOp::F32ConvertI32U),
2259        I32TruncF32S => Some(WasmOp::I32TruncF32S),
2260        I32TruncF32U => Some(WasmOp::I32TruncF32U),
2261        // #869: the 64-bit integer<->float conversion family — previously all
2262        // dropped here (`_ => None`), which loud-skipped every function using
2263        // them (3 of 5 falcon cascade stages at their public entry points).
2264        // ARMv7E-M VFP has no 64-bit-integer<->float instruction (VCVT on
2265        // FPv4-SP/FPv5-D16 encodes only S32/U32 <-> F32/F64), so the ARM32
2266        // lowering is a self-contained multi-step expansion on cortex-m7dp
2267        // (see `try_lower_f32`/`try_lower_f64`); the TRAPPING trunc forms
2268        // carry the #709-class i64 domain guard (WASM §4.3.3 requires a trap
2269        // on NaN/out-of-range — the saturating decompose alone would be a
2270        // silent miscompile). Backends without the machinery (RV32, aarch64's
2271        // current subset, single-precision ARM) still loud-decline downstream.
2272        F32ConvertI64S => Some(WasmOp::F32ConvertI64S),
2273        F32ConvertI64U => Some(WasmOp::F32ConvertI64U),
2274        I64TruncF32S => Some(WasmOp::I64TruncF32S),
2275        I64TruncF32U => Some(WasmOp::I64TruncF32U),
2276        // #782a: the nontrapping trunc_sat family (0xFC-prefixed,
2277        // saturating-float-to-int proposal) — TOTAL ops (§4.3.2: NaN → 0,
2278        // out-of-range saturates to INT_MIN/INT_MAX, no traps). Un-dropped so
2279        // the selectors see them: ARM32 lowers the i32-target forms as a bare
2280        // VCVT (round-toward-zero VCVT already saturates and gives 0 for NaN —
2281        // exactly trunc_sat, the very behavior the #709 guard exists to keep
2282        // away from the TRAPPING forms); aarch64 lowers all eight via
2283        // FCVTZS/FCVTZU. The i64-target forms LOUD-decline on 32-bit ARM (no
2284        // i64 register-pair conversion path) and RV32 (no floats at all).
2285        I32TruncSatF32S => Some(WasmOp::I32TruncSatF32S),
2286        I32TruncSatF32U => Some(WasmOp::I32TruncSatF32U),
2287        I64TruncSatF32S => Some(WasmOp::I64TruncSatF32S),
2288        I64TruncSatF32U => Some(WasmOp::I64TruncSatF32U),
2289
2290        // === Scalar f64 (GI-FPU-002 phase 2, #369) ===
2291        // Un-dropped for the DOUBLE-precision FPU target (cortex-m7dp D0..D15);
2292        // the capability gate lives in `select_with_stack`'s preamble — any
2293        // f64 op on m4f/m7 (single-precision) or m0/m3/r5 (no FPU) still
2294        // honest-rejects its function. Exactly this set is lowered by
2295        // `try_lower_f64` + the `F64Load`/`F64Store` selector arms; the rest
2296        // of the f64 surface (min/max/copysign/rounding/i64<->f64/…) stays at
2297        // `_ => None` (loud-skip) until a later increment wires it.
2298        F64Const { value } => Some(WasmOp::F64Const(f64::from_bits(value.bits()))),
2299        F64PromoteF32 => Some(WasmOp::F64PromoteF32),
2300        F64Add => Some(WasmOp::F64Add),
2301        F64Sub => Some(WasmOp::F64Sub),
2302        F64Mul => Some(WasmOp::F64Mul),
2303        F64Div => Some(WasmOp::F64Div),
2304        F64Abs => Some(WasmOp::F64Abs),
2305        F64Neg => Some(WasmOp::F64Neg),
2306        F64Sqrt => Some(WasmOp::F64Sqrt),
2307        F64Eq => Some(WasmOp::F64Eq),
2308        F64Ne => Some(WasmOp::F64Ne),
2309        F64Lt => Some(WasmOp::F64Lt),
2310        F64Le => Some(WasmOp::F64Le),
2311        F64Gt => Some(WasmOp::F64Gt),
2312        F64Ge => Some(WasmOp::F64Ge),
2313        F64Load { memarg } => Some(WasmOp::F64Load {
2314            offset: memarg.offset as u32,
2315            align: memarg.align as u32,
2316        }),
2317        F64Store { memarg } => Some(WasmOp::F64Store {
2318            offset: memarg.offset as u32,
2319            align: memarg.align as u32,
2320        }),
2321        // GI-FPU-002 phase 3 (#369): the f64 op tail — rounding via single
2322        // VRINT{P,M,Z,N}.F64 (FPv5), min/max via VMINNM/VMAXNM + the
2323        // NaN-propagating fix-up, copysign via the VABS/conditional-VNEG
2324        // splice, f32.demote_f64 / i32<->f64 conversions via VCVT
2325        // (i32.trunc_f64_* carries the #709 trap-on-out-of-range domain
2326        // guard). Still m7dp-only (the selector preamble honest-rejects any
2327        // f64 op elsewhere). The i64<->f64 reinterprets were un-dropped by
2328        // #851 and the i64<->f64 conversions by #869 (below).
2329        F64Ceil => Some(WasmOp::F64Ceil),
2330        F64Floor => Some(WasmOp::F64Floor),
2331        F64Trunc => Some(WasmOp::F64Trunc),
2332        F64Nearest => Some(WasmOp::F64Nearest),
2333        F64Min => Some(WasmOp::F64Min),
2334        F64Max => Some(WasmOp::F64Max),
2335        F64Copysign => Some(WasmOp::F64Copysign),
2336        F32DemoteF64 => Some(WasmOp::F32DemoteF64),
2337        F64ConvertI32S => Some(WasmOp::F64ConvertI32S),
2338        F64ConvertI32U => Some(WasmOp::F64ConvertI32U),
2339        I32TruncF64S => Some(WasmOp::I32TruncF64S),
2340        I32TruncF64U => Some(WasmOp::I32TruncF64U),
2341        // #869 (+#756): the f64 half of the 64-bit integer<->float family —
2342        // i64->f64 exact two-word VCVT+scale+add, i64.trunc_f64_* via the
2343        // #709-class i64 domain guard + the #782 word-decompose. See the f32
2344        // group above for the family story.
2345        F64ConvertI64S => Some(WasmOp::F64ConvertI64S),
2346        F64ConvertI64U => Some(WasmOp::F64ConvertI64U),
2347        I64TruncF64S => Some(WasmOp::I64TruncF64S),
2348        I64TruncF64U => Some(WasmOp::I64TruncF64U),
2349        // #782a: f64-source trunc_sat twins (see the f32 group above). falcon
2350        // v1.123 carries 7× i32.trunc_sat_f64_s — the m7dp double-precision
2351        // VCVT twins lower the i32-target forms; i64 targets loud-decline.
2352        I32TruncSatF64S => Some(WasmOp::I32TruncSatF64S),
2353        I32TruncSatF64U => Some(WasmOp::I32TruncSatF64U),
2354        I64TruncSatF64S => Some(WasmOp::I64TruncSatF64S),
2355        I64TruncSatF64U => Some(WasmOp::I64TruncSatF64U),
2356
2357        // f32x4
2358        F32x4Add => Some(WasmOp::F32x4Add),
2359        F32x4Sub => Some(WasmOp::F32x4Sub),
2360        F32x4Mul => Some(WasmOp::F32x4Mul),
2361        F32x4Div => Some(WasmOp::F32x4Div),
2362        F32x4Abs => Some(WasmOp::F32x4Abs),
2363        F32x4Neg => Some(WasmOp::F32x4Neg),
2364        F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
2365        F32x4Eq => Some(WasmOp::F32x4Eq),
2366        F32x4Ne => Some(WasmOp::F32x4Ne),
2367        F32x4Lt => Some(WasmOp::F32x4Lt),
2368        F32x4Le => Some(WasmOp::F32x4Le),
2369        F32x4Gt => Some(WasmOp::F32x4Gt),
2370        F32x4Ge => Some(WasmOp::F32x4Ge),
2371        F32x4Splat => Some(WasmOp::F32x4Splat),
2372        F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
2373        F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
2374
2375        // Other operators not yet supported
2376        _ => None,
2377    }
2378}
2379
2380#[cfg(test)]
2381mod tests {
2382    use super::*;
2383
2384    #[test]
2385    fn test_decode_simple_add() {
2386        let wat = r#"
2387            (module
2388                (func (export "add") (param i32 i32) (result i32)
2389                    local.get 0
2390                    local.get 1
2391                    i32.add
2392                )
2393            )
2394        "#;
2395
2396        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2397        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2398
2399        assert_eq!(functions.len(), 1);
2400        assert_eq!(functions[0].index, 0);
2401        assert_eq!(functions[0].export_name, Some("add".to_string()));
2402        assert_eq!(
2403            functions[0].ops,
2404            vec![
2405                WasmOp::LocalGet(0),
2406                WasmOp::LocalGet(1),
2407                WasmOp::I32Add,
2408                WasmOp::End
2409            ]
2410        );
2411    }
2412
2413    /// #782a: the 0xFC-prefixed trunc_sat family must DECODE (it was
2414    /// previously unmapped → the whole function loud-skipped as
2415    /// "unsupported operator", the falcon #782 `ts32`/`ts64` class). All
2416    /// eight forms must surface as their `WasmOp` variants so the selectors
2417    /// can lower (ARM32 i32-targets, aarch64 all) or LOUD-decline (ARM32
2418    /// i64-targets, RV32) per backend.
2419    #[test]
2420    fn test_decode_trunc_sat_family() {
2421        let wat = r#"
2422            (module
2423                (func (export "ts") (param f32 f64) (result i32)
2424                    (i32.trunc_sat_f32_s (local.get 0))
2425                    (i32.trunc_sat_f32_u (local.get 0))
2426                    i32.add
2427                    (i32.trunc_sat_f64_s (local.get 1))
2428                    i32.add
2429                    (i32.trunc_sat_f64_u (local.get 1))
2430                    i32.add
2431                    (i32.wrap_i64 (i64.trunc_sat_f32_s (local.get 0)))
2432                    i32.add
2433                    (i32.wrap_i64 (i64.trunc_sat_f32_u (local.get 0)))
2434                    i32.add
2435                    (i32.wrap_i64 (i64.trunc_sat_f64_s (local.get 1)))
2436                    i32.add
2437                    (i32.wrap_i64 (i64.trunc_sat_f64_u (local.get 1)))
2438                    i32.add
2439                )
2440            )
2441        "#;
2442        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2443        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2444        assert_eq!(functions.len(), 1);
2445        let f = &functions[0];
2446        assert!(
2447            f.unsupported.is_none(),
2448            "trunc_sat must decode, not loud-skip: {:?}",
2449            f.unsupported
2450        );
2451        for want in [
2452            WasmOp::I32TruncSatF32S,
2453            WasmOp::I32TruncSatF32U,
2454            WasmOp::I32TruncSatF64S,
2455            WasmOp::I32TruncSatF64U,
2456            WasmOp::I64TruncSatF32S,
2457            WasmOp::I64TruncSatF32U,
2458            WasmOp::I64TruncSatF64S,
2459            WasmOp::I64TruncSatF64U,
2460        ] {
2461            assert!(
2462                f.ops.contains(&want),
2463                "decoded ops must contain {want:?}: {:?}",
2464                f.ops
2465            );
2466        }
2467    }
2468
2469    /// #204 regression: `i64.extend_i32_u`, `i64.extend_i32_s` and
2470    /// `i32.wrap_i64` must DECODE (they were previously unmapped → silently
2471    /// dropped by `convert_operator`, leaving an i32 value as a 64-bit operand
2472    /// with a garbage high half — the root cause of gale's miscompiled
2473    /// `(new_count << 32)` pack). The decoder must surface all three.
2474    #[test]
2475    fn test_decode_i64_i32_width_conversions() {
2476        let wat = r#"
2477            (module
2478                (func (export "conv") (param i32 i64) (result i32)
2479                    local.get 0
2480                    i64.extend_i32_u
2481                    local.get 0
2482                    i64.extend_i32_s
2483                    i64.add
2484                    local.get 1
2485                    i64.add
2486                    i32.wrap_i64
2487                )
2488            )
2489        "#;
2490        let wasm = wat::parse_str(wat).expect("parse");
2491        let functions = decode_wasm_functions(&wasm).expect("decode");
2492        let ops = &functions[0].ops;
2493        assert!(
2494            ops.contains(&WasmOp::I64ExtendI32U),
2495            "i64.extend_i32_u must decode (not be dropped): {ops:?}"
2496        );
2497        assert!(
2498            ops.contains(&WasmOp::I64ExtendI32S),
2499            "i64.extend_i32_s must decode (not be dropped): {ops:?}"
2500        );
2501        assert!(
2502            ops.contains(&WasmOp::I32WrapI64),
2503            "i32.wrap_i64 must decode (not be dropped): {ops:?}"
2504        );
2505    }
2506
2507    /// #204 WAKE-path regression: `br_table` must DECODE (it was unmapped in
2508    /// `convert_operator` → silently dropped, so the selector emitted no index
2509    /// dispatch and every `br_table` fell through to target 0 — gale's binary
2510    /// semaphore never took its WAKE branch). Targets + default are preserved.
2511    #[test]
2512    fn test_decode_br_table() {
2513        let wat = r#"
2514            (module
2515                (func (export "bt") (param i32) (result i32)
2516                    (block (block (block
2517                        local.get 0
2518                        br_table 2 0 1 2)
2519                      i32.const 30 return)
2520                      i32.const 20 return)
2521                    i32.const 10))
2522        "#;
2523        let wasm = wat::parse_str(wat).expect("parse");
2524        let functions = decode_wasm_functions(&wasm).expect("decode");
2525        let bt = functions[0]
2526            .ops
2527            .iter()
2528            .find_map(|o| match o {
2529                WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
2530                _ => None,
2531            })
2532            .expect("br_table must decode (not be dropped)");
2533        assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
2534        assert_eq!(bt.1, 2, "br_table default preserved");
2535    }
2536
2537    #[test]
2538    fn test_decode_arithmetic() {
2539        let wat = r#"
2540            (module
2541                (func (export "calc") (result i32)
2542                    i32.const 5
2543                    i32.const 3
2544                    i32.mul
2545                    i32.const 2
2546                    i32.add
2547                )
2548            )
2549        "#;
2550
2551        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2552        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2553
2554        assert_eq!(functions.len(), 1);
2555        assert_eq!(functions[0].export_name, Some("calc".to_string()));
2556        assert_eq!(
2557            functions[0].ops,
2558            vec![
2559                WasmOp::I32Const(5),
2560                WasmOp::I32Const(3),
2561                WasmOp::I32Mul,
2562                WasmOp::I32Const(2),
2563                WasmOp::I32Add,
2564                WasmOp::End,
2565            ]
2566        );
2567    }
2568
2569    #[test]
2570    fn test_decode_multi_function_module() {
2571        let wat = r#"
2572            (module
2573                (func $helper)
2574                (func (export "add") (param i32 i32) (result i32)
2575                    local.get 0
2576                    local.get 1
2577                    i32.add
2578                )
2579                (func (export "sub") (param i32 i32) (result i32)
2580                    local.get 0
2581                    local.get 1
2582                    i32.sub
2583                )
2584            )
2585        "#;
2586
2587        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2588        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2589
2590        assert_eq!(functions.len(), 3);
2591        assert_eq!(functions[0].index, 0);
2592        assert_eq!(functions[0].export_name, None);
2593        assert_eq!(functions[1].index, 1);
2594        assert_eq!(functions[1].export_name, Some("add".to_string()));
2595        assert_eq!(functions[2].index, 2);
2596        assert_eq!(functions[2].export_name, Some("sub".to_string()));
2597    }
2598
2599    #[test]
2600    fn test_decode_module_with_imports() {
2601        let wat = r#"
2602            (module
2603                (import "env" "log" (func $log (param i32)))
2604                (import "env" "memory" (memory 1))
2605                (func (export "run") (param i32)
2606                    local.get 0
2607                    call 0
2608                )
2609            )
2610        "#;
2611
2612        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2613        let module = decode_wasm_module(&wasm).expect("Failed to decode");
2614
2615        // Should have 2 imports (1 func, 1 memory)
2616        assert_eq!(module.imports.len(), 2);
2617        assert_eq!(module.num_imported_funcs, 1);
2618
2619        // First import is the function
2620        assert_eq!(module.imports[0].module, "env");
2621        assert_eq!(module.imports[0].name, "log");
2622        assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
2623
2624        // Second import is memory
2625        assert_eq!(module.imports[1].module, "env");
2626        assert_eq!(module.imports[1].name, "memory");
2627        assert_eq!(module.imports[1].kind, ImportKind::Memory);
2628
2629        // Should have 1 local function (index 1, because import is index 0)
2630        assert_eq!(module.functions.len(), 1);
2631        assert_eq!(module.functions[0].index, 1);
2632        assert_eq!(module.functions[0].export_name, Some("run".to_string()));
2633    }
2634
2635    #[test]
2636    fn test_find_function_by_export_name() {
2637        let wat = r#"
2638            (module
2639                (func $helper)
2640                (func (export "add") (param i32 i32) (result i32)
2641                    local.get 0
2642                    local.get 1
2643                    i32.add
2644                )
2645            )
2646        "#;
2647
2648        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2649        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2650
2651        let add_func = functions
2652            .iter()
2653            .find(|f| f.export_name.as_deref() == Some("add"))
2654            .expect("Should find 'add' function");
2655
2656        assert_eq!(add_func.index, 1);
2657        assert!(add_func.ops.contains(&WasmOp::I32Add));
2658    }
2659
2660    #[test]
2661    fn test_decode_subword_loads() {
2662        let wat = r#"
2663            (module
2664                (memory 1)
2665                (func (export "test") (param i32) (result i32)
2666                    local.get 0
2667                    i32.load8_u
2668                )
2669            )
2670        "#;
2671
2672        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2673        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2674
2675        assert_eq!(functions.len(), 1);
2676        assert!(functions[0].ops.contains(&WasmOp::I32Load8U {
2677            offset: 0,
2678            align: 0,
2679        }));
2680    }
2681
2682    #[test]
2683    fn test_decode_subword_stores() {
2684        let wat = r#"
2685            (module
2686                (memory 1)
2687                (func (export "test") (param i32 i32)
2688                    local.get 0
2689                    local.get 1
2690                    i32.store8
2691                )
2692            )
2693        "#;
2694
2695        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2696        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2697
2698        assert_eq!(functions.len(), 1);
2699        assert!(functions[0].ops.contains(&WasmOp::I32Store8 {
2700            offset: 0,
2701            align: 0,
2702        }));
2703    }
2704
2705    #[test]
2706    fn test_decode_memory_size_grow() {
2707        let wat = r#"
2708            (module
2709                (memory 1)
2710                (func (export "test") (result i32)
2711                    memory.size
2712                )
2713            )
2714        "#;
2715
2716        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2717        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2718
2719        assert_eq!(functions.len(), 1);
2720        assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
2721    }
2722
2723    #[test]
2724    fn test_decode_memory_grow() {
2725        let wat = r#"
2726            (module
2727                (memory 1)
2728                (func (export "test") (param i32) (result i32)
2729                    local.get 0
2730                    memory.grow
2731                )
2732            )
2733        "#;
2734
2735        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2736        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2737
2738        assert_eq!(functions.len(), 1);
2739        assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
2740    }
2741
2742    #[test]
2743    fn test_decode_bulk_memory_374() {
2744        // #374: memory.copy / memory.fill on the single linear memory decode to
2745        // the new WasmOp variants (was `_ => None` -> loud-skip).
2746        let wat = r#"
2747            (module
2748                (memory 1)
2749                (func (export "cpy") (param i32 i32 i32)
2750                    local.get 0 local.get 1 local.get 2 memory.copy)
2751                (func (export "fil") (param i32 i32 i32)
2752                    local.get 0 local.get 1 local.get 2 memory.fill)
2753            )
2754        "#;
2755        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2756        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2757        assert_eq!(functions.len(), 2);
2758        assert!(functions[0].ops.contains(&WasmOp::MemoryCopy));
2759        assert!(functions[1].ops.contains(&WasmOp::MemoryFill));
2760        // Neither function is flagged unsupported (they now lower).
2761        assert!(functions[0].unsupported.is_none());
2762        assert!(functions[1].unsupported.is_none());
2763    }
2764
2765    #[test]
2766    fn test_decode_i64_subword_loads() {
2767        let wat = r#"
2768            (module
2769                (memory 1)
2770                (func (export "test") (param i32) (result i64)
2771                    local.get 0
2772                    i64.load8_s
2773                )
2774            )
2775        "#;
2776
2777        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2778        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2779
2780        assert_eq!(functions.len(), 1);
2781        assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
2782            offset: 0,
2783            align: 0,
2784        }));
2785    }
2786
2787    #[test]
2788    fn test_decode_all_subword_memory_ops() {
2789        // Test that all sub-word operations are decoded from WAT
2790        let wat = r#"
2791            (module
2792                (memory 1)
2793                (func (export "test") (param i32)
2794                    ;; i32 sub-word loads
2795                    local.get 0
2796                    i32.load8_s
2797                    drop
2798                    local.get 0
2799                    i32.load8_u
2800                    drop
2801                    local.get 0
2802                    i32.load16_s
2803                    drop
2804                    local.get 0
2805                    i32.load16_u
2806                    drop
2807
2808                    ;; i32 sub-word stores
2809                    local.get 0
2810                    i32.const 42
2811                    i32.store8
2812                    local.get 0
2813                    i32.const 42
2814                    i32.store16
2815
2816                    ;; i64 sub-word loads
2817                    local.get 0
2818                    i64.load8_s
2819                    drop
2820                    local.get 0
2821                    i64.load8_u
2822                    drop
2823                    local.get 0
2824                    i64.load16_s
2825                    drop
2826                    local.get 0
2827                    i64.load16_u
2828                    drop
2829                    local.get 0
2830                    i64.load32_s
2831                    drop
2832                    local.get 0
2833                    i64.load32_u
2834                    drop
2835
2836                    ;; i64 sub-word stores
2837                    local.get 0
2838                    i64.const 42
2839                    i64.store8
2840                    local.get 0
2841                    i64.const 42
2842                    i64.store16
2843                    local.get 0
2844                    i64.const 42
2845                    i64.store32
2846                )
2847            )
2848        "#;
2849
2850        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2851        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2852
2853        assert_eq!(functions.len(), 1);
2854        let ops = &functions[0].ops;
2855
2856        // Verify i32 sub-word ops are present
2857        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
2858        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
2859        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
2860        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
2861        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
2862        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
2863
2864        // Verify i64 sub-word ops are present
2865        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
2866        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
2867        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
2868        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
2869        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
2870        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
2871        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
2872        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
2873        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
2874    }
2875
2876    #[test]
2877    fn test_decode_simd_i32x4_add() {
2878        let wat = r#"
2879            (module
2880                (func (export "add_v128") (param v128 v128) (result v128)
2881                    local.get 0
2882                    local.get 1
2883                    i32x4.add
2884                )
2885            )
2886        "#;
2887
2888        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2889        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2890
2891        assert_eq!(functions.len(), 1);
2892        assert!(
2893            functions[0].ops.contains(&WasmOp::I32x4Add),
2894            "Should decode i32x4.add: {:?}",
2895            functions[0].ops
2896        );
2897    }
2898
2899    #[test]
2900    fn test_decode_simd_v128_const() {
2901        let wat = r#"
2902            (module
2903                (func (export "const_v128") (result v128)
2904                    v128.const i32x4 1 2 3 4
2905                )
2906            )
2907        "#;
2908
2909        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2910        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2911
2912        assert_eq!(functions.len(), 1);
2913        assert!(
2914            functions[0]
2915                .ops
2916                .iter()
2917                .any(|o| matches!(o, WasmOp::V128Const(_))),
2918            "Should decode v128.const: {:?}",
2919            functions[0].ops
2920        );
2921    }
2922
2923    #[test]
2924    fn test_decode_simd_v128_load_store() {
2925        let wat = r#"
2926            (module
2927                (memory 1)
2928                (func (export "load_store") (param i32)
2929                    local.get 0
2930                    v128.load
2931                    local.get 0
2932                    v128.store
2933                )
2934            )
2935        "#;
2936
2937        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2938        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2939
2940        assert_eq!(functions.len(), 1);
2941        let ops = &functions[0].ops;
2942        assert!(
2943            ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
2944            "Should decode v128.load"
2945        );
2946        assert!(
2947            ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
2948            "Should decode v128.store"
2949        );
2950    }
2951
2952    #[test]
2953    fn test_decode_simd_bitwise_ops() {
2954        let wat = r#"
2955            (module
2956                (func (export "bitwise") (param v128 v128) (result v128)
2957                    local.get 0
2958                    local.get 1
2959                    v128.and
2960                )
2961            )
2962        "#;
2963
2964        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2965        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2966
2967        assert_eq!(functions.len(), 1);
2968        assert!(functions[0].ops.contains(&WasmOp::V128And));
2969    }
2970
2971    #[test]
2972    fn test_decode_simd_splat() {
2973        let wat = r#"
2974            (module
2975                (func (export "splat") (param i32) (result v128)
2976                    local.get 0
2977                    i32x4.splat
2978                )
2979            )
2980        "#;
2981
2982        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2983        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2984
2985        assert_eq!(functions.len(), 1);
2986        assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
2987    }
2988
2989    #[test]
2990    fn test_decode_simd_extract_lane() {
2991        let wat = r#"
2992            (module
2993                (func (export "extract") (param v128) (result i32)
2994                    local.get 0
2995                    i32x4.extract_lane 2
2996                )
2997            )
2998        "#;
2999
3000        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
3001        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
3002
3003        assert_eq!(functions.len(), 1);
3004        assert!(
3005            functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
3006            "Should decode i32x4.extract_lane 2"
3007        );
3008    }
3009
3010    #[test]
3011    fn test_decode_simd_f32x4_arithmetic() {
3012        let wat = r#"
3013            (module
3014                (func (export "f32x4_add") (param v128 v128) (result v128)
3015                    local.get 0
3016                    local.get 1
3017                    f32x4.add
3018                )
3019            )
3020        "#;
3021
3022        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
3023        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
3024
3025        assert_eq!(functions.len(), 1);
3026        assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
3027    }
3028
3029    #[test]
3030    fn test_369_scalar_float_op_flags_function_unsupported_not_dropped() {
3031        // GI-FPU-002 (#619): the in-scope scalar f32 ops (add/sub/mul/div,
3032        // comparisons, i32.trunc_f32_s/u, f32.convert_i32_s/u, f32.const) are
3033        // now DECODED (routed to the VFP selector on FPU targets), so `f32.add`
3034        // is no longer flagged — and since phase 2 (#369) so is the lowered
3035        // f64 subset (`f64.add` here; the m7dp-only capability gate lives in
3036        // the selector). Since phase 3 the f64 op TAIL (`f64.min` here) is
3037        // decoded too; and since #869 the 64-bit integer<->float conversion
3038        // family (`i64.trunc_f64_s` here) decodes as well — the scalar float
3039        // decode surface is complete, with capability gating (m7dp-only)
3040        // living in the selector. The flag-never-drop honesty contract for
3041        // the remaining undecoded float surface is pinned by the
3042        // float-global test below. A pure-integer function stays clean.
3043        let wat = r#"
3044            (module
3045                (func (export "fadd") (param f32 f32) (result f32)
3046                    local.get 0 local.get 1 f32.add)
3047                (func (export "dadd") (param f64 f64) (result f64)
3048                    local.get 0 local.get 1 f64.add)
3049                (func (export "dmin") (param f64 f64) (result f64)
3050                    local.get 0 local.get 1 f64.min)
3051                (func (export "dtrunc64") (param f64) (result i64)
3052                    local.get 0 i64.trunc_f64_s)
3053                (func (export "iadd") (param i32 i32) (result i32)
3054                    local.get 0 local.get 1 i32.add))
3055        "#;
3056        let wasm = wat::parse_str(wat).expect("parse");
3057        let functions = decode_wasm_functions(&wasm).expect("decode");
3058        let fadd = functions
3059            .iter()
3060            .find(|f| f.export_name.as_deref() == Some("fadd"))
3061            .unwrap();
3062        let dadd = functions
3063            .iter()
3064            .find(|f| f.export_name.as_deref() == Some("dadd"))
3065            .unwrap();
3066        let dmin = functions
3067            .iter()
3068            .find(|f| f.export_name.as_deref() == Some("dmin"))
3069            .unwrap();
3070        let dtrunc64 = functions
3071            .iter()
3072            .find(|f| f.export_name.as_deref() == Some("dtrunc64"))
3073            .unwrap();
3074        let iadd = functions
3075            .iter()
3076            .find(|f| f.export_name.as_deref() == Some("iadd"))
3077            .unwrap();
3078        // In-scope f32 op: now decoded (reachable), not flagged.
3079        assert!(
3080            fadd.unsupported.is_none(),
3081            "GI-FPU-002: f32.add must now decode (not be flagged), got {:?}",
3082            fadd.unsupported
3083        );
3084        assert!(
3085            fadd.ops.contains(&WasmOp::F32Add),
3086            "f32.add must decode to WasmOp::F32Add: {:?}",
3087            fadd.ops
3088        );
3089        // In-scope f64 op (phase 2, #369): now decoded, not flagged.
3090        assert!(
3091            dadd.unsupported.is_none(),
3092            "GI-FPU-002 phase 2: f64.add must now decode (not be flagged), got {:?}",
3093            dadd.unsupported
3094        );
3095        assert!(
3096            dadd.ops.contains(&WasmOp::F64Add),
3097            "f64.add must decode to WasmOp::F64Add: {:?}",
3098            dadd.ops
3099        );
3100        // In-scope f64 tail op (phase 3, #369): now decoded, not flagged.
3101        assert!(
3102            dmin.unsupported.is_none(),
3103            "GI-FPU-002 phase 3: f64.min must now decode (not be flagged), got {:?}",
3104            dmin.unsupported
3105        );
3106        assert!(
3107            dmin.ops.contains(&WasmOp::F64Min),
3108            "f64.min must decode to WasmOp::F64Min: {:?}",
3109            dmin.ops
3110        );
3111        // #869: the i64<->f64 conversions are now IN scope — decoded, not
3112        // flagged (the m7dp capability gate lives in the selector preamble).
3113        assert!(
3114            dtrunc64.unsupported.is_none(),
3115            "#869: i64.trunc_f64_s must now decode (not be flagged), got {:?}",
3116            dtrunc64.unsupported
3117        );
3118        assert!(
3119            dtrunc64.ops.contains(&WasmOp::I64TruncF64S),
3120            "i64.trunc_f64_s must decode to WasmOp::I64TruncF64S: {:?}",
3121            dtrunc64.ops
3122        );
3123        assert!(
3124            iadd.unsupported.is_none(),
3125            "a pure-integer function must NOT be flagged: {:?}",
3126            iadd.unsupported
3127        );
3128    }
3129
3130    #[test]
3131    fn test_369_float_global_access_flags_function_unsupported() {
3132        // GI-FPU-001 (#369): `global.get`/`global.set` on an f32/f64-typed
3133        // global decode fine (the ops are type-agnostic), but the float
3134        // initializer is dropped (`init_i32: None` -> slot zeroed), so a read
3135        // returned a silently-wrong 0.0 instead of the init (verified: the
3136        // 2.5f bit pattern 0x40200000 was absent from the output ELF). The
3137        // access must flag the function for the loud-skip path. Accesses to
3138        // integer globals stay clean.
3139        let wat = r#"
3140            (module
3141                (global $fg f32 (f32.const 2.5))
3142                (global $dg (mut f64) (f64.const 1.5))
3143                (global $ig (mut i32) (i32.const 7))
3144                (func (export "fget") (result f32) global.get $fg)
3145                (func (export "dset") (param f64) local.get 0 global.set $dg)
3146                (func (export "iget") (result i32) global.get $ig))
3147        "#;
3148        let wasm = wat::parse_str(wat).expect("parse");
3149
3150        // Both decode entry points must flag (the CLI compiles through both:
3151        // decode_wasm_module on the all-exports/module paths,
3152        // decode_wasm_functions on the single-function path).
3153        let module = decode_wasm_module(&wasm).expect("decode module");
3154        for functions in [
3155            &module.functions,
3156            &decode_wasm_functions(&wasm).expect("decode fns"),
3157        ] {
3158            let by_name = |n: &str| {
3159                functions
3160                    .iter()
3161                    .find(|f| f.export_name.as_deref() == Some(n))
3162                    .unwrap()
3163            };
3164            let fget = by_name("fget");
3165            assert!(
3166                fget.unsupported.is_some(),
3167                "global.get of an f32 global must flag the function (loud-skip), got {:?}",
3168                fget.unsupported
3169            );
3170            let reason = fget.unsupported.as_deref().unwrap();
3171            assert!(
3172                reason.contains("GlobalGet") && reason.contains("GI-FPU-001"),
3173                "diagnostic should name the op and GI-FPU-001: {reason:?}"
3174            );
3175            let dset = by_name("dset");
3176            assert!(
3177                dset.unsupported
3178                    .as_deref()
3179                    .is_some_and(|r| r.contains("GlobalSet")),
3180                "global.set of an f64 global must flag the function, got {:?}",
3181                dset.unsupported
3182            );
3183            assert!(
3184                by_name("iget").unsupported.is_none(),
3185                "an i32 global access must NOT be flagged: {:?}",
3186                by_name("iget").unsupported
3187            );
3188        }
3189    }
3190
3191    #[test]
3192    fn test_369_imported_float_global_shifts_index_space() {
3193        // GI-FPU-001 (#369): imported globals come FIRST in the global index
3194        // space. An imported f64 global at index 0 must be flagged, and the
3195        // defined i32 global at index 1 must NOT be mistaken for it.
3196        let wat = r#"
3197            (module
3198                (import "env" "fg" (global f64))
3199                (global $ig i32 (i32.const 3))
3200                (func (export "fget") (result f64) global.get 0)
3201                (func (export "iget") (result i32) global.get 1))
3202        "#;
3203        let wasm = wat::parse_str(wat).expect("parse");
3204        let functions = decode_wasm_functions(&wasm).expect("decode");
3205        let by_name = |n: &str| {
3206            functions
3207                .iter()
3208                .find(|f| f.export_name.as_deref() == Some(n))
3209                .unwrap()
3210        };
3211        assert!(
3212            by_name("fget")
3213                .unsupported
3214                .as_deref()
3215                .is_some_and(|r| r.contains("GI-FPU-001")),
3216            "imported f64 global access must flag: {:?}",
3217            by_name("fget").unsupported
3218        );
3219        assert!(
3220            by_name("iget").unsupported.is_none(),
3221            "defined i32 global at shifted index 1 must NOT flag: {:?}",
3222            by_name("iget").unsupported
3223        );
3224    }
3225
3226    #[test]
3227    fn test_680_simd_ops_flag_function_unsupported_not_dropped() {
3228        // #680: SIMD (v128) ops decode into WasmOp variants no production
3229        // target can select (`has_helium` is test-only), so they were silently
3230        // dropped at selection — `i32x4.add` compiled to an operand
3231        // passthrough (`mov r0,r1`) and shipped a wrong result. The issue's
3232        // exact reproducer must flag the function; the scalar sibling must
3233        // stay compilable (non-vacuity).
3234        let wat = r#"
3235            (module
3236                (memory 1)
3237                (func (export "vadd") (param i32 i32) (result i32)
3238                    (i32x4.extract_lane 2
3239                        (i32x4.add (i32x4.splat (local.get 0))
3240                                   (i32x4.splat (local.get 1)))))
3241                (func (export "vstore") (param i32 i32) (result i32)
3242                    (v128.store (i32.const 0) (i32x4.splat (local.get 0)))
3243                    (i32.load (i32.const 0)))
3244                (func (export "iadd") (param i32 i32) (result i32)
3245                    local.get 0 local.get 1 i32.add))
3246        "#;
3247        let wasm = wat::parse_str(wat).expect("parse");
3248
3249        // Both decode entry points must flag (the CLI compiles through both).
3250        let module = decode_wasm_module(&wasm).expect("decode module");
3251        for functions in [
3252            &module.functions,
3253            &decode_wasm_functions(&wasm).expect("decode fns"),
3254        ] {
3255            let by_name = |n: &str| {
3256                functions
3257                    .iter()
3258                    .find(|f| f.export_name.as_deref() == Some(n))
3259                    .unwrap()
3260            };
3261            for name in ["vadd", "vstore"] {
3262                let reason = by_name(name).unsupported.as_deref();
3263                assert!(
3264                    reason.is_some(),
3265                    "{name}: v128 ops must flag the function (loud-skip), got None"
3266                );
3267                let reason = reason.unwrap();
3268                assert!(
3269                    reason.contains("no SIMD lowering for this target") && reason.contains("#680"),
3270                    "{name}: diagnostic must name the target gap and #680: {reason:?}"
3271                );
3272            }
3273            // The reason names the FIRST SIMD op hit (splat in both bodies).
3274            assert!(
3275                by_name("vadd")
3276                    .unsupported
3277                    .as_deref()
3278                    .unwrap()
3279                    .contains("I32x4Splat"),
3280                "diagnostic should name the op: {:?}",
3281                by_name("vadd").unsupported
3282            );
3283            assert!(
3284                by_name("iadd").unsupported.is_none(),
3285                "a scalar function in the same module must NOT be flagged: {:?}",
3286                by_name("iadd").unsupported
3287            );
3288        }
3289    }
3290
3291    #[test]
3292    fn test_680_v128_local_and_signature_flag_function() {
3293        // #680: v128 VALUES are expressible with ZERO SIMD-proposal operators
3294        // in the body — a v128-typed local or a v128 param/result is reached
3295        // through type-agnostic `local.get`/`local.set`, which the selectors
3296        // lower as 4-byte moves (silent 16-byte truncation). Both must flag.
3297        let wat = r#"
3298            (module
3299                (func (export "vlocal") (result i32) (local v128)
3300                    i32.const 7)
3301                (func (export "vpass") (param v128) (result v128)
3302                    local.get 0)
3303                (func (export "scalar") (param i32) (result i32)
3304                    local.get 0))
3305        "#;
3306        let wasm = wat::parse_str(wat).expect("parse");
3307        let module = decode_wasm_module(&wasm).expect("decode module");
3308        for functions in [
3309            &module.functions,
3310            &decode_wasm_functions(&wasm).expect("decode fns"),
3311        ] {
3312            let by_name = |n: &str| {
3313                functions
3314                    .iter()
3315                    .find(|f| f.export_name.as_deref() == Some(n))
3316                    .unwrap()
3317            };
3318            assert!(
3319                by_name("vlocal")
3320                    .unsupported
3321                    .as_deref()
3322                    .is_some_and(|r| r.contains("v128-typed local") && r.contains("#680")),
3323                "a v128-typed local declaration must flag: {:?}",
3324                by_name("vlocal").unsupported
3325            );
3326            assert!(
3327                by_name("vpass")
3328                    .unsupported
3329                    .as_deref()
3330                    .is_some_and(|r| r.contains("v128 param/result") && r.contains("#680")),
3331                "a v128 param/result signature must flag (op-free body!): {:?}",
3332                by_name("vpass").unsupported
3333            );
3334            assert!(
3335                by_name("scalar").unsupported.is_none(),
3336                "a scalar function must NOT be flagged: {:?}",
3337                by_name("scalar").unsupported
3338            );
3339        }
3340    }
3341
3342    #[test]
3343    fn test_680_v128_global_access_flags_function() {
3344        // #680: `global.get`/`global.set` on a v128-typed global decode fine
3345        // (type-agnostic ops), but the access would move 4 of the 16 bytes and
3346        // the `v128.const` initializer is never captured. Same lane as the
3347        // float globals (#648/GI-FPU-001); imported globals shift the index
3348        // space (imports first). The i32-global sibling stays compilable.
3349        let wat = r#"
3350            (module
3351                (import "env" "vg" (global v128))
3352                (global $ig (mut i32) (i32.const 7))
3353                (global $dg (mut v128) (v128.const i32x4 1 2 3 4))
3354                (func (export "vget") global.get 0 drop)
3355                (func (export "iget") (result i32) global.get $ig))
3356        "#;
3357        let wasm = wat::parse_str(wat).expect("parse");
3358        let module = decode_wasm_module(&wasm).expect("decode module");
3359        for functions in [
3360            &module.functions,
3361            &decode_wasm_functions(&wasm).expect("decode fns"),
3362        ] {
3363            let by_name = |n: &str| {
3364                functions
3365                    .iter()
3366                    .find(|f| f.export_name.as_deref() == Some(n))
3367                    .unwrap()
3368            };
3369            let reason = by_name("vget").unsupported.as_deref();
3370            assert!(
3371                reason.is_some_and(|r| r.contains("GlobalGet")
3372                    && r.contains("v128-typed global")
3373                    && r.contains("#680")),
3374                "global.get of an imported v128 global must flag: {reason:?}"
3375            );
3376            assert!(
3377                by_name("iget").unsupported.is_none(),
3378                "an i32 global access must NOT be flagged: {:?}",
3379                by_name("iget").unsupported
3380            );
3381        }
3382    }
3383
3384    #[test]
3385    fn test_decode_simd_multiple_ops() {
3386        let wat = r#"
3387            (module
3388                (func (export "simd_ops") (param v128 v128 v128) (result v128)
3389                    ;; (a + b) * c
3390                    local.get 0
3391                    local.get 1
3392                    i32x4.add
3393                    local.get 2
3394                    i32x4.mul
3395                )
3396            )
3397        "#;
3398
3399        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
3400        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
3401
3402        assert_eq!(functions.len(), 1);
3403        let ops = &functions[0].ops;
3404        assert!(ops.contains(&WasmOp::I32x4Add));
3405        assert!(ops.contains(&WasmOp::I32x4Mul));
3406    }
3407
3408    /// VCR-DBG-001 step 1 (#394): the decoder records a module-relative wasm byte
3409    /// offset per emitted op — the DWARF-for-wasm address space that bridges
3410    /// synth's op-index `source_line` to the input wasm's `.debug_line`. Purely
3411    /// additive metadata (no codegen consumer ⇒ frozen fixtures byte-identical,
3412    /// verified separately); this test pins the structural invariants.
3413    #[test]
3414    fn test_decode_records_aligned_increasing_op_offsets_dbg001() {
3415        let wat = r#"
3416            (module
3417                (func (export "f") (param i32 i32) (result i32)
3418                    local.get 0
3419                    local.get 1
3420                    i32.add
3421                    i32.const 7
3422                    i32.mul))
3423        "#;
3424        let wasm = wat::parse_str(wat).expect("parse WAT");
3425        let functions = decode_wasm_functions(&wasm).expect("decode");
3426        let f = &functions[0];
3427
3428        // One offset per emitted op, index-aligned with `ops`.
3429        assert_eq!(
3430            f.op_offsets.len(),
3431            f.ops.len(),
3432            "op_offsets must be parallel to ops"
3433        );
3434        assert!(!f.op_offsets.is_empty());
3435
3436        // Byte offsets are strictly increasing through the body (each op consumes
3437        // at least one byte) and module-relative (well past the header).
3438        assert!(
3439            f.op_offsets.windows(2).all(|w| w[1] > w[0]),
3440            "wasm byte offsets must strictly increase: {:?}",
3441            f.op_offsets
3442        );
3443        assert!(
3444            f.op_offsets[0] >= 8,
3445            "module-relative offset is past the 8-byte wasm header"
3446        );
3447    }
3448
3449    /// #237: the decoder captures a global's `i32.const` initializer + mutability,
3450    /// so the native-pointer ABI can recognize the stack-pointer global.
3451    #[test]
3452    fn test_decode_captures_global_initializer() {
3453        let wat = r#"
3454            (module
3455                (memory 2)
3456                (global $__stack_pointer (mut i32) (i32.const 65536))
3457                (global $immutable_const i32 (i32.const 7))
3458                (func (export "f") (result i32) global.get 0)
3459            )
3460        "#;
3461        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3462        let module = decode_wasm_module(&wasm).expect("Failed to decode");
3463
3464        assert_eq!(module.globals.len(), 2, "both globals captured");
3465        let sp = &module.globals[0];
3466        assert_eq!(sp.index, 0);
3467        assert_eq!(
3468            sp.init,
3469            Some(GlobalInit::I32(65536)),
3470            "stack-pointer init captured"
3471        );
3472        assert!(sp.mutable, "stack pointer is mutable");
3473        let c = &module.globals[1];
3474        assert_eq!(c.init, Some(GlobalInit::I32(7)));
3475        assert!(!c.mutable, "second global is immutable");
3476        assert_eq!(sp.slot_bytes, 4, "i32 global occupies one 4-byte slot");
3477        assert_eq!(c.slot_bytes, 4);
3478    }
3479
3480    /// #643: the decoder records the DECLARED slot width per global — an i64
3481    /// (or f64) global occupies 8 bytes, so the globals-table layout can give
3482    /// it room for both words and shift every later global's offset.
3483    #[test]
3484    fn test_decode_records_global_slot_widths_643() {
3485        let wat = r#"
3486            (module
3487                (global $c (mut i64) (i64.const 0))
3488                (global $k (mut i32) (i32.const 0))
3489                (global $f (mut f64) (f64.const 0))
3490                (func (export "f") (result i32) global.get 1)
3491            )
3492        "#;
3493        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3494        let module = decode_wasm_module(&wasm).expect("Failed to decode");
3495
3496        assert_eq!(module.globals.len(), 3);
3497        assert_eq!(module.globals[0].slot_bytes, 8, "i64 global is 8 bytes");
3498        assert_eq!(module.globals[1].slot_bytes, 4, "i32 global is 4 bytes");
3499        assert_eq!(module.globals[2].slot_bytes, 8, "f64 global is 8 bytes");
3500    }
3501
3502    /// #649: a nonzero `i64.const` initializer is captured as BOTH words — the
3503    /// `init_i32`-shaped capture dropped it to `None` and every consumer's
3504    /// `unwrap_or(0)` silently ZEROED the global. f32/f64 inits stay `None`
3505    /// (GI-FPU-001/#369 loud-skip lane — never fabricate a float bit-pattern).
3506    #[test]
3507    fn test_decode_captures_i64_global_initializer_649() {
3508        let wat = r#"
3509            (module
3510                (global $g (mut i64) (i64.const 0x123456789ABCDEF0))
3511                (global $n (mut i64) (i64.const -1))
3512                (global $f (mut f64) (f64.const 1.5))
3513                (global $h (mut f32) (f32.const 2.5))
3514                (func (export "f") (result i32) i32.const 0)
3515            )
3516        "#;
3517        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3518        let module = decode_wasm_module(&wasm).expect("Failed to decode");
3519
3520        assert_eq!(module.globals.len(), 4);
3521        assert_eq!(
3522            module.globals[0].init,
3523            Some(GlobalInit::I64(0x123456789ABCDEF0u64 as i64)),
3524            "nonzero i64 init captured with both words"
3525        );
3526        assert_eq!(module.globals[1].init, Some(GlobalInit::I64(-1)));
3527        assert_eq!(
3528            module.globals[2].init, None,
3529            "f64 init is NOT captured (GI-FPU-001 loud-skip lane)"
3530        );
3531        assert_eq!(
3532            module.globals[3].init, None,
3533            "f32 init is NOT captured (GI-FPU-001 loud-skip lane)"
3534        );
3535    }
3536
3537    /// #509: the decoder records `(param_count, result_count)` for every
3538    /// `Block`/`Loop`/`If`, ordinal-keyed in op order, covering all three
3539    /// blocktype encodings: `Empty → (0,0)`, `ValType → (0,1)`, and
3540    /// `FuncType(i) →` counts from the type section (here a multi-result
3541    /// block, which wat encodes as a functype blocktype).
3542    #[test]
3543    fn test_decode_records_block_arity_side_table_509() {
3544        let wat = r#"
3545            (module
3546                (func (export "f") (param i32) (result i32)
3547                    (block (result i32)
3548                        (block (nop))
3549                        (local.get 0)
3550                        (if (result i32)
3551                            (then (i32.const 1))
3552                            (else (i32.const 2)))))
3553                (func (export "g") (result i32)
3554                    (block (result i32 i32)
3555                        (i32.const 1) (i32.const 2))
3556                    i32.add)
3557                (func (export "h") (param i32) (result i32)
3558                    (local.get 0)
3559                    (loop (param i32) (result i32))))
3560        "#;
3561        let wasm = wat::parse_str(wat).expect("parse WAT");
3562
3563        // Both decode entry points must produce the same side-table.
3564        for functions in [
3565            decode_wasm_functions(&wasm).expect("decode"),
3566            decode_wasm_module(&wasm).expect("decode").functions,
3567        ] {
3568            // f: Block(result i32), Block(void), If(result i32) — in op order.
3569            assert_eq!(
3570                functions[0].block_arity,
3571                vec![(0, 1), (0, 0), (0, 1)],
3572                "f: ValType/Empty/ValType blocktypes"
3573            );
3574            // g: one multi-result block via a FuncType blocktype.
3575            assert_eq!(
3576                functions[1].block_arity,
3577                vec![(0, 2)],
3578                "g: functype blocktype result count from the type section"
3579            );
3580            // h: a parameterized loop — the input arity is what a br to the
3581            // header would carry (the #509 loud-decline discriminator).
3582            assert_eq!(
3583                functions[2].block_arity,
3584                vec![(1, 1)],
3585                "h: loop params captured"
3586            );
3587        }
3588    }
3589
3590    /// #642: the decoder captures table 0's compile-time size, per-segment
3591    /// element shapes and per-function type indices, and the closed-world
3592    /// verdict VERIFIES a fully-covered homogeneous table.
3593    #[test]
3594    fn test_call_indirect_guards_closed_world_verified_642() {
3595        // The #642 repro shape: 3-entry table, fully covered, one signature.
3596        let wat = r#"
3597            (module
3598                (type $bin (func (param i32 i32) (result i32)))
3599                (table 3 funcref)
3600                (elem (i32.const 0) $add $sub $mul)
3601                (func $add (param i32 i32) (result i32)
3602                    (i32.add (local.get 0) (local.get 1)))
3603                (func $sub (param i32 i32) (result i32)
3604                    (i32.sub (local.get 0) (local.get 1)))
3605                (func $mul (param i32 i32) (result i32)
3606                    (i32.mul (local.get 0) (local.get 1)))
3607                (func (export "f") (param i32 i32) (result i32)
3608                    (call_indirect (type $bin)
3609                        (local.get 0) (i32.const 10) (local.get 1)))
3610            )
3611        "#;
3612        let wasm = wat::parse_str(wat).expect("parse");
3613        let module = decode_wasm_module(&wasm).expect("decode");
3614
3615        assert_eq!(module.table_size, Some(3), "table section min size");
3616        assert_eq!(module.table_sizes, vec![Some(3)], "#650 per-table sizes");
3617        assert_eq!(
3618            module.elem_segments,
3619            vec![ElemSegmentInfo {
3620                table_index: 0,
3621                offset: Some(0),
3622                funcs: Some(vec![0, 1, 2]),
3623            }]
3624        );
3625        // 2 type-section entries ($bin + the export's (i32 i32)->i32 dedups
3626        // to one in practice, but don't assume — just check func 0..2 share
3627        // a signature with type 0).
3628        assert_eq!(module.func_type_indices.len(), 4);
3629
3630        let guards = module.call_indirect_guards();
3631        assert_eq!(guards.tables.len(), 1);
3632        assert_eq!(guards.tables[0].table_size, Some(3));
3633        assert_eq!(
3634            guards.tables[0].base_byte_offset,
3635            Some(0),
3636            "#650: a single-table module keeps table 0 at R11 offset 0 by construction"
3637        );
3638        // Type index 0 ($bin) must be VERIFIED: every table entry has its
3639        // exact signature.
3640        assert_eq!(
3641            guards.tables[0].type_reject.first(),
3642            Some(&None),
3643            "closed-world type check must verify the homogeneous table: {:?}",
3644            guards.tables[0].type_reject
3645        );
3646        assert!(
3647            !guards.tables[0].has_null_slots,
3648            "#664: a fully-initialized table must NOT request the runtime \
3649             null check (dispatch bytes stay identical by construction)"
3650        );
3651    }
3652
3653    /// #642: a heterogeneous table (an entry whose signature differs from the
3654    /// expected type) must REJECT that expected type — the raw code-pointer
3655    /// table cannot be runtime-type-checked, so the lowering has to decline.
3656    #[test]
3657    fn test_call_indirect_guards_heterogeneous_table_rejects_642() {
3658        let wat = r#"
3659            (module
3660                (type $bin (func (param i32 i32) (result i32)))
3661                (type $un (func (param i32) (result i32)))
3662                (table 2 funcref)
3663                (elem (i32.const 0) $add $neg)
3664                (func $add (type $bin)
3665                    (i32.add (local.get 0) (local.get 1)))
3666                (func $neg (type $un)
3667                    (i32.sub (i32.const 0) (local.get 0)))
3668                (func (export "f") (param i32 i32) (result i32)
3669                    (call_indirect (type $bin)
3670                        (local.get 0) (i32.const 10) (local.get 1)))
3671            )
3672        "#;
3673        let wasm = wat::parse_str(wat).expect("parse");
3674        let module = decode_wasm_module(&wasm).expect("decode");
3675        let guards = module.call_indirect_guards();
3676        assert_eq!(guards.tables[0].table_size, Some(2));
3677        // BOTH expected types must be rejected: the table holds one function
3678        // of each signature, so neither type's closed world holds.
3679        assert!(
3680            guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3681            "heterogeneous table must reject every expected type: {:?}",
3682            guards.tables[0].type_reject
3683        );
3684        // #676: ... but the image is statically known, so the mismatch trap
3685        // is dischargeable at RUNTIME via the type-id sidecar.
3686        assert!(
3687            guards.tables[0].runtime_type_check,
3688            "heterogeneous-but-known table must offer the runtime check (#676)"
3689        );
3690        assert_eq!(
3691            guards.type_ids_byte_offset,
3692            Some(8),
3693            "sidecar sits after the 2-slot pointer region"
3694        );
3695        assert_eq!(
3696            guards.type_ids_image,
3697            vec![1, 2],
3698            "slot 0 = $bin (class 1), slot 1 = $un (class 2)"
3699        );
3700        assert_eq!(guards.type_class_ids, vec![1, 2]);
3701    }
3702
3703    /// #664 (relaxes the #642 all-reject): an uninitialized table slot (elem
3704    /// covers less than the declared size) is a null funcref — calling it
3705    /// must trap, which is now discharged at RUNTIME (null check on the
3706    /// zero-linked pointer), so the closed-world verdict verifies the
3707    /// INITIALIZED slots and sets `has_null_slots` for the lowering.
3708    #[test]
3709    fn test_call_indirect_guards_null_slot_verifies_with_flag_664() {
3710        let wat = r#"
3711            (module
3712                (type $s (func (result i32)))
3713                (table 3 funcref)
3714                (elem (i32.const 0) $f0 $f1)
3715                (func $f0 (result i32) (i32.const 10))
3716                (func $f1 (result i32) (i32.const 11))
3717                (func (export "run") (param i32) (result i32)
3718                    (call_indirect (type $s) (local.get 0)))
3719            )
3720        "#;
3721        let wasm = wat::parse_str(wat).expect("parse");
3722        let module = decode_wasm_module(&wasm).expect("decode");
3723        let guards = module.call_indirect_guards();
3724        assert_eq!(guards.tables[0].table_size, Some(3));
3725        assert_eq!(
3726            guards.tables[0].type_reject.first(),
3727            Some(&None),
3728            "initialized slots are homogeneous in $s — the verdict must \
3729             verify despite the null slot (#664): {:?}",
3730            guards.tables[0].type_reject
3731        );
3732        assert!(
3733            guards.tables[0].has_null_slots,
3734            "slot 2 is uninitialized — the lowering must emit the runtime \
3735             null check (#664)"
3736        );
3737    }
3738
3739    /// #664: the falcon shape — a SPARSE table (only slots 1 and 3 of 4
3740    /// initialized, by two separate segments) verifies with the null flag;
3741    /// a sparse table whose INITIALIZED slots are heterogeneous still
3742    /// rejects (the runtime null check cannot discharge a TYPE mismatch).
3743    #[test]
3744    fn test_call_indirect_guards_sparse_table_664() {
3745        let wat = r#"
3746            (module
3747                (type $t (func (param i32) (result i32)))
3748                (table 4 4 funcref)
3749                (func $f1 (type $t) (i32.add (local.get 0) (i32.const 100)))
3750                (func $f3 (type $t) (i32.sub (i32.const 1000) (local.get 0)))
3751                (elem (i32.const 1) $f1)
3752                (elem (i32.const 3) $f3)
3753                (func (export "via") (param i32 i32) (result i32)
3754                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3755            )
3756        "#;
3757        let wasm = wat::parse_str(wat).expect("parse");
3758        let module = decode_wasm_module(&wasm).expect("decode");
3759        let guards = module.call_indirect_guards();
3760        assert_eq!(guards.tables[0].table_size, Some(4));
3761        assert_eq!(
3762            guards.tables[0].type_reject.first(),
3763            Some(&None),
3764            "slots 1,3 are homogeneous in $t — verified: {:?}",
3765            guards.tables[0].type_reject
3766        );
3767        assert!(guards.tables[0].has_null_slots, "slots 0,2 are null");
3768
3769        // Heterogeneous INITIALIZED slots in a sparse table: still rejected.
3770        let wat = r#"
3771            (module
3772                (type $t (func (param i32) (result i32)))
3773                (type $u (func (param i32 i32) (result i32)))
3774                (table 4 4 funcref)
3775                (func $f1 (type $t) (local.get 0))
3776                (func $f3 (type $u) (i32.add (local.get 0) (local.get 1)))
3777                (elem (i32.const 1) $f1)
3778                (elem (i32.const 3) $f3)
3779                (func (export "via") (param i32 i32) (result i32)
3780                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3781            )
3782        "#;
3783        let wasm = wat::parse_str(wat).expect("parse");
3784        let module = decode_wasm_module(&wasm).expect("decode");
3785        let guards = module.call_indirect_guards();
3786        assert!(
3787            guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3788            "a heterogeneous sparse table must still reject every type: {:?}",
3789            guards.tables[0].type_reject
3790        );
3791        // #676: the sparse-heterogeneous case is now dischargeable at
3792        // runtime too — null slots take the reserved class id 0, so ONE
3793        // sidecar compare covers both the type mismatch and the null trap.
3794        assert!(guards.tables[0].runtime_type_check, "#676 runtime check");
3795        assert_eq!(guards.type_ids_byte_offset, Some(16), "4 pointer slots");
3796        assert_eq!(
3797            guards.type_ids_image,
3798            vec![0, 1, 0, 2],
3799            "nulls at 0/2 carry the reserved id 0; $t slot 1 = class 1, \
3800             $u slot 3 = class 2"
3801        );
3802    }
3803
3804    /// #676: the heterogeneous type-id sidecar — structural duplicate types
3805    /// share one class id (the meld 31-decls/25-distinct shape), null slots
3806    /// take the reserved id 0, and the sidecar base is the total pointer
3807    /// region size. A module with NO heterogeneous table gets NO sidecar
3808    /// (empty image, `None` offset) — homogeneous modules stay untouched.
3809    #[test]
3810    fn test_call_indirect_guards_heterogeneous_sidecar_676() {
3811        let wat = r#"
3812            (module
3813                (type $bin (func (param i32 i32) (result i32)))
3814                (type $un (func (param i32) (result i32)))
3815                (type $bin2 (func (param i32 i32) (result i32)))
3816                (table 5 5 funcref)
3817                (func $add (type $bin) (i32.add (local.get 0) (local.get 1)))
3818                (func $neg (type $un) (i32.sub (i32.const 0) (local.get 0)))
3819                (func $sub (type $bin2) (i32.sub (local.get 0) (local.get 1)))
3820                (elem (i32.const 0) func $add $neg $sub)
3821                (func (export "via2") (param i32 i32) (result i32)
3822                    (call_indirect (type $bin)
3823                        (local.get 0) (i32.const 3) (local.get 1)))
3824                (func (export "via1") (param i32 i32) (result i32)
3825                    (call_indirect (type $un) (local.get 0) (local.get 1)))
3826            )
3827        "#;
3828        let wasm = wat::parse_str(wat).expect("parse");
3829        let module = decode_wasm_module(&wasm).expect("decode");
3830        let guards = module.call_indirect_guards();
3831        assert!(guards.tables[0].runtime_type_check);
3832        assert_eq!(
3833            guards.type_class_ids,
3834            vec![1, 2, 1],
3835            "$bin2 is a structural duplicate of $bin — one class id (#676)"
3836        );
3837        assert_eq!(
3838            guards.type_ids_image,
3839            vec![1, 2, 1, 0, 0],
3840            "slots: $add(bin)=1, $neg(un)=2, $sub(bin2 ≡ bin)=1, null, null"
3841        );
3842        assert_eq!(
3843            guards.type_ids_byte_offset,
3844            Some(20),
3845            "sidecar starts after the 5 pointer words"
3846        );
3847
3848        // Homogeneous module → NO sidecar, no runtime check anywhere.
3849        let wat = r#"
3850            (module
3851                (type $t (func (param i32) (result i32)))
3852                (table 2 2 funcref)
3853                (func $f0 (type $t) (local.get 0))
3854                (func $f1 (type $t) (i32.const 7))
3855                (elem (i32.const 0) func $f0 $f1)
3856                (func (export "via") (param i32 i32) (result i32)
3857                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3858            )
3859        "#;
3860        let wasm = wat::parse_str(wat).expect("parse");
3861        let module = decode_wasm_module(&wasm).expect("decode");
3862        let guards = module.call_indirect_guards();
3863        assert!(!guards.tables[0].runtime_type_check);
3864        assert_eq!(guards.type_ids_byte_offset, None, "no heterogeneous table");
3865        assert!(guards.type_ids_image.is_empty());
3866        assert!(guards.type_class_ids.is_empty());
3867    }
3868
3869    /// #642: no table at all → no compile-time bound → table_size None and
3870    /// every type rejected (the lowering declines).
3871    #[test]
3872    fn test_call_indirect_guards_no_table_642() {
3873        let wat = r#"
3874            (module
3875                (func (export "f") (param i32) (result i32) (local.get 0))
3876            )
3877        "#;
3878        let wasm = wat::parse_str(wat).expect("parse");
3879        let module = decode_wasm_module(&wasm).expect("decode");
3880        assert_eq!(module.table_size, None);
3881        assert!(module.table_sizes.is_empty(), "#650: no tables declared");
3882        let guards = module.call_indirect_guards();
3883        assert!(
3884            guards.tables.is_empty(),
3885            "no table → no guard entry → every call_indirect declines"
3886        );
3887    }
3888
3889    /// #642: duplicate-but-structurally-identical types stay interchangeable —
3890    /// the closed-world check compares SIGNATURES, not type indices.
3891    #[test]
3892    fn test_call_indirect_guards_duplicate_types_verified_642() {
3893        let wat = r#"
3894            (module
3895                (type $a (func (result i32)))
3896                (type $b (func (result i32)))
3897                (table 1 funcref)
3898                (elem (i32.const 0) $f)
3899                (func $f (type $a) (i32.const 7))
3900                (func (export "run") (param i32) (result i32)
3901                    (call_indirect (type $b) (local.get 0)))
3902            )
3903        "#;
3904        let wasm = wat::parse_str(wat).expect("parse");
3905        let module = decode_wasm_module(&wasm).expect("decode");
3906        let guards = module.call_indirect_guards();
3907        // $f has type $a; the call expects $b — structurally identical, so
3908        // BOTH type indices must verify. (A third type — the export's
3909        // (i32)->i32 — is correctly rejected: different signature.)
3910        assert_eq!(
3911            &guards.tables[0].type_reject[0..2],
3912            &[None, None],
3913            "structural signature comparison must accept duplicate types: {:?}",
3914            guards.tables[0].type_reject
3915        );
3916        assert!(
3917            guards.tables[0].type_reject[2].is_some(),
3918            "the structurally-different third type must still be rejected"
3919        );
3920    }
3921
3922    /// #650: TWO tables become a contiguous R11 region — table 0 at offset 0
3923    /// (byte-identical single-table degeneration), table 1 at
3924    /// `size(table 0) * 4`. Each table gets its OWN size, base offset, and
3925    /// per-type closed-world verdicts (segments only poison the table they
3926    /// target).
3927    #[test]
3928    fn test_call_indirect_guards_multi_table_650() {
3929        // The #650 repro shape: overlapping indices, distinct functions —
3930        // table0[1] != table1[1] (the aliasing canary).
3931        let wat = r#"
3932            (module
3933                (type $t (func (param i32) (result i32)))
3934                (type $u (func (param i32 i32) (result i32)))
3935                (table $t0 3 3 funcref)
3936                (table $t1 2 2 funcref)
3937                (func $a0 (type $t) (i32.add (local.get 0) (i32.const 100)))
3938                (func $a1 (type $t) (i32.add (local.get 0) (i32.const 200)))
3939                (func $a2 (type $t) (i32.add (local.get 0) (i32.const 300)))
3940                (func $b0 (type $u) (i32.add (local.get 0) (local.get 1)))
3941                (func $b1 (type $u) (i32.sub (local.get 0) (local.get 1)))
3942                (elem (table $t0) (i32.const 0) func $a0 $a1 $a2)
3943                (elem (table $t1) (i32.const 0) func $b0 $b1)
3944                (func (export "f") (param i32 i32) (result i32)
3945                    (call_indirect $t1 (type $u)
3946                        (local.get 0) (i32.const 10) (local.get 1)))
3947            )
3948        "#;
3949        let wasm = wat::parse_str(wat).expect("parse");
3950        let module = decode_wasm_module(&wasm).expect("decode");
3951        assert_eq!(module.table_sizes, vec![Some(3), Some(2)]);
3952        assert_eq!(module.table_size, Some(3), "compat accessor = table 0");
3953        assert_eq!(
3954            module.elem_segments[0].table_index, 0,
3955            "segment 0 targets table 0"
3956        );
3957        assert_eq!(
3958            module.elem_segments[1],
3959            ElemSegmentInfo {
3960                table_index: 1,
3961                offset: Some(0),
3962                funcs: Some(vec![3, 4]),
3963            },
3964            "segment 1 is statically attributed to table 1 (#650)"
3965        );
3966
3967        let guards = module.call_indirect_guards();
3968        assert_eq!(guards.tables.len(), 2);
3969        assert_eq!(guards.tables[0].table_size, Some(3));
3970        assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3971        assert_eq!(guards.tables[1].table_size, Some(2));
3972        assert_eq!(
3973            guards.tables[1].base_byte_offset,
3974            Some(12),
3975            "table 1 base = size(table 0) * 4 within the contiguous R11 region"
3976        );
3977        // Table 0 is homogeneous in $t (type 0); table 1 in $u (type 1) —
3978        // each verifies ITS type and rejects the other's.
3979        assert_eq!(guards.tables[0].type_reject[0], None, "table 0 vs $t");
3980        assert!(guards.tables[0].type_reject[1].is_some(), "table 0 vs $u");
3981        assert!(guards.tables[1].type_reject[0].is_some(), "table 1 vs $t");
3982        assert_eq!(guards.tables[1].type_reject[1], None, "table 1 vs $u");
3983    }
3984
3985    /// #650: an unknown-size table (growable import) declines ITSELF and
3986    /// makes every LATER table's base offset non-constant — but a table
3987    /// BEFORE it is unaffected.
3988    #[test]
3989    fn test_call_indirect_guards_unknown_size_poisons_later_bases_650() {
3990        let wat = r#"
3991            (module
3992                (type $t (func (result i32)))
3993                (import "env" "tbl" (table 4 funcref))
3994                (table $d 1 1 funcref)
3995                (func $f (type $t) (i32.const 7))
3996                (elem (table $d) (i32.const 0) func $f)
3997                (func (export "run") (param i32) (result i32)
3998                    (call_indirect $d (type $t) (local.get 0)))
3999            )
4000        "#;
4001        let wasm = wat::parse_str(wat).expect("parse");
4002        let module = decode_wasm_module(&wasm).expect("decode");
4003        assert_eq!(
4004            module.table_sizes,
4005            vec![None, Some(1)],
4006            "growable import (no max) has no sound compile-time size"
4007        );
4008        let guards = module.call_indirect_guards();
4009        assert_eq!(guards.tables[0].base_byte_offset, Some(0));
4010        assert!(
4011            guards.tables[0].type_reject.iter().all(|r| r.is_some()),
4012            "unknown-size table rejects every type"
4013        );
4014        assert_eq!(
4015            guards.tables[1].base_byte_offset, None,
4016            "a later table's base is not a compile-time constant when a \
4017             preceding table's size is unknown (#650)"
4018        );
4019        assert_eq!(guards.tables[1].table_size, Some(1));
4020    }
4021}