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