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        // #782a: the nontrapping trunc_sat family (0xFC-prefixed,
2262        // saturating-float-to-int proposal) — TOTAL ops (§4.3.2: NaN → 0,
2263        // out-of-range saturates to INT_MIN/INT_MAX, no traps). Un-dropped so
2264        // the selectors see them: ARM32 lowers the i32-target forms as a bare
2265        // VCVT (round-toward-zero VCVT already saturates and gives 0 for NaN —
2266        // exactly trunc_sat, the very behavior the #709 guard exists to keep
2267        // away from the TRAPPING forms); aarch64 lowers all eight via
2268        // FCVTZS/FCVTZU. The i64-target forms LOUD-decline on 32-bit ARM (no
2269        // i64 register-pair conversion path) and RV32 (no floats at all).
2270        I32TruncSatF32S => Some(WasmOp::I32TruncSatF32S),
2271        I32TruncSatF32U => Some(WasmOp::I32TruncSatF32U),
2272        I64TruncSatF32S => Some(WasmOp::I64TruncSatF32S),
2273        I64TruncSatF32U => Some(WasmOp::I64TruncSatF32U),
2274
2275        // === Scalar f64 (GI-FPU-002 phase 2, #369) ===
2276        // Un-dropped for the DOUBLE-precision FPU target (cortex-m7dp D0..D15);
2277        // the capability gate lives in `select_with_stack`'s preamble — any
2278        // f64 op on m4f/m7 (single-precision) or m0/m3/r5 (no FPU) still
2279        // honest-rejects its function. Exactly this set is lowered by
2280        // `try_lower_f64` + the `F64Load`/`F64Store` selector arms; the rest
2281        // of the f64 surface (min/max/copysign/rounding/i64<->f64/…) stays at
2282        // `_ => None` (loud-skip) until a later increment wires it.
2283        F64Const { value } => Some(WasmOp::F64Const(f64::from_bits(value.bits()))),
2284        F64PromoteF32 => Some(WasmOp::F64PromoteF32),
2285        F64Add => Some(WasmOp::F64Add),
2286        F64Sub => Some(WasmOp::F64Sub),
2287        F64Mul => Some(WasmOp::F64Mul),
2288        F64Div => Some(WasmOp::F64Div),
2289        F64Abs => Some(WasmOp::F64Abs),
2290        F64Neg => Some(WasmOp::F64Neg),
2291        F64Sqrt => Some(WasmOp::F64Sqrt),
2292        F64Eq => Some(WasmOp::F64Eq),
2293        F64Ne => Some(WasmOp::F64Ne),
2294        F64Lt => Some(WasmOp::F64Lt),
2295        F64Le => Some(WasmOp::F64Le),
2296        F64Gt => Some(WasmOp::F64Gt),
2297        F64Ge => Some(WasmOp::F64Ge),
2298        F64Load { memarg } => Some(WasmOp::F64Load {
2299            offset: memarg.offset as u32,
2300            align: memarg.align as u32,
2301        }),
2302        F64Store { memarg } => Some(WasmOp::F64Store {
2303            offset: memarg.offset as u32,
2304            align: memarg.align as u32,
2305        }),
2306        // GI-FPU-002 phase 3 (#369): the f64 op tail — rounding via single
2307        // VRINT{P,M,Z,N}.F64 (FPv5), min/max via VMINNM/VMAXNM + the
2308        // NaN-propagating fix-up, copysign via the VABS/conditional-VNEG
2309        // splice, f32.demote_f64 / i32<->f64 conversions via VCVT
2310        // (i32.trunc_f64_* carries the #709 trap-on-out-of-range domain
2311        // guard). Still m7dp-only (the selector preamble honest-rejects any
2312        // f64 op elsewhere). Remaining dropped f64 surface: the i64<->f64
2313        // conversions and reinterprets (need lowered i64 pair plumbing).
2314        F64Ceil => Some(WasmOp::F64Ceil),
2315        F64Floor => Some(WasmOp::F64Floor),
2316        F64Trunc => Some(WasmOp::F64Trunc),
2317        F64Nearest => Some(WasmOp::F64Nearest),
2318        F64Min => Some(WasmOp::F64Min),
2319        F64Max => Some(WasmOp::F64Max),
2320        F64Copysign => Some(WasmOp::F64Copysign),
2321        F32DemoteF64 => Some(WasmOp::F32DemoteF64),
2322        F64ConvertI32S => Some(WasmOp::F64ConvertI32S),
2323        F64ConvertI32U => Some(WasmOp::F64ConvertI32U),
2324        I32TruncF64S => Some(WasmOp::I32TruncF64S),
2325        I32TruncF64U => Some(WasmOp::I32TruncF64U),
2326        // #782a: f64-source trunc_sat twins (see the f32 group above). falcon
2327        // v1.123 carries 7× i32.trunc_sat_f64_s — the m7dp double-precision
2328        // VCVT twins lower the i32-target forms; i64 targets loud-decline.
2329        I32TruncSatF64S => Some(WasmOp::I32TruncSatF64S),
2330        I32TruncSatF64U => Some(WasmOp::I32TruncSatF64U),
2331        I64TruncSatF64S => Some(WasmOp::I64TruncSatF64S),
2332        I64TruncSatF64U => Some(WasmOp::I64TruncSatF64U),
2333
2334        // f32x4
2335        F32x4Add => Some(WasmOp::F32x4Add),
2336        F32x4Sub => Some(WasmOp::F32x4Sub),
2337        F32x4Mul => Some(WasmOp::F32x4Mul),
2338        F32x4Div => Some(WasmOp::F32x4Div),
2339        F32x4Abs => Some(WasmOp::F32x4Abs),
2340        F32x4Neg => Some(WasmOp::F32x4Neg),
2341        F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
2342        F32x4Eq => Some(WasmOp::F32x4Eq),
2343        F32x4Ne => Some(WasmOp::F32x4Ne),
2344        F32x4Lt => Some(WasmOp::F32x4Lt),
2345        F32x4Le => Some(WasmOp::F32x4Le),
2346        F32x4Gt => Some(WasmOp::F32x4Gt),
2347        F32x4Ge => Some(WasmOp::F32x4Ge),
2348        F32x4Splat => Some(WasmOp::F32x4Splat),
2349        F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
2350        F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
2351
2352        // Other operators not yet supported
2353        _ => None,
2354    }
2355}
2356
2357#[cfg(test)]
2358mod tests {
2359    use super::*;
2360
2361    #[test]
2362    fn test_decode_simple_add() {
2363        let wat = r#"
2364            (module
2365                (func (export "add") (param i32 i32) (result i32)
2366                    local.get 0
2367                    local.get 1
2368                    i32.add
2369                )
2370            )
2371        "#;
2372
2373        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2374        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2375
2376        assert_eq!(functions.len(), 1);
2377        assert_eq!(functions[0].index, 0);
2378        assert_eq!(functions[0].export_name, Some("add".to_string()));
2379        assert_eq!(
2380            functions[0].ops,
2381            vec![
2382                WasmOp::LocalGet(0),
2383                WasmOp::LocalGet(1),
2384                WasmOp::I32Add,
2385                WasmOp::End
2386            ]
2387        );
2388    }
2389
2390    /// #782a: the 0xFC-prefixed trunc_sat family must DECODE (it was
2391    /// previously unmapped → the whole function loud-skipped as
2392    /// "unsupported operator", the falcon #782 `ts32`/`ts64` class). All
2393    /// eight forms must surface as their `WasmOp` variants so the selectors
2394    /// can lower (ARM32 i32-targets, aarch64 all) or LOUD-decline (ARM32
2395    /// i64-targets, RV32) per backend.
2396    #[test]
2397    fn test_decode_trunc_sat_family() {
2398        let wat = r#"
2399            (module
2400                (func (export "ts") (param f32 f64) (result i32)
2401                    (i32.trunc_sat_f32_s (local.get 0))
2402                    (i32.trunc_sat_f32_u (local.get 0))
2403                    i32.add
2404                    (i32.trunc_sat_f64_s (local.get 1))
2405                    i32.add
2406                    (i32.trunc_sat_f64_u (local.get 1))
2407                    i32.add
2408                    (i32.wrap_i64 (i64.trunc_sat_f32_s (local.get 0)))
2409                    i32.add
2410                    (i32.wrap_i64 (i64.trunc_sat_f32_u (local.get 0)))
2411                    i32.add
2412                    (i32.wrap_i64 (i64.trunc_sat_f64_s (local.get 1)))
2413                    i32.add
2414                    (i32.wrap_i64 (i64.trunc_sat_f64_u (local.get 1)))
2415                    i32.add
2416                )
2417            )
2418        "#;
2419        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2420        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2421        assert_eq!(functions.len(), 1);
2422        let f = &functions[0];
2423        assert!(
2424            f.unsupported.is_none(),
2425            "trunc_sat must decode, not loud-skip: {:?}",
2426            f.unsupported
2427        );
2428        for want in [
2429            WasmOp::I32TruncSatF32S,
2430            WasmOp::I32TruncSatF32U,
2431            WasmOp::I32TruncSatF64S,
2432            WasmOp::I32TruncSatF64U,
2433            WasmOp::I64TruncSatF32S,
2434            WasmOp::I64TruncSatF32U,
2435            WasmOp::I64TruncSatF64S,
2436            WasmOp::I64TruncSatF64U,
2437        ] {
2438            assert!(
2439                f.ops.contains(&want),
2440                "decoded ops must contain {want:?}: {:?}",
2441                f.ops
2442            );
2443        }
2444    }
2445
2446    /// #204 regression: `i64.extend_i32_u`, `i64.extend_i32_s` and
2447    /// `i32.wrap_i64` must DECODE (they were previously unmapped → silently
2448    /// dropped by `convert_operator`, leaving an i32 value as a 64-bit operand
2449    /// with a garbage high half — the root cause of gale's miscompiled
2450    /// `(new_count << 32)` pack). The decoder must surface all three.
2451    #[test]
2452    fn test_decode_i64_i32_width_conversions() {
2453        let wat = r#"
2454            (module
2455                (func (export "conv") (param i32 i64) (result i32)
2456                    local.get 0
2457                    i64.extend_i32_u
2458                    local.get 0
2459                    i64.extend_i32_s
2460                    i64.add
2461                    local.get 1
2462                    i64.add
2463                    i32.wrap_i64
2464                )
2465            )
2466        "#;
2467        let wasm = wat::parse_str(wat).expect("parse");
2468        let functions = decode_wasm_functions(&wasm).expect("decode");
2469        let ops = &functions[0].ops;
2470        assert!(
2471            ops.contains(&WasmOp::I64ExtendI32U),
2472            "i64.extend_i32_u must decode (not be dropped): {ops:?}"
2473        );
2474        assert!(
2475            ops.contains(&WasmOp::I64ExtendI32S),
2476            "i64.extend_i32_s must decode (not be dropped): {ops:?}"
2477        );
2478        assert!(
2479            ops.contains(&WasmOp::I32WrapI64),
2480            "i32.wrap_i64 must decode (not be dropped): {ops:?}"
2481        );
2482    }
2483
2484    /// #204 WAKE-path regression: `br_table` must DECODE (it was unmapped in
2485    /// `convert_operator` → silently dropped, so the selector emitted no index
2486    /// dispatch and every `br_table` fell through to target 0 — gale's binary
2487    /// semaphore never took its WAKE branch). Targets + default are preserved.
2488    #[test]
2489    fn test_decode_br_table() {
2490        let wat = r#"
2491            (module
2492                (func (export "bt") (param i32) (result i32)
2493                    (block (block (block
2494                        local.get 0
2495                        br_table 2 0 1 2)
2496                      i32.const 30 return)
2497                      i32.const 20 return)
2498                    i32.const 10))
2499        "#;
2500        let wasm = wat::parse_str(wat).expect("parse");
2501        let functions = decode_wasm_functions(&wasm).expect("decode");
2502        let bt = functions[0]
2503            .ops
2504            .iter()
2505            .find_map(|o| match o {
2506                WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
2507                _ => None,
2508            })
2509            .expect("br_table must decode (not be dropped)");
2510        assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
2511        assert_eq!(bt.1, 2, "br_table default preserved");
2512    }
2513
2514    #[test]
2515    fn test_decode_arithmetic() {
2516        let wat = r#"
2517            (module
2518                (func (export "calc") (result i32)
2519                    i32.const 5
2520                    i32.const 3
2521                    i32.mul
2522                    i32.const 2
2523                    i32.add
2524                )
2525            )
2526        "#;
2527
2528        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2529        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2530
2531        assert_eq!(functions.len(), 1);
2532        assert_eq!(functions[0].export_name, Some("calc".to_string()));
2533        assert_eq!(
2534            functions[0].ops,
2535            vec![
2536                WasmOp::I32Const(5),
2537                WasmOp::I32Const(3),
2538                WasmOp::I32Mul,
2539                WasmOp::I32Const(2),
2540                WasmOp::I32Add,
2541                WasmOp::End,
2542            ]
2543        );
2544    }
2545
2546    #[test]
2547    fn test_decode_multi_function_module() {
2548        let wat = r#"
2549            (module
2550                (func $helper)
2551                (func (export "add") (param i32 i32) (result i32)
2552                    local.get 0
2553                    local.get 1
2554                    i32.add
2555                )
2556                (func (export "sub") (param i32 i32) (result i32)
2557                    local.get 0
2558                    local.get 1
2559                    i32.sub
2560                )
2561            )
2562        "#;
2563
2564        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2565        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2566
2567        assert_eq!(functions.len(), 3);
2568        assert_eq!(functions[0].index, 0);
2569        assert_eq!(functions[0].export_name, None);
2570        assert_eq!(functions[1].index, 1);
2571        assert_eq!(functions[1].export_name, Some("add".to_string()));
2572        assert_eq!(functions[2].index, 2);
2573        assert_eq!(functions[2].export_name, Some("sub".to_string()));
2574    }
2575
2576    #[test]
2577    fn test_decode_module_with_imports() {
2578        let wat = r#"
2579            (module
2580                (import "env" "log" (func $log (param i32)))
2581                (import "env" "memory" (memory 1))
2582                (func (export "run") (param i32)
2583                    local.get 0
2584                    call 0
2585                )
2586            )
2587        "#;
2588
2589        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2590        let module = decode_wasm_module(&wasm).expect("Failed to decode");
2591
2592        // Should have 2 imports (1 func, 1 memory)
2593        assert_eq!(module.imports.len(), 2);
2594        assert_eq!(module.num_imported_funcs, 1);
2595
2596        // First import is the function
2597        assert_eq!(module.imports[0].module, "env");
2598        assert_eq!(module.imports[0].name, "log");
2599        assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
2600
2601        // Second import is memory
2602        assert_eq!(module.imports[1].module, "env");
2603        assert_eq!(module.imports[1].name, "memory");
2604        assert_eq!(module.imports[1].kind, ImportKind::Memory);
2605
2606        // Should have 1 local function (index 1, because import is index 0)
2607        assert_eq!(module.functions.len(), 1);
2608        assert_eq!(module.functions[0].index, 1);
2609        assert_eq!(module.functions[0].export_name, Some("run".to_string()));
2610    }
2611
2612    #[test]
2613    fn test_find_function_by_export_name() {
2614        let wat = r#"
2615            (module
2616                (func $helper)
2617                (func (export "add") (param i32 i32) (result i32)
2618                    local.get 0
2619                    local.get 1
2620                    i32.add
2621                )
2622            )
2623        "#;
2624
2625        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2626        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2627
2628        let add_func = functions
2629            .iter()
2630            .find(|f| f.export_name.as_deref() == Some("add"))
2631            .expect("Should find 'add' function");
2632
2633        assert_eq!(add_func.index, 1);
2634        assert!(add_func.ops.contains(&WasmOp::I32Add));
2635    }
2636
2637    #[test]
2638    fn test_decode_subword_loads() {
2639        let wat = r#"
2640            (module
2641                (memory 1)
2642                (func (export "test") (param i32) (result i32)
2643                    local.get 0
2644                    i32.load8_u
2645                )
2646            )
2647        "#;
2648
2649        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2650        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2651
2652        assert_eq!(functions.len(), 1);
2653        assert!(functions[0].ops.contains(&WasmOp::I32Load8U {
2654            offset: 0,
2655            align: 0,
2656        }));
2657    }
2658
2659    #[test]
2660    fn test_decode_subword_stores() {
2661        let wat = r#"
2662            (module
2663                (memory 1)
2664                (func (export "test") (param i32 i32)
2665                    local.get 0
2666                    local.get 1
2667                    i32.store8
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::I32Store8 {
2677            offset: 0,
2678            align: 0,
2679        }));
2680    }
2681
2682    #[test]
2683    fn test_decode_memory_size_grow() {
2684        let wat = r#"
2685            (module
2686                (memory 1)
2687                (func (export "test") (result i32)
2688                    memory.size
2689                )
2690            )
2691        "#;
2692
2693        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2694        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2695
2696        assert_eq!(functions.len(), 1);
2697        assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
2698    }
2699
2700    #[test]
2701    fn test_decode_memory_grow() {
2702        let wat = r#"
2703            (module
2704                (memory 1)
2705                (func (export "test") (param i32) (result i32)
2706                    local.get 0
2707                    memory.grow
2708                )
2709            )
2710        "#;
2711
2712        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2713        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2714
2715        assert_eq!(functions.len(), 1);
2716        assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
2717    }
2718
2719    #[test]
2720    fn test_decode_bulk_memory_374() {
2721        // #374: memory.copy / memory.fill on the single linear memory decode to
2722        // the new WasmOp variants (was `_ => None` -> loud-skip).
2723        let wat = r#"
2724            (module
2725                (memory 1)
2726                (func (export "cpy") (param i32 i32 i32)
2727                    local.get 0 local.get 1 local.get 2 memory.copy)
2728                (func (export "fil") (param i32 i32 i32)
2729                    local.get 0 local.get 1 local.get 2 memory.fill)
2730            )
2731        "#;
2732        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2733        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2734        assert_eq!(functions.len(), 2);
2735        assert!(functions[0].ops.contains(&WasmOp::MemoryCopy));
2736        assert!(functions[1].ops.contains(&WasmOp::MemoryFill));
2737        // Neither function is flagged unsupported (they now lower).
2738        assert!(functions[0].unsupported.is_none());
2739        assert!(functions[1].unsupported.is_none());
2740    }
2741
2742    #[test]
2743    fn test_decode_i64_subword_loads() {
2744        let wat = r#"
2745            (module
2746                (memory 1)
2747                (func (export "test") (param i32) (result i64)
2748                    local.get 0
2749                    i64.load8_s
2750                )
2751            )
2752        "#;
2753
2754        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2755        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2756
2757        assert_eq!(functions.len(), 1);
2758        assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
2759            offset: 0,
2760            align: 0,
2761        }));
2762    }
2763
2764    #[test]
2765    fn test_decode_all_subword_memory_ops() {
2766        // Test that all sub-word operations are decoded from WAT
2767        let wat = r#"
2768            (module
2769                (memory 1)
2770                (func (export "test") (param i32)
2771                    ;; i32 sub-word loads
2772                    local.get 0
2773                    i32.load8_s
2774                    drop
2775                    local.get 0
2776                    i32.load8_u
2777                    drop
2778                    local.get 0
2779                    i32.load16_s
2780                    drop
2781                    local.get 0
2782                    i32.load16_u
2783                    drop
2784
2785                    ;; i32 sub-word stores
2786                    local.get 0
2787                    i32.const 42
2788                    i32.store8
2789                    local.get 0
2790                    i32.const 42
2791                    i32.store16
2792
2793                    ;; i64 sub-word loads
2794                    local.get 0
2795                    i64.load8_s
2796                    drop
2797                    local.get 0
2798                    i64.load8_u
2799                    drop
2800                    local.get 0
2801                    i64.load16_s
2802                    drop
2803                    local.get 0
2804                    i64.load16_u
2805                    drop
2806                    local.get 0
2807                    i64.load32_s
2808                    drop
2809                    local.get 0
2810                    i64.load32_u
2811                    drop
2812
2813                    ;; i64 sub-word stores
2814                    local.get 0
2815                    i64.const 42
2816                    i64.store8
2817                    local.get 0
2818                    i64.const 42
2819                    i64.store16
2820                    local.get 0
2821                    i64.const 42
2822                    i64.store32
2823                )
2824            )
2825        "#;
2826
2827        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2828        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2829
2830        assert_eq!(functions.len(), 1);
2831        let ops = &functions[0].ops;
2832
2833        // Verify i32 sub-word ops are present
2834        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
2835        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
2836        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
2837        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
2838        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
2839        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
2840
2841        // Verify i64 sub-word ops are present
2842        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
2843        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
2844        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
2845        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
2846        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
2847        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
2848        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
2849        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
2850        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
2851    }
2852
2853    #[test]
2854    fn test_decode_simd_i32x4_add() {
2855        let wat = r#"
2856            (module
2857                (func (export "add_v128") (param v128 v128) (result v128)
2858                    local.get 0
2859                    local.get 1
2860                    i32x4.add
2861                )
2862            )
2863        "#;
2864
2865        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2866        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2867
2868        assert_eq!(functions.len(), 1);
2869        assert!(
2870            functions[0].ops.contains(&WasmOp::I32x4Add),
2871            "Should decode i32x4.add: {:?}",
2872            functions[0].ops
2873        );
2874    }
2875
2876    #[test]
2877    fn test_decode_simd_v128_const() {
2878        let wat = r#"
2879            (module
2880                (func (export "const_v128") (result v128)
2881                    v128.const i32x4 1 2 3 4
2882                )
2883            )
2884        "#;
2885
2886        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2887        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2888
2889        assert_eq!(functions.len(), 1);
2890        assert!(
2891            functions[0]
2892                .ops
2893                .iter()
2894                .any(|o| matches!(o, WasmOp::V128Const(_))),
2895            "Should decode v128.const: {:?}",
2896            functions[0].ops
2897        );
2898    }
2899
2900    #[test]
2901    fn test_decode_simd_v128_load_store() {
2902        let wat = r#"
2903            (module
2904                (memory 1)
2905                (func (export "load_store") (param i32)
2906                    local.get 0
2907                    v128.load
2908                    local.get 0
2909                    v128.store
2910                )
2911            )
2912        "#;
2913
2914        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2915        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2916
2917        assert_eq!(functions.len(), 1);
2918        let ops = &functions[0].ops;
2919        assert!(
2920            ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
2921            "Should decode v128.load"
2922        );
2923        assert!(
2924            ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
2925            "Should decode v128.store"
2926        );
2927    }
2928
2929    #[test]
2930    fn test_decode_simd_bitwise_ops() {
2931        let wat = r#"
2932            (module
2933                (func (export "bitwise") (param v128 v128) (result v128)
2934                    local.get 0
2935                    local.get 1
2936                    v128.and
2937                )
2938            )
2939        "#;
2940
2941        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2942        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2943
2944        assert_eq!(functions.len(), 1);
2945        assert!(functions[0].ops.contains(&WasmOp::V128And));
2946    }
2947
2948    #[test]
2949    fn test_decode_simd_splat() {
2950        let wat = r#"
2951            (module
2952                (func (export "splat") (param i32) (result v128)
2953                    local.get 0
2954                    i32x4.splat
2955                )
2956            )
2957        "#;
2958
2959        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2960        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2961
2962        assert_eq!(functions.len(), 1);
2963        assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
2964    }
2965
2966    #[test]
2967    fn test_decode_simd_extract_lane() {
2968        let wat = r#"
2969            (module
2970                (func (export "extract") (param v128) (result i32)
2971                    local.get 0
2972                    i32x4.extract_lane 2
2973                )
2974            )
2975        "#;
2976
2977        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2978        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2979
2980        assert_eq!(functions.len(), 1);
2981        assert!(
2982            functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
2983            "Should decode i32x4.extract_lane 2"
2984        );
2985    }
2986
2987    #[test]
2988    fn test_decode_simd_f32x4_arithmetic() {
2989        let wat = r#"
2990            (module
2991                (func (export "f32x4_add") (param v128 v128) (result v128)
2992                    local.get 0
2993                    local.get 1
2994                    f32x4.add
2995                )
2996            )
2997        "#;
2998
2999        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
3000        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
3001
3002        assert_eq!(functions.len(), 1);
3003        assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
3004    }
3005
3006    #[test]
3007    fn test_369_scalar_float_op_flags_function_unsupported_not_dropped() {
3008        // GI-FPU-002 (#619): the in-scope scalar f32 ops (add/sub/mul/div,
3009        // comparisons, i32.trunc_f32_s/u, f32.convert_i32_s/u, f32.const) are
3010        // now DECODED (routed to the VFP selector on FPU targets), so `f32.add`
3011        // is no longer flagged — and since phase 2 (#369) so is the lowered
3012        // f64 subset (`f64.add` here; the m7dp-only capability gate lives in
3013        // the selector). Since phase 3 the f64 op TAIL (`f64.min` here) is
3014        // decoded too; an out-of-scope scalar float op (`i64.trunc_f64_s` —
3015        // the i64<->f64 conversions need lowered pair plumbing) is STILL
3016        // flagged (loud-skip), never silently dropped — the #369 honesty
3017        // contract holds for the not-yet-lowered surface. A pure-integer
3018        // function stays clean.
3019        let wat = r#"
3020            (module
3021                (func (export "fadd") (param f32 f32) (result f32)
3022                    local.get 0 local.get 1 f32.add)
3023                (func (export "dadd") (param f64 f64) (result f64)
3024                    local.get 0 local.get 1 f64.add)
3025                (func (export "dmin") (param f64 f64) (result f64)
3026                    local.get 0 local.get 1 f64.min)
3027                (func (export "dtrunc64") (param f64) (result i64)
3028                    local.get 0 i64.trunc_f64_s)
3029                (func (export "iadd") (param i32 i32) (result i32)
3030                    local.get 0 local.get 1 i32.add))
3031        "#;
3032        let wasm = wat::parse_str(wat).expect("parse");
3033        let functions = decode_wasm_functions(&wasm).expect("decode");
3034        let fadd = functions
3035            .iter()
3036            .find(|f| f.export_name.as_deref() == Some("fadd"))
3037            .unwrap();
3038        let dadd = functions
3039            .iter()
3040            .find(|f| f.export_name.as_deref() == Some("dadd"))
3041            .unwrap();
3042        let dmin = functions
3043            .iter()
3044            .find(|f| f.export_name.as_deref() == Some("dmin"))
3045            .unwrap();
3046        let dtrunc64 = functions
3047            .iter()
3048            .find(|f| f.export_name.as_deref() == Some("dtrunc64"))
3049            .unwrap();
3050        let iadd = functions
3051            .iter()
3052            .find(|f| f.export_name.as_deref() == Some("iadd"))
3053            .unwrap();
3054        // In-scope f32 op: now decoded (reachable), not flagged.
3055        assert!(
3056            fadd.unsupported.is_none(),
3057            "GI-FPU-002: f32.add must now decode (not be flagged), got {:?}",
3058            fadd.unsupported
3059        );
3060        assert!(
3061            fadd.ops.contains(&WasmOp::F32Add),
3062            "f32.add must decode to WasmOp::F32Add: {:?}",
3063            fadd.ops
3064        );
3065        // In-scope f64 op (phase 2, #369): now decoded, not flagged.
3066        assert!(
3067            dadd.unsupported.is_none(),
3068            "GI-FPU-002 phase 2: f64.add must now decode (not be flagged), got {:?}",
3069            dadd.unsupported
3070        );
3071        assert!(
3072            dadd.ops.contains(&WasmOp::F64Add),
3073            "f64.add must decode to WasmOp::F64Add: {:?}",
3074            dadd.ops
3075        );
3076        // In-scope f64 tail op (phase 3, #369): now decoded, not flagged.
3077        assert!(
3078            dmin.unsupported.is_none(),
3079            "GI-FPU-002 phase 3: f64.min must now decode (not be flagged), got {:?}",
3080            dmin.unsupported
3081        );
3082        assert!(
3083            dmin.ops.contains(&WasmOp::F64Min),
3084            "f64.min must decode to WasmOp::F64Min: {:?}",
3085            dmin.ops
3086        );
3087        // Out-of-scope scalar double op: still flagged, never dropped.
3088        assert!(
3089            dtrunc64.unsupported.is_some(),
3090            "i64.trunc_f64_s must still flag the function unsupported (out of scope), got {:?}",
3091            dtrunc64.unsupported
3092        );
3093        assert!(
3094            iadd.unsupported.is_none(),
3095            "a pure-integer function must NOT be flagged: {:?}",
3096            iadd.unsupported
3097        );
3098    }
3099
3100    #[test]
3101    fn test_369_float_global_access_flags_function_unsupported() {
3102        // GI-FPU-001 (#369): `global.get`/`global.set` on an f32/f64-typed
3103        // global decode fine (the ops are type-agnostic), but the float
3104        // initializer is dropped (`init_i32: None` -> slot zeroed), so a read
3105        // returned a silently-wrong 0.0 instead of the init (verified: the
3106        // 2.5f bit pattern 0x40200000 was absent from the output ELF). The
3107        // access must flag the function for the loud-skip path. Accesses to
3108        // integer globals stay clean.
3109        let wat = r#"
3110            (module
3111                (global $fg f32 (f32.const 2.5))
3112                (global $dg (mut f64) (f64.const 1.5))
3113                (global $ig (mut i32) (i32.const 7))
3114                (func (export "fget") (result f32) global.get $fg)
3115                (func (export "dset") (param f64) local.get 0 global.set $dg)
3116                (func (export "iget") (result i32) global.get $ig))
3117        "#;
3118        let wasm = wat::parse_str(wat).expect("parse");
3119
3120        // Both decode entry points must flag (the CLI compiles through both:
3121        // decode_wasm_module on the all-exports/module paths,
3122        // decode_wasm_functions on the single-function path).
3123        let module = decode_wasm_module(&wasm).expect("decode module");
3124        for functions in [
3125            &module.functions,
3126            &decode_wasm_functions(&wasm).expect("decode fns"),
3127        ] {
3128            let by_name = |n: &str| {
3129                functions
3130                    .iter()
3131                    .find(|f| f.export_name.as_deref() == Some(n))
3132                    .unwrap()
3133            };
3134            let fget = by_name("fget");
3135            assert!(
3136                fget.unsupported.is_some(),
3137                "global.get of an f32 global must flag the function (loud-skip), got {:?}",
3138                fget.unsupported
3139            );
3140            let reason = fget.unsupported.as_deref().unwrap();
3141            assert!(
3142                reason.contains("GlobalGet") && reason.contains("GI-FPU-001"),
3143                "diagnostic should name the op and GI-FPU-001: {reason:?}"
3144            );
3145            let dset = by_name("dset");
3146            assert!(
3147                dset.unsupported
3148                    .as_deref()
3149                    .is_some_and(|r| r.contains("GlobalSet")),
3150                "global.set of an f64 global must flag the function, got {:?}",
3151                dset.unsupported
3152            );
3153            assert!(
3154                by_name("iget").unsupported.is_none(),
3155                "an i32 global access must NOT be flagged: {:?}",
3156                by_name("iget").unsupported
3157            );
3158        }
3159    }
3160
3161    #[test]
3162    fn test_369_imported_float_global_shifts_index_space() {
3163        // GI-FPU-001 (#369): imported globals come FIRST in the global index
3164        // space. An imported f64 global at index 0 must be flagged, and the
3165        // defined i32 global at index 1 must NOT be mistaken for it.
3166        let wat = r#"
3167            (module
3168                (import "env" "fg" (global f64))
3169                (global $ig i32 (i32.const 3))
3170                (func (export "fget") (result f64) global.get 0)
3171                (func (export "iget") (result i32) global.get 1))
3172        "#;
3173        let wasm = wat::parse_str(wat).expect("parse");
3174        let functions = decode_wasm_functions(&wasm).expect("decode");
3175        let by_name = |n: &str| {
3176            functions
3177                .iter()
3178                .find(|f| f.export_name.as_deref() == Some(n))
3179                .unwrap()
3180        };
3181        assert!(
3182            by_name("fget")
3183                .unsupported
3184                .as_deref()
3185                .is_some_and(|r| r.contains("GI-FPU-001")),
3186            "imported f64 global access must flag: {:?}",
3187            by_name("fget").unsupported
3188        );
3189        assert!(
3190            by_name("iget").unsupported.is_none(),
3191            "defined i32 global at shifted index 1 must NOT flag: {:?}",
3192            by_name("iget").unsupported
3193        );
3194    }
3195
3196    #[test]
3197    fn test_680_simd_ops_flag_function_unsupported_not_dropped() {
3198        // #680: SIMD (v128) ops decode into WasmOp variants no production
3199        // target can select (`has_helium` is test-only), so they were silently
3200        // dropped at selection — `i32x4.add` compiled to an operand
3201        // passthrough (`mov r0,r1`) and shipped a wrong result. The issue's
3202        // exact reproducer must flag the function; the scalar sibling must
3203        // stay compilable (non-vacuity).
3204        let wat = r#"
3205            (module
3206                (memory 1)
3207                (func (export "vadd") (param i32 i32) (result i32)
3208                    (i32x4.extract_lane 2
3209                        (i32x4.add (i32x4.splat (local.get 0))
3210                                   (i32x4.splat (local.get 1)))))
3211                (func (export "vstore") (param i32 i32) (result i32)
3212                    (v128.store (i32.const 0) (i32x4.splat (local.get 0)))
3213                    (i32.load (i32.const 0)))
3214                (func (export "iadd") (param i32 i32) (result i32)
3215                    local.get 0 local.get 1 i32.add))
3216        "#;
3217        let wasm = wat::parse_str(wat).expect("parse");
3218
3219        // Both decode entry points must flag (the CLI compiles through both).
3220        let module = decode_wasm_module(&wasm).expect("decode module");
3221        for functions in [
3222            &module.functions,
3223            &decode_wasm_functions(&wasm).expect("decode fns"),
3224        ] {
3225            let by_name = |n: &str| {
3226                functions
3227                    .iter()
3228                    .find(|f| f.export_name.as_deref() == Some(n))
3229                    .unwrap()
3230            };
3231            for name in ["vadd", "vstore"] {
3232                let reason = by_name(name).unsupported.as_deref();
3233                assert!(
3234                    reason.is_some(),
3235                    "{name}: v128 ops must flag the function (loud-skip), got None"
3236                );
3237                let reason = reason.unwrap();
3238                assert!(
3239                    reason.contains("no SIMD lowering for this target") && reason.contains("#680"),
3240                    "{name}: diagnostic must name the target gap and #680: {reason:?}"
3241                );
3242            }
3243            // The reason names the FIRST SIMD op hit (splat in both bodies).
3244            assert!(
3245                by_name("vadd")
3246                    .unsupported
3247                    .as_deref()
3248                    .unwrap()
3249                    .contains("I32x4Splat"),
3250                "diagnostic should name the op: {:?}",
3251                by_name("vadd").unsupported
3252            );
3253            assert!(
3254                by_name("iadd").unsupported.is_none(),
3255                "a scalar function in the same module must NOT be flagged: {:?}",
3256                by_name("iadd").unsupported
3257            );
3258        }
3259    }
3260
3261    #[test]
3262    fn test_680_v128_local_and_signature_flag_function() {
3263        // #680: v128 VALUES are expressible with ZERO SIMD-proposal operators
3264        // in the body — a v128-typed local or a v128 param/result is reached
3265        // through type-agnostic `local.get`/`local.set`, which the selectors
3266        // lower as 4-byte moves (silent 16-byte truncation). Both must flag.
3267        let wat = r#"
3268            (module
3269                (func (export "vlocal") (result i32) (local v128)
3270                    i32.const 7)
3271                (func (export "vpass") (param v128) (result v128)
3272                    local.get 0)
3273                (func (export "scalar") (param i32) (result i32)
3274                    local.get 0))
3275        "#;
3276        let wasm = wat::parse_str(wat).expect("parse");
3277        let module = decode_wasm_module(&wasm).expect("decode module");
3278        for functions in [
3279            &module.functions,
3280            &decode_wasm_functions(&wasm).expect("decode fns"),
3281        ] {
3282            let by_name = |n: &str| {
3283                functions
3284                    .iter()
3285                    .find(|f| f.export_name.as_deref() == Some(n))
3286                    .unwrap()
3287            };
3288            assert!(
3289                by_name("vlocal")
3290                    .unsupported
3291                    .as_deref()
3292                    .is_some_and(|r| r.contains("v128-typed local") && r.contains("#680")),
3293                "a v128-typed local declaration must flag: {:?}",
3294                by_name("vlocal").unsupported
3295            );
3296            assert!(
3297                by_name("vpass")
3298                    .unsupported
3299                    .as_deref()
3300                    .is_some_and(|r| r.contains("v128 param/result") && r.contains("#680")),
3301                "a v128 param/result signature must flag (op-free body!): {:?}",
3302                by_name("vpass").unsupported
3303            );
3304            assert!(
3305                by_name("scalar").unsupported.is_none(),
3306                "a scalar function must NOT be flagged: {:?}",
3307                by_name("scalar").unsupported
3308            );
3309        }
3310    }
3311
3312    #[test]
3313    fn test_680_v128_global_access_flags_function() {
3314        // #680: `global.get`/`global.set` on a v128-typed global decode fine
3315        // (type-agnostic ops), but the access would move 4 of the 16 bytes and
3316        // the `v128.const` initializer is never captured. Same lane as the
3317        // float globals (#648/GI-FPU-001); imported globals shift the index
3318        // space (imports first). The i32-global sibling stays compilable.
3319        let wat = r#"
3320            (module
3321                (import "env" "vg" (global v128))
3322                (global $ig (mut i32) (i32.const 7))
3323                (global $dg (mut v128) (v128.const i32x4 1 2 3 4))
3324                (func (export "vget") global.get 0 drop)
3325                (func (export "iget") (result i32) global.get $ig))
3326        "#;
3327        let wasm = wat::parse_str(wat).expect("parse");
3328        let module = decode_wasm_module(&wasm).expect("decode module");
3329        for functions in [
3330            &module.functions,
3331            &decode_wasm_functions(&wasm).expect("decode fns"),
3332        ] {
3333            let by_name = |n: &str| {
3334                functions
3335                    .iter()
3336                    .find(|f| f.export_name.as_deref() == Some(n))
3337                    .unwrap()
3338            };
3339            let reason = by_name("vget").unsupported.as_deref();
3340            assert!(
3341                reason.is_some_and(|r| r.contains("GlobalGet")
3342                    && r.contains("v128-typed global")
3343                    && r.contains("#680")),
3344                "global.get of an imported v128 global must flag: {reason:?}"
3345            );
3346            assert!(
3347                by_name("iget").unsupported.is_none(),
3348                "an i32 global access must NOT be flagged: {:?}",
3349                by_name("iget").unsupported
3350            );
3351        }
3352    }
3353
3354    #[test]
3355    fn test_decode_simd_multiple_ops() {
3356        let wat = r#"
3357            (module
3358                (func (export "simd_ops") (param v128 v128 v128) (result v128)
3359                    ;; (a + b) * c
3360                    local.get 0
3361                    local.get 1
3362                    i32x4.add
3363                    local.get 2
3364                    i32x4.mul
3365                )
3366            )
3367        "#;
3368
3369        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
3370        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
3371
3372        assert_eq!(functions.len(), 1);
3373        let ops = &functions[0].ops;
3374        assert!(ops.contains(&WasmOp::I32x4Add));
3375        assert!(ops.contains(&WasmOp::I32x4Mul));
3376    }
3377
3378    /// VCR-DBG-001 step 1 (#394): the decoder records a module-relative wasm byte
3379    /// offset per emitted op — the DWARF-for-wasm address space that bridges
3380    /// synth's op-index `source_line` to the input wasm's `.debug_line`. Purely
3381    /// additive metadata (no codegen consumer ⇒ frozen fixtures byte-identical,
3382    /// verified separately); this test pins the structural invariants.
3383    #[test]
3384    fn test_decode_records_aligned_increasing_op_offsets_dbg001() {
3385        let wat = r#"
3386            (module
3387                (func (export "f") (param i32 i32) (result i32)
3388                    local.get 0
3389                    local.get 1
3390                    i32.add
3391                    i32.const 7
3392                    i32.mul))
3393        "#;
3394        let wasm = wat::parse_str(wat).expect("parse WAT");
3395        let functions = decode_wasm_functions(&wasm).expect("decode");
3396        let f = &functions[0];
3397
3398        // One offset per emitted op, index-aligned with `ops`.
3399        assert_eq!(
3400            f.op_offsets.len(),
3401            f.ops.len(),
3402            "op_offsets must be parallel to ops"
3403        );
3404        assert!(!f.op_offsets.is_empty());
3405
3406        // Byte offsets are strictly increasing through the body (each op consumes
3407        // at least one byte) and module-relative (well past the header).
3408        assert!(
3409            f.op_offsets.windows(2).all(|w| w[1] > w[0]),
3410            "wasm byte offsets must strictly increase: {:?}",
3411            f.op_offsets
3412        );
3413        assert!(
3414            f.op_offsets[0] >= 8,
3415            "module-relative offset is past the 8-byte wasm header"
3416        );
3417    }
3418
3419    /// #237: the decoder captures a global's `i32.const` initializer + mutability,
3420    /// so the native-pointer ABI can recognize the stack-pointer global.
3421    #[test]
3422    fn test_decode_captures_global_initializer() {
3423        let wat = r#"
3424            (module
3425                (memory 2)
3426                (global $__stack_pointer (mut i32) (i32.const 65536))
3427                (global $immutable_const i32 (i32.const 7))
3428                (func (export "f") (result i32) global.get 0)
3429            )
3430        "#;
3431        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3432        let module = decode_wasm_module(&wasm).expect("Failed to decode");
3433
3434        assert_eq!(module.globals.len(), 2, "both globals captured");
3435        let sp = &module.globals[0];
3436        assert_eq!(sp.index, 0);
3437        assert_eq!(
3438            sp.init,
3439            Some(GlobalInit::I32(65536)),
3440            "stack-pointer init captured"
3441        );
3442        assert!(sp.mutable, "stack pointer is mutable");
3443        let c = &module.globals[1];
3444        assert_eq!(c.init, Some(GlobalInit::I32(7)));
3445        assert!(!c.mutable, "second global is immutable");
3446        assert_eq!(sp.slot_bytes, 4, "i32 global occupies one 4-byte slot");
3447        assert_eq!(c.slot_bytes, 4);
3448    }
3449
3450    /// #643: the decoder records the DECLARED slot width per global — an i64
3451    /// (or f64) global occupies 8 bytes, so the globals-table layout can give
3452    /// it room for both words and shift every later global's offset.
3453    #[test]
3454    fn test_decode_records_global_slot_widths_643() {
3455        let wat = r#"
3456            (module
3457                (global $c (mut i64) (i64.const 0))
3458                (global $k (mut i32) (i32.const 0))
3459                (global $f (mut f64) (f64.const 0))
3460                (func (export "f") (result i32) global.get 1)
3461            )
3462        "#;
3463        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3464        let module = decode_wasm_module(&wasm).expect("Failed to decode");
3465
3466        assert_eq!(module.globals.len(), 3);
3467        assert_eq!(module.globals[0].slot_bytes, 8, "i64 global is 8 bytes");
3468        assert_eq!(module.globals[1].slot_bytes, 4, "i32 global is 4 bytes");
3469        assert_eq!(module.globals[2].slot_bytes, 8, "f64 global is 8 bytes");
3470    }
3471
3472    /// #649: a nonzero `i64.const` initializer is captured as BOTH words — the
3473    /// `init_i32`-shaped capture dropped it to `None` and every consumer's
3474    /// `unwrap_or(0)` silently ZEROED the global. f32/f64 inits stay `None`
3475    /// (GI-FPU-001/#369 loud-skip lane — never fabricate a float bit-pattern).
3476    #[test]
3477    fn test_decode_captures_i64_global_initializer_649() {
3478        let wat = r#"
3479            (module
3480                (global $g (mut i64) (i64.const 0x123456789ABCDEF0))
3481                (global $n (mut i64) (i64.const -1))
3482                (global $f (mut f64) (f64.const 1.5))
3483                (global $h (mut f32) (f32.const 2.5))
3484                (func (export "f") (result i32) i32.const 0)
3485            )
3486        "#;
3487        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3488        let module = decode_wasm_module(&wasm).expect("Failed to decode");
3489
3490        assert_eq!(module.globals.len(), 4);
3491        assert_eq!(
3492            module.globals[0].init,
3493            Some(GlobalInit::I64(0x123456789ABCDEF0u64 as i64)),
3494            "nonzero i64 init captured with both words"
3495        );
3496        assert_eq!(module.globals[1].init, Some(GlobalInit::I64(-1)));
3497        assert_eq!(
3498            module.globals[2].init, None,
3499            "f64 init is NOT captured (GI-FPU-001 loud-skip lane)"
3500        );
3501        assert_eq!(
3502            module.globals[3].init, None,
3503            "f32 init is NOT captured (GI-FPU-001 loud-skip lane)"
3504        );
3505    }
3506
3507    /// #509: the decoder records `(param_count, result_count)` for every
3508    /// `Block`/`Loop`/`If`, ordinal-keyed in op order, covering all three
3509    /// blocktype encodings: `Empty → (0,0)`, `ValType → (0,1)`, and
3510    /// `FuncType(i) →` counts from the type section (here a multi-result
3511    /// block, which wat encodes as a functype blocktype).
3512    #[test]
3513    fn test_decode_records_block_arity_side_table_509() {
3514        let wat = r#"
3515            (module
3516                (func (export "f") (param i32) (result i32)
3517                    (block (result i32)
3518                        (block (nop))
3519                        (local.get 0)
3520                        (if (result i32)
3521                            (then (i32.const 1))
3522                            (else (i32.const 2)))))
3523                (func (export "g") (result i32)
3524                    (block (result i32 i32)
3525                        (i32.const 1) (i32.const 2))
3526                    i32.add)
3527                (func (export "h") (param i32) (result i32)
3528                    (local.get 0)
3529                    (loop (param i32) (result i32))))
3530        "#;
3531        let wasm = wat::parse_str(wat).expect("parse WAT");
3532
3533        // Both decode entry points must produce the same side-table.
3534        for functions in [
3535            decode_wasm_functions(&wasm).expect("decode"),
3536            decode_wasm_module(&wasm).expect("decode").functions,
3537        ] {
3538            // f: Block(result i32), Block(void), If(result i32) — in op order.
3539            assert_eq!(
3540                functions[0].block_arity,
3541                vec![(0, 1), (0, 0), (0, 1)],
3542                "f: ValType/Empty/ValType blocktypes"
3543            );
3544            // g: one multi-result block via a FuncType blocktype.
3545            assert_eq!(
3546                functions[1].block_arity,
3547                vec![(0, 2)],
3548                "g: functype blocktype result count from the type section"
3549            );
3550            // h: a parameterized loop — the input arity is what a br to the
3551            // header would carry (the #509 loud-decline discriminator).
3552            assert_eq!(
3553                functions[2].block_arity,
3554                vec![(1, 1)],
3555                "h: loop params captured"
3556            );
3557        }
3558    }
3559
3560    /// #642: the decoder captures table 0's compile-time size, per-segment
3561    /// element shapes and per-function type indices, and the closed-world
3562    /// verdict VERIFIES a fully-covered homogeneous table.
3563    #[test]
3564    fn test_call_indirect_guards_closed_world_verified_642() {
3565        // The #642 repro shape: 3-entry table, fully covered, one signature.
3566        let wat = r#"
3567            (module
3568                (type $bin (func (param i32 i32) (result i32)))
3569                (table 3 funcref)
3570                (elem (i32.const 0) $add $sub $mul)
3571                (func $add (param i32 i32) (result i32)
3572                    (i32.add (local.get 0) (local.get 1)))
3573                (func $sub (param i32 i32) (result i32)
3574                    (i32.sub (local.get 0) (local.get 1)))
3575                (func $mul (param i32 i32) (result i32)
3576                    (i32.mul (local.get 0) (local.get 1)))
3577                (func (export "f") (param i32 i32) (result i32)
3578                    (call_indirect (type $bin)
3579                        (local.get 0) (i32.const 10) (local.get 1)))
3580            )
3581        "#;
3582        let wasm = wat::parse_str(wat).expect("parse");
3583        let module = decode_wasm_module(&wasm).expect("decode");
3584
3585        assert_eq!(module.table_size, Some(3), "table section min size");
3586        assert_eq!(module.table_sizes, vec![Some(3)], "#650 per-table sizes");
3587        assert_eq!(
3588            module.elem_segments,
3589            vec![ElemSegmentInfo {
3590                table_index: 0,
3591                offset: Some(0),
3592                funcs: Some(vec![0, 1, 2]),
3593            }]
3594        );
3595        // 2 type-section entries ($bin + the export's (i32 i32)->i32 dedups
3596        // to one in practice, but don't assume — just check func 0..2 share
3597        // a signature with type 0).
3598        assert_eq!(module.func_type_indices.len(), 4);
3599
3600        let guards = module.call_indirect_guards();
3601        assert_eq!(guards.tables.len(), 1);
3602        assert_eq!(guards.tables[0].table_size, Some(3));
3603        assert_eq!(
3604            guards.tables[0].base_byte_offset,
3605            Some(0),
3606            "#650: a single-table module keeps table 0 at R11 offset 0 by construction"
3607        );
3608        // Type index 0 ($bin) must be VERIFIED: every table entry has its
3609        // exact signature.
3610        assert_eq!(
3611            guards.tables[0].type_reject.first(),
3612            Some(&None),
3613            "closed-world type check must verify the homogeneous table: {:?}",
3614            guards.tables[0].type_reject
3615        );
3616        assert!(
3617            !guards.tables[0].has_null_slots,
3618            "#664: a fully-initialized table must NOT request the runtime \
3619             null check (dispatch bytes stay identical by construction)"
3620        );
3621    }
3622
3623    /// #642: a heterogeneous table (an entry whose signature differs from the
3624    /// expected type) must REJECT that expected type — the raw code-pointer
3625    /// table cannot be runtime-type-checked, so the lowering has to decline.
3626    #[test]
3627    fn test_call_indirect_guards_heterogeneous_table_rejects_642() {
3628        let wat = r#"
3629            (module
3630                (type $bin (func (param i32 i32) (result i32)))
3631                (type $un (func (param i32) (result i32)))
3632                (table 2 funcref)
3633                (elem (i32.const 0) $add $neg)
3634                (func $add (type $bin)
3635                    (i32.add (local.get 0) (local.get 1)))
3636                (func $neg (type $un)
3637                    (i32.sub (i32.const 0) (local.get 0)))
3638                (func (export "f") (param i32 i32) (result i32)
3639                    (call_indirect (type $bin)
3640                        (local.get 0) (i32.const 10) (local.get 1)))
3641            )
3642        "#;
3643        let wasm = wat::parse_str(wat).expect("parse");
3644        let module = decode_wasm_module(&wasm).expect("decode");
3645        let guards = module.call_indirect_guards();
3646        assert_eq!(guards.tables[0].table_size, Some(2));
3647        // BOTH expected types must be rejected: the table holds one function
3648        // of each signature, so neither type's closed world holds.
3649        assert!(
3650            guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3651            "heterogeneous table must reject every expected type: {:?}",
3652            guards.tables[0].type_reject
3653        );
3654        // #676: ... but the image is statically known, so the mismatch trap
3655        // is dischargeable at RUNTIME via the type-id sidecar.
3656        assert!(
3657            guards.tables[0].runtime_type_check,
3658            "heterogeneous-but-known table must offer the runtime check (#676)"
3659        );
3660        assert_eq!(
3661            guards.type_ids_byte_offset,
3662            Some(8),
3663            "sidecar sits after the 2-slot pointer region"
3664        );
3665        assert_eq!(
3666            guards.type_ids_image,
3667            vec![1, 2],
3668            "slot 0 = $bin (class 1), slot 1 = $un (class 2)"
3669        );
3670        assert_eq!(guards.type_class_ids, vec![1, 2]);
3671    }
3672
3673    /// #664 (relaxes the #642 all-reject): an uninitialized table slot (elem
3674    /// covers less than the declared size) is a null funcref — calling it
3675    /// must trap, which is now discharged at RUNTIME (null check on the
3676    /// zero-linked pointer), so the closed-world verdict verifies the
3677    /// INITIALIZED slots and sets `has_null_slots` for the lowering.
3678    #[test]
3679    fn test_call_indirect_guards_null_slot_verifies_with_flag_664() {
3680        let wat = r#"
3681            (module
3682                (type $s (func (result i32)))
3683                (table 3 funcref)
3684                (elem (i32.const 0) $f0 $f1)
3685                (func $f0 (result i32) (i32.const 10))
3686                (func $f1 (result i32) (i32.const 11))
3687                (func (export "run") (param i32) (result i32)
3688                    (call_indirect (type $s) (local.get 0)))
3689            )
3690        "#;
3691        let wasm = wat::parse_str(wat).expect("parse");
3692        let module = decode_wasm_module(&wasm).expect("decode");
3693        let guards = module.call_indirect_guards();
3694        assert_eq!(guards.tables[0].table_size, Some(3));
3695        assert_eq!(
3696            guards.tables[0].type_reject.first(),
3697            Some(&None),
3698            "initialized slots are homogeneous in $s — the verdict must \
3699             verify despite the null slot (#664): {:?}",
3700            guards.tables[0].type_reject
3701        );
3702        assert!(
3703            guards.tables[0].has_null_slots,
3704            "slot 2 is uninitialized — the lowering must emit the runtime \
3705             null check (#664)"
3706        );
3707    }
3708
3709    /// #664: the falcon shape — a SPARSE table (only slots 1 and 3 of 4
3710    /// initialized, by two separate segments) verifies with the null flag;
3711    /// a sparse table whose INITIALIZED slots are heterogeneous still
3712    /// rejects (the runtime null check cannot discharge a TYPE mismatch).
3713    #[test]
3714    fn test_call_indirect_guards_sparse_table_664() {
3715        let wat = r#"
3716            (module
3717                (type $t (func (param i32) (result i32)))
3718                (table 4 4 funcref)
3719                (func $f1 (type $t) (i32.add (local.get 0) (i32.const 100)))
3720                (func $f3 (type $t) (i32.sub (i32.const 1000) (local.get 0)))
3721                (elem (i32.const 1) $f1)
3722                (elem (i32.const 3) $f3)
3723                (func (export "via") (param i32 i32) (result i32)
3724                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3725            )
3726        "#;
3727        let wasm = wat::parse_str(wat).expect("parse");
3728        let module = decode_wasm_module(&wasm).expect("decode");
3729        let guards = module.call_indirect_guards();
3730        assert_eq!(guards.tables[0].table_size, Some(4));
3731        assert_eq!(
3732            guards.tables[0].type_reject.first(),
3733            Some(&None),
3734            "slots 1,3 are homogeneous in $t — verified: {:?}",
3735            guards.tables[0].type_reject
3736        );
3737        assert!(guards.tables[0].has_null_slots, "slots 0,2 are null");
3738
3739        // Heterogeneous INITIALIZED slots in a sparse table: still rejected.
3740        let wat = r#"
3741            (module
3742                (type $t (func (param i32) (result i32)))
3743                (type $u (func (param i32 i32) (result i32)))
3744                (table 4 4 funcref)
3745                (func $f1 (type $t) (local.get 0))
3746                (func $f3 (type $u) (i32.add (local.get 0) (local.get 1)))
3747                (elem (i32.const 1) $f1)
3748                (elem (i32.const 3) $f3)
3749                (func (export "via") (param i32 i32) (result i32)
3750                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3751            )
3752        "#;
3753        let wasm = wat::parse_str(wat).expect("parse");
3754        let module = decode_wasm_module(&wasm).expect("decode");
3755        let guards = module.call_indirect_guards();
3756        assert!(
3757            guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3758            "a heterogeneous sparse table must still reject every type: {:?}",
3759            guards.tables[0].type_reject
3760        );
3761        // #676: the sparse-heterogeneous case is now dischargeable at
3762        // runtime too — null slots take the reserved class id 0, so ONE
3763        // sidecar compare covers both the type mismatch and the null trap.
3764        assert!(guards.tables[0].runtime_type_check, "#676 runtime check");
3765        assert_eq!(guards.type_ids_byte_offset, Some(16), "4 pointer slots");
3766        assert_eq!(
3767            guards.type_ids_image,
3768            vec![0, 1, 0, 2],
3769            "nulls at 0/2 carry the reserved id 0; $t slot 1 = class 1, \
3770             $u slot 3 = class 2"
3771        );
3772    }
3773
3774    /// #676: the heterogeneous type-id sidecar — structural duplicate types
3775    /// share one class id (the meld 31-decls/25-distinct shape), null slots
3776    /// take the reserved id 0, and the sidecar base is the total pointer
3777    /// region size. A module with NO heterogeneous table gets NO sidecar
3778    /// (empty image, `None` offset) — homogeneous modules stay untouched.
3779    #[test]
3780    fn test_call_indirect_guards_heterogeneous_sidecar_676() {
3781        let wat = r#"
3782            (module
3783                (type $bin (func (param i32 i32) (result i32)))
3784                (type $un (func (param i32) (result i32)))
3785                (type $bin2 (func (param i32 i32) (result i32)))
3786                (table 5 5 funcref)
3787                (func $add (type $bin) (i32.add (local.get 0) (local.get 1)))
3788                (func $neg (type $un) (i32.sub (i32.const 0) (local.get 0)))
3789                (func $sub (type $bin2) (i32.sub (local.get 0) (local.get 1)))
3790                (elem (i32.const 0) func $add $neg $sub)
3791                (func (export "via2") (param i32 i32) (result i32)
3792                    (call_indirect (type $bin)
3793                        (local.get 0) (i32.const 3) (local.get 1)))
3794                (func (export "via1") (param i32 i32) (result i32)
3795                    (call_indirect (type $un) (local.get 0) (local.get 1)))
3796            )
3797        "#;
3798        let wasm = wat::parse_str(wat).expect("parse");
3799        let module = decode_wasm_module(&wasm).expect("decode");
3800        let guards = module.call_indirect_guards();
3801        assert!(guards.tables[0].runtime_type_check);
3802        assert_eq!(
3803            guards.type_class_ids,
3804            vec![1, 2, 1],
3805            "$bin2 is a structural duplicate of $bin — one class id (#676)"
3806        );
3807        assert_eq!(
3808            guards.type_ids_image,
3809            vec![1, 2, 1, 0, 0],
3810            "slots: $add(bin)=1, $neg(un)=2, $sub(bin2 ≡ bin)=1, null, null"
3811        );
3812        assert_eq!(
3813            guards.type_ids_byte_offset,
3814            Some(20),
3815            "sidecar starts after the 5 pointer words"
3816        );
3817
3818        // Homogeneous module → NO sidecar, no runtime check anywhere.
3819        let wat = r#"
3820            (module
3821                (type $t (func (param i32) (result i32)))
3822                (table 2 2 funcref)
3823                (func $f0 (type $t) (local.get 0))
3824                (func $f1 (type $t) (i32.const 7))
3825                (elem (i32.const 0) func $f0 $f1)
3826                (func (export "via") (param i32 i32) (result i32)
3827                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3828            )
3829        "#;
3830        let wasm = wat::parse_str(wat).expect("parse");
3831        let module = decode_wasm_module(&wasm).expect("decode");
3832        let guards = module.call_indirect_guards();
3833        assert!(!guards.tables[0].runtime_type_check);
3834        assert_eq!(guards.type_ids_byte_offset, None, "no heterogeneous table");
3835        assert!(guards.type_ids_image.is_empty());
3836        assert!(guards.type_class_ids.is_empty());
3837    }
3838
3839    /// #642: no table at all → no compile-time bound → table_size None and
3840    /// every type rejected (the lowering declines).
3841    #[test]
3842    fn test_call_indirect_guards_no_table_642() {
3843        let wat = r#"
3844            (module
3845                (func (export "f") (param i32) (result i32) (local.get 0))
3846            )
3847        "#;
3848        let wasm = wat::parse_str(wat).expect("parse");
3849        let module = decode_wasm_module(&wasm).expect("decode");
3850        assert_eq!(module.table_size, None);
3851        assert!(module.table_sizes.is_empty(), "#650: no tables declared");
3852        let guards = module.call_indirect_guards();
3853        assert!(
3854            guards.tables.is_empty(),
3855            "no table → no guard entry → every call_indirect declines"
3856        );
3857    }
3858
3859    /// #642: duplicate-but-structurally-identical types stay interchangeable —
3860    /// the closed-world check compares SIGNATURES, not type indices.
3861    #[test]
3862    fn test_call_indirect_guards_duplicate_types_verified_642() {
3863        let wat = r#"
3864            (module
3865                (type $a (func (result i32)))
3866                (type $b (func (result i32)))
3867                (table 1 funcref)
3868                (elem (i32.const 0) $f)
3869                (func $f (type $a) (i32.const 7))
3870                (func (export "run") (param i32) (result i32)
3871                    (call_indirect (type $b) (local.get 0)))
3872            )
3873        "#;
3874        let wasm = wat::parse_str(wat).expect("parse");
3875        let module = decode_wasm_module(&wasm).expect("decode");
3876        let guards = module.call_indirect_guards();
3877        // $f has type $a; the call expects $b — structurally identical, so
3878        // BOTH type indices must verify. (A third type — the export's
3879        // (i32)->i32 — is correctly rejected: different signature.)
3880        assert_eq!(
3881            &guards.tables[0].type_reject[0..2],
3882            &[None, None],
3883            "structural signature comparison must accept duplicate types: {:?}",
3884            guards.tables[0].type_reject
3885        );
3886        assert!(
3887            guards.tables[0].type_reject[2].is_some(),
3888            "the structurally-different third type must still be rejected"
3889        );
3890    }
3891
3892    /// #650: TWO tables become a contiguous R11 region — table 0 at offset 0
3893    /// (byte-identical single-table degeneration), table 1 at
3894    /// `size(table 0) * 4`. Each table gets its OWN size, base offset, and
3895    /// per-type closed-world verdicts (segments only poison the table they
3896    /// target).
3897    #[test]
3898    fn test_call_indirect_guards_multi_table_650() {
3899        // The #650 repro shape: overlapping indices, distinct functions —
3900        // table0[1] != table1[1] (the aliasing canary).
3901        let wat = r#"
3902            (module
3903                (type $t (func (param i32) (result i32)))
3904                (type $u (func (param i32 i32) (result i32)))
3905                (table $t0 3 3 funcref)
3906                (table $t1 2 2 funcref)
3907                (func $a0 (type $t) (i32.add (local.get 0) (i32.const 100)))
3908                (func $a1 (type $t) (i32.add (local.get 0) (i32.const 200)))
3909                (func $a2 (type $t) (i32.add (local.get 0) (i32.const 300)))
3910                (func $b0 (type $u) (i32.add (local.get 0) (local.get 1)))
3911                (func $b1 (type $u) (i32.sub (local.get 0) (local.get 1)))
3912                (elem (table $t0) (i32.const 0) func $a0 $a1 $a2)
3913                (elem (table $t1) (i32.const 0) func $b0 $b1)
3914                (func (export "f") (param i32 i32) (result i32)
3915                    (call_indirect $t1 (type $u)
3916                        (local.get 0) (i32.const 10) (local.get 1)))
3917            )
3918        "#;
3919        let wasm = wat::parse_str(wat).expect("parse");
3920        let module = decode_wasm_module(&wasm).expect("decode");
3921        assert_eq!(module.table_sizes, vec![Some(3), Some(2)]);
3922        assert_eq!(module.table_size, Some(3), "compat accessor = table 0");
3923        assert_eq!(
3924            module.elem_segments[0].table_index, 0,
3925            "segment 0 targets table 0"
3926        );
3927        assert_eq!(
3928            module.elem_segments[1],
3929            ElemSegmentInfo {
3930                table_index: 1,
3931                offset: Some(0),
3932                funcs: Some(vec![3, 4]),
3933            },
3934            "segment 1 is statically attributed to table 1 (#650)"
3935        );
3936
3937        let guards = module.call_indirect_guards();
3938        assert_eq!(guards.tables.len(), 2);
3939        assert_eq!(guards.tables[0].table_size, Some(3));
3940        assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3941        assert_eq!(guards.tables[1].table_size, Some(2));
3942        assert_eq!(
3943            guards.tables[1].base_byte_offset,
3944            Some(12),
3945            "table 1 base = size(table 0) * 4 within the contiguous R11 region"
3946        );
3947        // Table 0 is homogeneous in $t (type 0); table 1 in $u (type 1) —
3948        // each verifies ITS type and rejects the other's.
3949        assert_eq!(guards.tables[0].type_reject[0], None, "table 0 vs $t");
3950        assert!(guards.tables[0].type_reject[1].is_some(), "table 0 vs $u");
3951        assert!(guards.tables[1].type_reject[0].is_some(), "table 1 vs $t");
3952        assert_eq!(guards.tables[1].type_reject[1], None, "table 1 vs $u");
3953    }
3954
3955    /// #650: an unknown-size table (growable import) declines ITSELF and
3956    /// makes every LATER table's base offset non-constant — but a table
3957    /// BEFORE it is unaffected.
3958    #[test]
3959    fn test_call_indirect_guards_unknown_size_poisons_later_bases_650() {
3960        let wat = r#"
3961            (module
3962                (type $t (func (result i32)))
3963                (import "env" "tbl" (table 4 funcref))
3964                (table $d 1 1 funcref)
3965                (func $f (type $t) (i32.const 7))
3966                (elem (table $d) (i32.const 0) func $f)
3967                (func (export "run") (param i32) (result i32)
3968                    (call_indirect $d (type $t) (local.get 0)))
3969            )
3970        "#;
3971        let wasm = wat::parse_str(wat).expect("parse");
3972        let module = decode_wasm_module(&wasm).expect("decode");
3973        assert_eq!(
3974            module.table_sizes,
3975            vec![None, Some(1)],
3976            "growable import (no max) has no sound compile-time size"
3977        );
3978        let guards = module.call_indirect_guards();
3979        assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3980        assert!(
3981            guards.tables[0].type_reject.iter().all(|r| r.is_some()),
3982            "unknown-size table rejects every type"
3983        );
3984        assert_eq!(
3985            guards.tables[1].base_byte_offset, None,
3986            "a later table's base is not a compile-time constant when a \
3987             preceding table's size is unknown (#650)"
3988        );
3989        assert_eq!(guards.tables[1].table_size, Some(1));
3990    }
3991}