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        // #708 (phase 1b): `f32.load` un-dropped. The selector lowers it as the
1910        // proven `i32.load` address sequence (`[R11,idx]`→absolute-base rewrite +
1911        // bounds guard) into a core register, then a bit-exact `VMOV Sd,Rd`
1912        // (reinterpret) — a VLDR loads the same 4 bytes, so the bit pattern is
1913        // identical. `f32.store` STAYS dropped (falcon's #369 inventory needs
1914        // only the load + reinterpret; the store loud-skips at decode until a
1915        // consumer needs it — never a silent miscompile).
1916        F32Load { memarg } => Some(WasmOp::F32Load {
1917            offset: memarg.offset as u32,
1918            align: memarg.align as u32,
1919        }),
1920        // #708 (phase 1b): the f32<->i32 bit-casts. Pure `VMOV` between a core
1921        // register and a single-precision S-register — no numeric conversion.
1922        F32ReinterpretI32 => Some(WasmOp::F32ReinterpretI32),
1923        I32ReinterpretF32 => Some(WasmOp::I32ReinterpretF32),
1924        F32ConvertI32S => Some(WasmOp::F32ConvertI32S),
1925        F32ConvertI32U => Some(WasmOp::F32ConvertI32U),
1926        I32TruncF32S => Some(WasmOp::I32TruncF32S),
1927        I32TruncF32U => Some(WasmOp::I32TruncF32U),
1928
1929        // f32x4
1930        F32x4Add => Some(WasmOp::F32x4Add),
1931        F32x4Sub => Some(WasmOp::F32x4Sub),
1932        F32x4Mul => Some(WasmOp::F32x4Mul),
1933        F32x4Div => Some(WasmOp::F32x4Div),
1934        F32x4Abs => Some(WasmOp::F32x4Abs),
1935        F32x4Neg => Some(WasmOp::F32x4Neg),
1936        F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
1937        F32x4Eq => Some(WasmOp::F32x4Eq),
1938        F32x4Ne => Some(WasmOp::F32x4Ne),
1939        F32x4Lt => Some(WasmOp::F32x4Lt),
1940        F32x4Le => Some(WasmOp::F32x4Le),
1941        F32x4Gt => Some(WasmOp::F32x4Gt),
1942        F32x4Ge => Some(WasmOp::F32x4Ge),
1943        F32x4Splat => Some(WasmOp::F32x4Splat),
1944        F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
1945        F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
1946
1947        // Other operators not yet supported
1948        _ => None,
1949    }
1950}
1951
1952#[cfg(test)]
1953mod tests {
1954    use super::*;
1955
1956    #[test]
1957    fn test_decode_simple_add() {
1958        let wat = r#"
1959            (module
1960                (func (export "add") (param i32 i32) (result i32)
1961                    local.get 0
1962                    local.get 1
1963                    i32.add
1964                )
1965            )
1966        "#;
1967
1968        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1969        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1970
1971        assert_eq!(functions.len(), 1);
1972        assert_eq!(functions[0].index, 0);
1973        assert_eq!(functions[0].export_name, Some("add".to_string()));
1974        assert_eq!(
1975            functions[0].ops,
1976            vec![
1977                WasmOp::LocalGet(0),
1978                WasmOp::LocalGet(1),
1979                WasmOp::I32Add,
1980                WasmOp::End
1981            ]
1982        );
1983    }
1984
1985    /// #204 regression: `i64.extend_i32_u`, `i64.extend_i32_s` and
1986    /// `i32.wrap_i64` must DECODE (they were previously unmapped → silently
1987    /// dropped by `convert_operator`, leaving an i32 value as a 64-bit operand
1988    /// with a garbage high half — the root cause of gale's miscompiled
1989    /// `(new_count << 32)` pack). The decoder must surface all three.
1990    #[test]
1991    fn test_decode_i64_i32_width_conversions() {
1992        let wat = r#"
1993            (module
1994                (func (export "conv") (param i32 i64) (result i32)
1995                    local.get 0
1996                    i64.extend_i32_u
1997                    local.get 0
1998                    i64.extend_i32_s
1999                    i64.add
2000                    local.get 1
2001                    i64.add
2002                    i32.wrap_i64
2003                )
2004            )
2005        "#;
2006        let wasm = wat::parse_str(wat).expect("parse");
2007        let functions = decode_wasm_functions(&wasm).expect("decode");
2008        let ops = &functions[0].ops;
2009        assert!(
2010            ops.contains(&WasmOp::I64ExtendI32U),
2011            "i64.extend_i32_u must decode (not be dropped): {ops:?}"
2012        );
2013        assert!(
2014            ops.contains(&WasmOp::I64ExtendI32S),
2015            "i64.extend_i32_s must decode (not be dropped): {ops:?}"
2016        );
2017        assert!(
2018            ops.contains(&WasmOp::I32WrapI64),
2019            "i32.wrap_i64 must decode (not be dropped): {ops:?}"
2020        );
2021    }
2022
2023    /// #204 WAKE-path regression: `br_table` must DECODE (it was unmapped in
2024    /// `convert_operator` → silently dropped, so the selector emitted no index
2025    /// dispatch and every `br_table` fell through to target 0 — gale's binary
2026    /// semaphore never took its WAKE branch). Targets + default are preserved.
2027    #[test]
2028    fn test_decode_br_table() {
2029        let wat = r#"
2030            (module
2031                (func (export "bt") (param i32) (result i32)
2032                    (block (block (block
2033                        local.get 0
2034                        br_table 2 0 1 2)
2035                      i32.const 30 return)
2036                      i32.const 20 return)
2037                    i32.const 10))
2038        "#;
2039        let wasm = wat::parse_str(wat).expect("parse");
2040        let functions = decode_wasm_functions(&wasm).expect("decode");
2041        let bt = functions[0]
2042            .ops
2043            .iter()
2044            .find_map(|o| match o {
2045                WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
2046                _ => None,
2047            })
2048            .expect("br_table must decode (not be dropped)");
2049        assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
2050        assert_eq!(bt.1, 2, "br_table default preserved");
2051    }
2052
2053    #[test]
2054    fn test_decode_arithmetic() {
2055        let wat = r#"
2056            (module
2057                (func (export "calc") (result i32)
2058                    i32.const 5
2059                    i32.const 3
2060                    i32.mul
2061                    i32.const 2
2062                    i32.add
2063                )
2064            )
2065        "#;
2066
2067        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2068        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2069
2070        assert_eq!(functions.len(), 1);
2071        assert_eq!(functions[0].export_name, Some("calc".to_string()));
2072        assert_eq!(
2073            functions[0].ops,
2074            vec![
2075                WasmOp::I32Const(5),
2076                WasmOp::I32Const(3),
2077                WasmOp::I32Mul,
2078                WasmOp::I32Const(2),
2079                WasmOp::I32Add,
2080                WasmOp::End,
2081            ]
2082        );
2083    }
2084
2085    #[test]
2086    fn test_decode_multi_function_module() {
2087        let wat = r#"
2088            (module
2089                (func $helper)
2090                (func (export "add") (param i32 i32) (result i32)
2091                    local.get 0
2092                    local.get 1
2093                    i32.add
2094                )
2095                (func (export "sub") (param i32 i32) (result i32)
2096                    local.get 0
2097                    local.get 1
2098                    i32.sub
2099                )
2100            )
2101        "#;
2102
2103        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2104        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2105
2106        assert_eq!(functions.len(), 3);
2107        assert_eq!(functions[0].index, 0);
2108        assert_eq!(functions[0].export_name, None);
2109        assert_eq!(functions[1].index, 1);
2110        assert_eq!(functions[1].export_name, Some("add".to_string()));
2111        assert_eq!(functions[2].index, 2);
2112        assert_eq!(functions[2].export_name, Some("sub".to_string()));
2113    }
2114
2115    #[test]
2116    fn test_decode_module_with_imports() {
2117        let wat = r#"
2118            (module
2119                (import "env" "log" (func $log (param i32)))
2120                (import "env" "memory" (memory 1))
2121                (func (export "run") (param i32)
2122                    local.get 0
2123                    call 0
2124                )
2125            )
2126        "#;
2127
2128        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2129        let module = decode_wasm_module(&wasm).expect("Failed to decode");
2130
2131        // Should have 2 imports (1 func, 1 memory)
2132        assert_eq!(module.imports.len(), 2);
2133        assert_eq!(module.num_imported_funcs, 1);
2134
2135        // First import is the function
2136        assert_eq!(module.imports[0].module, "env");
2137        assert_eq!(module.imports[0].name, "log");
2138        assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
2139
2140        // Second import is memory
2141        assert_eq!(module.imports[1].module, "env");
2142        assert_eq!(module.imports[1].name, "memory");
2143        assert_eq!(module.imports[1].kind, ImportKind::Memory);
2144
2145        // Should have 1 local function (index 1, because import is index 0)
2146        assert_eq!(module.functions.len(), 1);
2147        assert_eq!(module.functions[0].index, 1);
2148        assert_eq!(module.functions[0].export_name, Some("run".to_string()));
2149    }
2150
2151    #[test]
2152    fn test_find_function_by_export_name() {
2153        let wat = r#"
2154            (module
2155                (func $helper)
2156                (func (export "add") (param i32 i32) (result i32)
2157                    local.get 0
2158                    local.get 1
2159                    i32.add
2160                )
2161            )
2162        "#;
2163
2164        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2165        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2166
2167        let add_func = functions
2168            .iter()
2169            .find(|f| f.export_name.as_deref() == Some("add"))
2170            .expect("Should find 'add' function");
2171
2172        assert_eq!(add_func.index, 1);
2173        assert!(add_func.ops.contains(&WasmOp::I32Add));
2174    }
2175
2176    #[test]
2177    fn test_decode_subword_loads() {
2178        let wat = r#"
2179            (module
2180                (memory 1)
2181                (func (export "test") (param i32) (result i32)
2182                    local.get 0
2183                    i32.load8_u
2184                )
2185            )
2186        "#;
2187
2188        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2189        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2190
2191        assert_eq!(functions.len(), 1);
2192        assert!(functions[0].ops.contains(&WasmOp::I32Load8U {
2193            offset: 0,
2194            align: 0,
2195        }));
2196    }
2197
2198    #[test]
2199    fn test_decode_subword_stores() {
2200        let wat = r#"
2201            (module
2202                (memory 1)
2203                (func (export "test") (param i32 i32)
2204                    local.get 0
2205                    local.get 1
2206                    i32.store8
2207                )
2208            )
2209        "#;
2210
2211        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2212        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2213
2214        assert_eq!(functions.len(), 1);
2215        assert!(functions[0].ops.contains(&WasmOp::I32Store8 {
2216            offset: 0,
2217            align: 0,
2218        }));
2219    }
2220
2221    #[test]
2222    fn test_decode_memory_size_grow() {
2223        let wat = r#"
2224            (module
2225                (memory 1)
2226                (func (export "test") (result i32)
2227                    memory.size
2228                )
2229            )
2230        "#;
2231
2232        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2233        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2234
2235        assert_eq!(functions.len(), 1);
2236        assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
2237    }
2238
2239    #[test]
2240    fn test_decode_memory_grow() {
2241        let wat = r#"
2242            (module
2243                (memory 1)
2244                (func (export "test") (param i32) (result i32)
2245                    local.get 0
2246                    memory.grow
2247                )
2248            )
2249        "#;
2250
2251        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2252        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2253
2254        assert_eq!(functions.len(), 1);
2255        assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
2256    }
2257
2258    #[test]
2259    fn test_decode_bulk_memory_374() {
2260        // #374: memory.copy / memory.fill on the single linear memory decode to
2261        // the new WasmOp variants (was `_ => None` -> loud-skip).
2262        let wat = r#"
2263            (module
2264                (memory 1)
2265                (func (export "cpy") (param i32 i32 i32)
2266                    local.get 0 local.get 1 local.get 2 memory.copy)
2267                (func (export "fil") (param i32 i32 i32)
2268                    local.get 0 local.get 1 local.get 2 memory.fill)
2269            )
2270        "#;
2271        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2272        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2273        assert_eq!(functions.len(), 2);
2274        assert!(functions[0].ops.contains(&WasmOp::MemoryCopy));
2275        assert!(functions[1].ops.contains(&WasmOp::MemoryFill));
2276        // Neither function is flagged unsupported (they now lower).
2277        assert!(functions[0].unsupported.is_none());
2278        assert!(functions[1].unsupported.is_none());
2279    }
2280
2281    #[test]
2282    fn test_decode_i64_subword_loads() {
2283        let wat = r#"
2284            (module
2285                (memory 1)
2286                (func (export "test") (param i32) (result i64)
2287                    local.get 0
2288                    i64.load8_s
2289                )
2290            )
2291        "#;
2292
2293        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2294        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2295
2296        assert_eq!(functions.len(), 1);
2297        assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
2298            offset: 0,
2299            align: 0,
2300        }));
2301    }
2302
2303    #[test]
2304    fn test_decode_all_subword_memory_ops() {
2305        // Test that all sub-word operations are decoded from WAT
2306        let wat = r#"
2307            (module
2308                (memory 1)
2309                (func (export "test") (param i32)
2310                    ;; i32 sub-word loads
2311                    local.get 0
2312                    i32.load8_s
2313                    drop
2314                    local.get 0
2315                    i32.load8_u
2316                    drop
2317                    local.get 0
2318                    i32.load16_s
2319                    drop
2320                    local.get 0
2321                    i32.load16_u
2322                    drop
2323
2324                    ;; i32 sub-word stores
2325                    local.get 0
2326                    i32.const 42
2327                    i32.store8
2328                    local.get 0
2329                    i32.const 42
2330                    i32.store16
2331
2332                    ;; i64 sub-word loads
2333                    local.get 0
2334                    i64.load8_s
2335                    drop
2336                    local.get 0
2337                    i64.load8_u
2338                    drop
2339                    local.get 0
2340                    i64.load16_s
2341                    drop
2342                    local.get 0
2343                    i64.load16_u
2344                    drop
2345                    local.get 0
2346                    i64.load32_s
2347                    drop
2348                    local.get 0
2349                    i64.load32_u
2350                    drop
2351
2352                    ;; i64 sub-word stores
2353                    local.get 0
2354                    i64.const 42
2355                    i64.store8
2356                    local.get 0
2357                    i64.const 42
2358                    i64.store16
2359                    local.get 0
2360                    i64.const 42
2361                    i64.store32
2362                )
2363            )
2364        "#;
2365
2366        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2367        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2368
2369        assert_eq!(functions.len(), 1);
2370        let ops = &functions[0].ops;
2371
2372        // Verify i32 sub-word ops are present
2373        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
2374        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
2375        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
2376        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
2377        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
2378        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
2379
2380        // Verify i64 sub-word ops are present
2381        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
2382        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
2383        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
2384        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
2385        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
2386        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
2387        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
2388        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
2389        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
2390    }
2391
2392    #[test]
2393    fn test_decode_simd_i32x4_add() {
2394        let wat = r#"
2395            (module
2396                (func (export "add_v128") (param v128 v128) (result v128)
2397                    local.get 0
2398                    local.get 1
2399                    i32x4.add
2400                )
2401            )
2402        "#;
2403
2404        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2405        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2406
2407        assert_eq!(functions.len(), 1);
2408        assert!(
2409            functions[0].ops.contains(&WasmOp::I32x4Add),
2410            "Should decode i32x4.add: {:?}",
2411            functions[0].ops
2412        );
2413    }
2414
2415    #[test]
2416    fn test_decode_simd_v128_const() {
2417        let wat = r#"
2418            (module
2419                (func (export "const_v128") (result v128)
2420                    v128.const i32x4 1 2 3 4
2421                )
2422            )
2423        "#;
2424
2425        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2426        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2427
2428        assert_eq!(functions.len(), 1);
2429        assert!(
2430            functions[0]
2431                .ops
2432                .iter()
2433                .any(|o| matches!(o, WasmOp::V128Const(_))),
2434            "Should decode v128.const: {:?}",
2435            functions[0].ops
2436        );
2437    }
2438
2439    #[test]
2440    fn test_decode_simd_v128_load_store() {
2441        let wat = r#"
2442            (module
2443                (memory 1)
2444                (func (export "load_store") (param i32)
2445                    local.get 0
2446                    v128.load
2447                    local.get 0
2448                    v128.store
2449                )
2450            )
2451        "#;
2452
2453        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2454        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2455
2456        assert_eq!(functions.len(), 1);
2457        let ops = &functions[0].ops;
2458        assert!(
2459            ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
2460            "Should decode v128.load"
2461        );
2462        assert!(
2463            ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
2464            "Should decode v128.store"
2465        );
2466    }
2467
2468    #[test]
2469    fn test_decode_simd_bitwise_ops() {
2470        let wat = r#"
2471            (module
2472                (func (export "bitwise") (param v128 v128) (result v128)
2473                    local.get 0
2474                    local.get 1
2475                    v128.and
2476                )
2477            )
2478        "#;
2479
2480        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2481        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2482
2483        assert_eq!(functions.len(), 1);
2484        assert!(functions[0].ops.contains(&WasmOp::V128And));
2485    }
2486
2487    #[test]
2488    fn test_decode_simd_splat() {
2489        let wat = r#"
2490            (module
2491                (func (export "splat") (param i32) (result v128)
2492                    local.get 0
2493                    i32x4.splat
2494                )
2495            )
2496        "#;
2497
2498        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2499        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2500
2501        assert_eq!(functions.len(), 1);
2502        assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
2503    }
2504
2505    #[test]
2506    fn test_decode_simd_extract_lane() {
2507        let wat = r#"
2508            (module
2509                (func (export "extract") (param v128) (result i32)
2510                    local.get 0
2511                    i32x4.extract_lane 2
2512                )
2513            )
2514        "#;
2515
2516        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2517        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2518
2519        assert_eq!(functions.len(), 1);
2520        assert!(
2521            functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
2522            "Should decode i32x4.extract_lane 2"
2523        );
2524    }
2525
2526    #[test]
2527    fn test_decode_simd_f32x4_arithmetic() {
2528        let wat = r#"
2529            (module
2530                (func (export "f32x4_add") (param v128 v128) (result v128)
2531                    local.get 0
2532                    local.get 1
2533                    f32x4.add
2534                )
2535            )
2536        "#;
2537
2538        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2539        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2540
2541        assert_eq!(functions.len(), 1);
2542        assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
2543    }
2544
2545    #[test]
2546    fn test_369_scalar_float_op_flags_function_unsupported_not_dropped() {
2547        // GI-FPU-002 (#619): the in-scope scalar f32 ops (add/sub/mul/div,
2548        // comparisons, i32.trunc_f32_s/u, f32.convert_i32_s/u, f32.const) are
2549        // now DECODED (routed to the VFP selector on FPU targets), so `f32.add`
2550        // is no longer flagged. An out-of-scope scalar float op (`f64.add`,
2551        // phase 2) is STILL flagged (loud-skip), never silently dropped — the
2552        // #369 honesty contract holds for the not-yet-lowered surface. A
2553        // pure-integer function stays clean.
2554        let wat = r#"
2555            (module
2556                (func (export "fadd") (param f32 f32) (result f32)
2557                    local.get 0 local.get 1 f32.add)
2558                (func (export "dadd") (param f64 f64) (result f64)
2559                    local.get 0 local.get 1 f64.add)
2560                (func (export "iadd") (param i32 i32) (result i32)
2561                    local.get 0 local.get 1 i32.add))
2562        "#;
2563        let wasm = wat::parse_str(wat).expect("parse");
2564        let functions = decode_wasm_functions(&wasm).expect("decode");
2565        let fadd = functions
2566            .iter()
2567            .find(|f| f.export_name.as_deref() == Some("fadd"))
2568            .unwrap();
2569        let dadd = functions
2570            .iter()
2571            .find(|f| f.export_name.as_deref() == Some("dadd"))
2572            .unwrap();
2573        let iadd = functions
2574            .iter()
2575            .find(|f| f.export_name.as_deref() == Some("iadd"))
2576            .unwrap();
2577        // In-scope f32 op: now decoded (reachable), not flagged.
2578        assert!(
2579            fadd.unsupported.is_none(),
2580            "GI-FPU-002: f32.add must now decode (not be flagged), got {:?}",
2581            fadd.unsupported
2582        );
2583        assert!(
2584            fadd.ops.contains(&WasmOp::F32Add),
2585            "f32.add must decode to WasmOp::F32Add: {:?}",
2586            fadd.ops
2587        );
2588        // Out-of-scope scalar double op: still flagged (phase 2), never dropped.
2589        assert!(
2590            dadd.unsupported.is_some(),
2591            "f64.add must still flag the function unsupported (phase 2), got {:?}",
2592            dadd.unsupported
2593        );
2594        assert!(
2595            iadd.unsupported.is_none(),
2596            "a pure-integer function must NOT be flagged: {:?}",
2597            iadd.unsupported
2598        );
2599    }
2600
2601    #[test]
2602    fn test_369_float_global_access_flags_function_unsupported() {
2603        // GI-FPU-001 (#369): `global.get`/`global.set` on an f32/f64-typed
2604        // global decode fine (the ops are type-agnostic), but the float
2605        // initializer is dropped (`init_i32: None` -> slot zeroed), so a read
2606        // returned a silently-wrong 0.0 instead of the init (verified: the
2607        // 2.5f bit pattern 0x40200000 was absent from the output ELF). The
2608        // access must flag the function for the loud-skip path. Accesses to
2609        // integer globals stay clean.
2610        let wat = r#"
2611            (module
2612                (global $fg f32 (f32.const 2.5))
2613                (global $dg (mut f64) (f64.const 1.5))
2614                (global $ig (mut i32) (i32.const 7))
2615                (func (export "fget") (result f32) global.get $fg)
2616                (func (export "dset") (param f64) local.get 0 global.set $dg)
2617                (func (export "iget") (result i32) global.get $ig))
2618        "#;
2619        let wasm = wat::parse_str(wat).expect("parse");
2620
2621        // Both decode entry points must flag (the CLI compiles through both:
2622        // decode_wasm_module on the all-exports/module paths,
2623        // decode_wasm_functions on the single-function path).
2624        let module = decode_wasm_module(&wasm).expect("decode module");
2625        for functions in [
2626            &module.functions,
2627            &decode_wasm_functions(&wasm).expect("decode fns"),
2628        ] {
2629            let by_name = |n: &str| {
2630                functions
2631                    .iter()
2632                    .find(|f| f.export_name.as_deref() == Some(n))
2633                    .unwrap()
2634            };
2635            let fget = by_name("fget");
2636            assert!(
2637                fget.unsupported.is_some(),
2638                "global.get of an f32 global must flag the function (loud-skip), got {:?}",
2639                fget.unsupported
2640            );
2641            let reason = fget.unsupported.as_deref().unwrap();
2642            assert!(
2643                reason.contains("GlobalGet") && reason.contains("GI-FPU-001"),
2644                "diagnostic should name the op and GI-FPU-001: {reason:?}"
2645            );
2646            let dset = by_name("dset");
2647            assert!(
2648                dset.unsupported
2649                    .as_deref()
2650                    .is_some_and(|r| r.contains("GlobalSet")),
2651                "global.set of an f64 global must flag the function, got {:?}",
2652                dset.unsupported
2653            );
2654            assert!(
2655                by_name("iget").unsupported.is_none(),
2656                "an i32 global access must NOT be flagged: {:?}",
2657                by_name("iget").unsupported
2658            );
2659        }
2660    }
2661
2662    #[test]
2663    fn test_369_imported_float_global_shifts_index_space() {
2664        // GI-FPU-001 (#369): imported globals come FIRST in the global index
2665        // space. An imported f64 global at index 0 must be flagged, and the
2666        // defined i32 global at index 1 must NOT be mistaken for it.
2667        let wat = r#"
2668            (module
2669                (import "env" "fg" (global f64))
2670                (global $ig i32 (i32.const 3))
2671                (func (export "fget") (result f64) global.get 0)
2672                (func (export "iget") (result i32) global.get 1))
2673        "#;
2674        let wasm = wat::parse_str(wat).expect("parse");
2675        let functions = decode_wasm_functions(&wasm).expect("decode");
2676        let by_name = |n: &str| {
2677            functions
2678                .iter()
2679                .find(|f| f.export_name.as_deref() == Some(n))
2680                .unwrap()
2681        };
2682        assert!(
2683            by_name("fget")
2684                .unsupported
2685                .as_deref()
2686                .is_some_and(|r| r.contains("GI-FPU-001")),
2687            "imported f64 global access must flag: {:?}",
2688            by_name("fget").unsupported
2689        );
2690        assert!(
2691            by_name("iget").unsupported.is_none(),
2692            "defined i32 global at shifted index 1 must NOT flag: {:?}",
2693            by_name("iget").unsupported
2694        );
2695    }
2696
2697    #[test]
2698    fn test_680_simd_ops_flag_function_unsupported_not_dropped() {
2699        // #680: SIMD (v128) ops decode into WasmOp variants no production
2700        // target can select (`has_helium` is test-only), so they were silently
2701        // dropped at selection — `i32x4.add` compiled to an operand
2702        // passthrough (`mov r0,r1`) and shipped a wrong result. The issue's
2703        // exact reproducer must flag the function; the scalar sibling must
2704        // stay compilable (non-vacuity).
2705        let wat = r#"
2706            (module
2707                (memory 1)
2708                (func (export "vadd") (param i32 i32) (result i32)
2709                    (i32x4.extract_lane 2
2710                        (i32x4.add (i32x4.splat (local.get 0))
2711                                   (i32x4.splat (local.get 1)))))
2712                (func (export "vstore") (param i32 i32) (result i32)
2713                    (v128.store (i32.const 0) (i32x4.splat (local.get 0)))
2714                    (i32.load (i32.const 0)))
2715                (func (export "iadd") (param i32 i32) (result i32)
2716                    local.get 0 local.get 1 i32.add))
2717        "#;
2718        let wasm = wat::parse_str(wat).expect("parse");
2719
2720        // Both decode entry points must flag (the CLI compiles through both).
2721        let module = decode_wasm_module(&wasm).expect("decode module");
2722        for functions in [
2723            &module.functions,
2724            &decode_wasm_functions(&wasm).expect("decode fns"),
2725        ] {
2726            let by_name = |n: &str| {
2727                functions
2728                    .iter()
2729                    .find(|f| f.export_name.as_deref() == Some(n))
2730                    .unwrap()
2731            };
2732            for name in ["vadd", "vstore"] {
2733                let reason = by_name(name).unsupported.as_deref();
2734                assert!(
2735                    reason.is_some(),
2736                    "{name}: v128 ops must flag the function (loud-skip), got None"
2737                );
2738                let reason = reason.unwrap();
2739                assert!(
2740                    reason.contains("no SIMD lowering for this target") && reason.contains("#680"),
2741                    "{name}: diagnostic must name the target gap and #680: {reason:?}"
2742                );
2743            }
2744            // The reason names the FIRST SIMD op hit (splat in both bodies).
2745            assert!(
2746                by_name("vadd")
2747                    .unsupported
2748                    .as_deref()
2749                    .unwrap()
2750                    .contains("I32x4Splat"),
2751                "diagnostic should name the op: {:?}",
2752                by_name("vadd").unsupported
2753            );
2754            assert!(
2755                by_name("iadd").unsupported.is_none(),
2756                "a scalar function in the same module must NOT be flagged: {:?}",
2757                by_name("iadd").unsupported
2758            );
2759        }
2760    }
2761
2762    #[test]
2763    fn test_680_v128_local_and_signature_flag_function() {
2764        // #680: v128 VALUES are expressible with ZERO SIMD-proposal operators
2765        // in the body — a v128-typed local or a v128 param/result is reached
2766        // through type-agnostic `local.get`/`local.set`, which the selectors
2767        // lower as 4-byte moves (silent 16-byte truncation). Both must flag.
2768        let wat = r#"
2769            (module
2770                (func (export "vlocal") (result i32) (local v128)
2771                    i32.const 7)
2772                (func (export "vpass") (param v128) (result v128)
2773                    local.get 0)
2774                (func (export "scalar") (param i32) (result i32)
2775                    local.get 0))
2776        "#;
2777        let wasm = wat::parse_str(wat).expect("parse");
2778        let module = decode_wasm_module(&wasm).expect("decode module");
2779        for functions in [
2780            &module.functions,
2781            &decode_wasm_functions(&wasm).expect("decode fns"),
2782        ] {
2783            let by_name = |n: &str| {
2784                functions
2785                    .iter()
2786                    .find(|f| f.export_name.as_deref() == Some(n))
2787                    .unwrap()
2788            };
2789            assert!(
2790                by_name("vlocal")
2791                    .unsupported
2792                    .as_deref()
2793                    .is_some_and(|r| r.contains("v128-typed local") && r.contains("#680")),
2794                "a v128-typed local declaration must flag: {:?}",
2795                by_name("vlocal").unsupported
2796            );
2797            assert!(
2798                by_name("vpass")
2799                    .unsupported
2800                    .as_deref()
2801                    .is_some_and(|r| r.contains("v128 param/result") && r.contains("#680")),
2802                "a v128 param/result signature must flag (op-free body!): {:?}",
2803                by_name("vpass").unsupported
2804            );
2805            assert!(
2806                by_name("scalar").unsupported.is_none(),
2807                "a scalar function must NOT be flagged: {:?}",
2808                by_name("scalar").unsupported
2809            );
2810        }
2811    }
2812
2813    #[test]
2814    fn test_680_v128_global_access_flags_function() {
2815        // #680: `global.get`/`global.set` on a v128-typed global decode fine
2816        // (type-agnostic ops), but the access would move 4 of the 16 bytes and
2817        // the `v128.const` initializer is never captured. Same lane as the
2818        // float globals (#648/GI-FPU-001); imported globals shift the index
2819        // space (imports first). The i32-global sibling stays compilable.
2820        let wat = r#"
2821            (module
2822                (import "env" "vg" (global v128))
2823                (global $ig (mut i32) (i32.const 7))
2824                (global $dg (mut v128) (v128.const i32x4 1 2 3 4))
2825                (func (export "vget") global.get 0 drop)
2826                (func (export "iget") (result i32) global.get $ig))
2827        "#;
2828        let wasm = wat::parse_str(wat).expect("parse");
2829        let module = decode_wasm_module(&wasm).expect("decode module");
2830        for functions in [
2831            &module.functions,
2832            &decode_wasm_functions(&wasm).expect("decode fns"),
2833        ] {
2834            let by_name = |n: &str| {
2835                functions
2836                    .iter()
2837                    .find(|f| f.export_name.as_deref() == Some(n))
2838                    .unwrap()
2839            };
2840            let reason = by_name("vget").unsupported.as_deref();
2841            assert!(
2842                reason.is_some_and(|r| r.contains("GlobalGet")
2843                    && r.contains("v128-typed global")
2844                    && r.contains("#680")),
2845                "global.get of an imported v128 global must flag: {reason:?}"
2846            );
2847            assert!(
2848                by_name("iget").unsupported.is_none(),
2849                "an i32 global access must NOT be flagged: {:?}",
2850                by_name("iget").unsupported
2851            );
2852        }
2853    }
2854
2855    #[test]
2856    fn test_decode_simd_multiple_ops() {
2857        let wat = r#"
2858            (module
2859                (func (export "simd_ops") (param v128 v128 v128) (result v128)
2860                    ;; (a + b) * c
2861                    local.get 0
2862                    local.get 1
2863                    i32x4.add
2864                    local.get 2
2865                    i32x4.mul
2866                )
2867            )
2868        "#;
2869
2870        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2871        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2872
2873        assert_eq!(functions.len(), 1);
2874        let ops = &functions[0].ops;
2875        assert!(ops.contains(&WasmOp::I32x4Add));
2876        assert!(ops.contains(&WasmOp::I32x4Mul));
2877    }
2878
2879    /// VCR-DBG-001 step 1 (#394): the decoder records a module-relative wasm byte
2880    /// offset per emitted op — the DWARF-for-wasm address space that bridges
2881    /// synth's op-index `source_line` to the input wasm's `.debug_line`. Purely
2882    /// additive metadata (no codegen consumer ⇒ frozen fixtures byte-identical,
2883    /// verified separately); this test pins the structural invariants.
2884    #[test]
2885    fn test_decode_records_aligned_increasing_op_offsets_dbg001() {
2886        let wat = r#"
2887            (module
2888                (func (export "f") (param i32 i32) (result i32)
2889                    local.get 0
2890                    local.get 1
2891                    i32.add
2892                    i32.const 7
2893                    i32.mul))
2894        "#;
2895        let wasm = wat::parse_str(wat).expect("parse WAT");
2896        let functions = decode_wasm_functions(&wasm).expect("decode");
2897        let f = &functions[0];
2898
2899        // One offset per emitted op, index-aligned with `ops`.
2900        assert_eq!(
2901            f.op_offsets.len(),
2902            f.ops.len(),
2903            "op_offsets must be parallel to ops"
2904        );
2905        assert!(!f.op_offsets.is_empty());
2906
2907        // Byte offsets are strictly increasing through the body (each op consumes
2908        // at least one byte) and module-relative (well past the header).
2909        assert!(
2910            f.op_offsets.windows(2).all(|w| w[1] > w[0]),
2911            "wasm byte offsets must strictly increase: {:?}",
2912            f.op_offsets
2913        );
2914        assert!(
2915            f.op_offsets[0] >= 8,
2916            "module-relative offset is past the 8-byte wasm header"
2917        );
2918    }
2919
2920    /// #237: the decoder captures a global's `i32.const` initializer + mutability,
2921    /// so the native-pointer ABI can recognize the stack-pointer global.
2922    #[test]
2923    fn test_decode_captures_global_initializer() {
2924        let wat = r#"
2925            (module
2926                (memory 2)
2927                (global $__stack_pointer (mut i32) (i32.const 65536))
2928                (global $immutable_const i32 (i32.const 7))
2929                (func (export "f") (result i32) global.get 0)
2930            )
2931        "#;
2932        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2933        let module = decode_wasm_module(&wasm).expect("Failed to decode");
2934
2935        assert_eq!(module.globals.len(), 2, "both globals captured");
2936        let sp = &module.globals[0];
2937        assert_eq!(sp.index, 0);
2938        assert_eq!(
2939            sp.init,
2940            Some(GlobalInit::I32(65536)),
2941            "stack-pointer init captured"
2942        );
2943        assert!(sp.mutable, "stack pointer is mutable");
2944        let c = &module.globals[1];
2945        assert_eq!(c.init, Some(GlobalInit::I32(7)));
2946        assert!(!c.mutable, "second global is immutable");
2947        assert_eq!(sp.slot_bytes, 4, "i32 global occupies one 4-byte slot");
2948        assert_eq!(c.slot_bytes, 4);
2949    }
2950
2951    /// #643: the decoder records the DECLARED slot width per global — an i64
2952    /// (or f64) global occupies 8 bytes, so the globals-table layout can give
2953    /// it room for both words and shift every later global's offset.
2954    #[test]
2955    fn test_decode_records_global_slot_widths_643() {
2956        let wat = r#"
2957            (module
2958                (global $c (mut i64) (i64.const 0))
2959                (global $k (mut i32) (i32.const 0))
2960                (global $f (mut f64) (f64.const 0))
2961                (func (export "f") (result i32) global.get 1)
2962            )
2963        "#;
2964        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2965        let module = decode_wasm_module(&wasm).expect("Failed to decode");
2966
2967        assert_eq!(module.globals.len(), 3);
2968        assert_eq!(module.globals[0].slot_bytes, 8, "i64 global is 8 bytes");
2969        assert_eq!(module.globals[1].slot_bytes, 4, "i32 global is 4 bytes");
2970        assert_eq!(module.globals[2].slot_bytes, 8, "f64 global is 8 bytes");
2971    }
2972
2973    /// #649: a nonzero `i64.const` initializer is captured as BOTH words — the
2974    /// `init_i32`-shaped capture dropped it to `None` and every consumer's
2975    /// `unwrap_or(0)` silently ZEROED the global. f32/f64 inits stay `None`
2976    /// (GI-FPU-001/#369 loud-skip lane — never fabricate a float bit-pattern).
2977    #[test]
2978    fn test_decode_captures_i64_global_initializer_649() {
2979        let wat = r#"
2980            (module
2981                (global $g (mut i64) (i64.const 0x123456789ABCDEF0))
2982                (global $n (mut i64) (i64.const -1))
2983                (global $f (mut f64) (f64.const 1.5))
2984                (global $h (mut f32) (f32.const 2.5))
2985                (func (export "f") (result i32) i32.const 0)
2986            )
2987        "#;
2988        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2989        let module = decode_wasm_module(&wasm).expect("Failed to decode");
2990
2991        assert_eq!(module.globals.len(), 4);
2992        assert_eq!(
2993            module.globals[0].init,
2994            Some(GlobalInit::I64(0x123456789ABCDEF0u64 as i64)),
2995            "nonzero i64 init captured with both words"
2996        );
2997        assert_eq!(module.globals[1].init, Some(GlobalInit::I64(-1)));
2998        assert_eq!(
2999            module.globals[2].init, None,
3000            "f64 init is NOT captured (GI-FPU-001 loud-skip lane)"
3001        );
3002        assert_eq!(
3003            module.globals[3].init, None,
3004            "f32 init is NOT captured (GI-FPU-001 loud-skip lane)"
3005        );
3006    }
3007
3008    /// #509: the decoder records `(param_count, result_count)` for every
3009    /// `Block`/`Loop`/`If`, ordinal-keyed in op order, covering all three
3010    /// blocktype encodings: `Empty → (0,0)`, `ValType → (0,1)`, and
3011    /// `FuncType(i) →` counts from the type section (here a multi-result
3012    /// block, which wat encodes as a functype blocktype).
3013    #[test]
3014    fn test_decode_records_block_arity_side_table_509() {
3015        let wat = r#"
3016            (module
3017                (func (export "f") (param i32) (result i32)
3018                    (block (result i32)
3019                        (block (nop))
3020                        (local.get 0)
3021                        (if (result i32)
3022                            (then (i32.const 1))
3023                            (else (i32.const 2)))))
3024                (func (export "g") (result i32)
3025                    (block (result i32 i32)
3026                        (i32.const 1) (i32.const 2))
3027                    i32.add)
3028                (func (export "h") (param i32) (result i32)
3029                    (local.get 0)
3030                    (loop (param i32) (result i32))))
3031        "#;
3032        let wasm = wat::parse_str(wat).expect("parse WAT");
3033
3034        // Both decode entry points must produce the same side-table.
3035        for functions in [
3036            decode_wasm_functions(&wasm).expect("decode"),
3037            decode_wasm_module(&wasm).expect("decode").functions,
3038        ] {
3039            // f: Block(result i32), Block(void), If(result i32) — in op order.
3040            assert_eq!(
3041                functions[0].block_arity,
3042                vec![(0, 1), (0, 0), (0, 1)],
3043                "f: ValType/Empty/ValType blocktypes"
3044            );
3045            // g: one multi-result block via a FuncType blocktype.
3046            assert_eq!(
3047                functions[1].block_arity,
3048                vec![(0, 2)],
3049                "g: functype blocktype result count from the type section"
3050            );
3051            // h: a parameterized loop — the input arity is what a br to the
3052            // header would carry (the #509 loud-decline discriminator).
3053            assert_eq!(
3054                functions[2].block_arity,
3055                vec![(1, 1)],
3056                "h: loop params captured"
3057            );
3058        }
3059    }
3060
3061    /// #642: the decoder captures table 0's compile-time size, per-segment
3062    /// element shapes and per-function type indices, and the closed-world
3063    /// verdict VERIFIES a fully-covered homogeneous table.
3064    #[test]
3065    fn test_call_indirect_guards_closed_world_verified_642() {
3066        // The #642 repro shape: 3-entry table, fully covered, one signature.
3067        let wat = r#"
3068            (module
3069                (type $bin (func (param i32 i32) (result i32)))
3070                (table 3 funcref)
3071                (elem (i32.const 0) $add $sub $mul)
3072                (func $add (param i32 i32) (result i32)
3073                    (i32.add (local.get 0) (local.get 1)))
3074                (func $sub (param i32 i32) (result i32)
3075                    (i32.sub (local.get 0) (local.get 1)))
3076                (func $mul (param i32 i32) (result i32)
3077                    (i32.mul (local.get 0) (local.get 1)))
3078                (func (export "f") (param i32 i32) (result i32)
3079                    (call_indirect (type $bin)
3080                        (local.get 0) (i32.const 10) (local.get 1)))
3081            )
3082        "#;
3083        let wasm = wat::parse_str(wat).expect("parse");
3084        let module = decode_wasm_module(&wasm).expect("decode");
3085
3086        assert_eq!(module.table_size, Some(3), "table section min size");
3087        assert_eq!(module.table_sizes, vec![Some(3)], "#650 per-table sizes");
3088        assert_eq!(
3089            module.elem_segments,
3090            vec![ElemSegmentInfo {
3091                table_index: 0,
3092                offset: Some(0),
3093                funcs: Some(vec![0, 1, 2]),
3094            }]
3095        );
3096        // 2 type-section entries ($bin + the export's (i32 i32)->i32 dedups
3097        // to one in practice, but don't assume — just check func 0..2 share
3098        // a signature with type 0).
3099        assert_eq!(module.func_type_indices.len(), 4);
3100
3101        let guards = module.call_indirect_guards();
3102        assert_eq!(guards.tables.len(), 1);
3103        assert_eq!(guards.tables[0].table_size, Some(3));
3104        assert_eq!(
3105            guards.tables[0].base_byte_offset,
3106            Some(0),
3107            "#650: a single-table module keeps table 0 at R11 offset 0 by construction"
3108        );
3109        // Type index 0 ($bin) must be VERIFIED: every table entry has its
3110        // exact signature.
3111        assert_eq!(
3112            guards.tables[0].type_reject.first(),
3113            Some(&None),
3114            "closed-world type check must verify the homogeneous table: {:?}",
3115            guards.tables[0].type_reject
3116        );
3117        assert!(
3118            !guards.tables[0].has_null_slots,
3119            "#664: a fully-initialized table must NOT request the runtime \
3120             null check (dispatch bytes stay identical by construction)"
3121        );
3122    }
3123
3124    /// #642: a heterogeneous table (an entry whose signature differs from the
3125    /// expected type) must REJECT that expected type — the raw code-pointer
3126    /// table cannot be runtime-type-checked, so the lowering has to decline.
3127    #[test]
3128    fn test_call_indirect_guards_heterogeneous_table_rejects_642() {
3129        let wat = r#"
3130            (module
3131                (type $bin (func (param i32 i32) (result i32)))
3132                (type $un (func (param i32) (result i32)))
3133                (table 2 funcref)
3134                (elem (i32.const 0) $add $neg)
3135                (func $add (type $bin)
3136                    (i32.add (local.get 0) (local.get 1)))
3137                (func $neg (type $un)
3138                    (i32.sub (i32.const 0) (local.get 0)))
3139                (func (export "f") (param i32 i32) (result i32)
3140                    (call_indirect (type $bin)
3141                        (local.get 0) (i32.const 10) (local.get 1)))
3142            )
3143        "#;
3144        let wasm = wat::parse_str(wat).expect("parse");
3145        let module = decode_wasm_module(&wasm).expect("decode");
3146        let guards = module.call_indirect_guards();
3147        assert_eq!(guards.tables[0].table_size, Some(2));
3148        // BOTH expected types must be rejected: the table holds one function
3149        // of each signature, so neither type's closed world holds.
3150        assert!(
3151            guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3152            "heterogeneous table must reject every expected type: {:?}",
3153            guards.tables[0].type_reject
3154        );
3155        // #676: ... but the image is statically known, so the mismatch trap
3156        // is dischargeable at RUNTIME via the type-id sidecar.
3157        assert!(
3158            guards.tables[0].runtime_type_check,
3159            "heterogeneous-but-known table must offer the runtime check (#676)"
3160        );
3161        assert_eq!(
3162            guards.type_ids_byte_offset,
3163            Some(8),
3164            "sidecar sits after the 2-slot pointer region"
3165        );
3166        assert_eq!(
3167            guards.type_ids_image,
3168            vec![1, 2],
3169            "slot 0 = $bin (class 1), slot 1 = $un (class 2)"
3170        );
3171        assert_eq!(guards.type_class_ids, vec![1, 2]);
3172    }
3173
3174    /// #664 (relaxes the #642 all-reject): an uninitialized table slot (elem
3175    /// covers less than the declared size) is a null funcref — calling it
3176    /// must trap, which is now discharged at RUNTIME (null check on the
3177    /// zero-linked pointer), so the closed-world verdict verifies the
3178    /// INITIALIZED slots and sets `has_null_slots` for the lowering.
3179    #[test]
3180    fn test_call_indirect_guards_null_slot_verifies_with_flag_664() {
3181        let wat = r#"
3182            (module
3183                (type $s (func (result i32)))
3184                (table 3 funcref)
3185                (elem (i32.const 0) $f0 $f1)
3186                (func $f0 (result i32) (i32.const 10))
3187                (func $f1 (result i32) (i32.const 11))
3188                (func (export "run") (param i32) (result i32)
3189                    (call_indirect (type $s) (local.get 0)))
3190            )
3191        "#;
3192        let wasm = wat::parse_str(wat).expect("parse");
3193        let module = decode_wasm_module(&wasm).expect("decode");
3194        let guards = module.call_indirect_guards();
3195        assert_eq!(guards.tables[0].table_size, Some(3));
3196        assert_eq!(
3197            guards.tables[0].type_reject.first(),
3198            Some(&None),
3199            "initialized slots are homogeneous in $s — the verdict must \
3200             verify despite the null slot (#664): {:?}",
3201            guards.tables[0].type_reject
3202        );
3203        assert!(
3204            guards.tables[0].has_null_slots,
3205            "slot 2 is uninitialized — the lowering must emit the runtime \
3206             null check (#664)"
3207        );
3208    }
3209
3210    /// #664: the falcon shape — a SPARSE table (only slots 1 and 3 of 4
3211    /// initialized, by two separate segments) verifies with the null flag;
3212    /// a sparse table whose INITIALIZED slots are heterogeneous still
3213    /// rejects (the runtime null check cannot discharge a TYPE mismatch).
3214    #[test]
3215    fn test_call_indirect_guards_sparse_table_664() {
3216        let wat = r#"
3217            (module
3218                (type $t (func (param i32) (result i32)))
3219                (table 4 4 funcref)
3220                (func $f1 (type $t) (i32.add (local.get 0) (i32.const 100)))
3221                (func $f3 (type $t) (i32.sub (i32.const 1000) (local.get 0)))
3222                (elem (i32.const 1) $f1)
3223                (elem (i32.const 3) $f3)
3224                (func (export "via") (param i32 i32) (result i32)
3225                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3226            )
3227        "#;
3228        let wasm = wat::parse_str(wat).expect("parse");
3229        let module = decode_wasm_module(&wasm).expect("decode");
3230        let guards = module.call_indirect_guards();
3231        assert_eq!(guards.tables[0].table_size, Some(4));
3232        assert_eq!(
3233            guards.tables[0].type_reject.first(),
3234            Some(&None),
3235            "slots 1,3 are homogeneous in $t — verified: {:?}",
3236            guards.tables[0].type_reject
3237        );
3238        assert!(guards.tables[0].has_null_slots, "slots 0,2 are null");
3239
3240        // Heterogeneous INITIALIZED slots in a sparse table: still rejected.
3241        let wat = r#"
3242            (module
3243                (type $t (func (param i32) (result i32)))
3244                (type $u (func (param i32 i32) (result i32)))
3245                (table 4 4 funcref)
3246                (func $f1 (type $t) (local.get 0))
3247                (func $f3 (type $u) (i32.add (local.get 0) (local.get 1)))
3248                (elem (i32.const 1) $f1)
3249                (elem (i32.const 3) $f3)
3250                (func (export "via") (param i32 i32) (result i32)
3251                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3252            )
3253        "#;
3254        let wasm = wat::parse_str(wat).expect("parse");
3255        let module = decode_wasm_module(&wasm).expect("decode");
3256        let guards = module.call_indirect_guards();
3257        assert!(
3258            guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3259            "a heterogeneous sparse table must still reject every type: {:?}",
3260            guards.tables[0].type_reject
3261        );
3262        // #676: the sparse-heterogeneous case is now dischargeable at
3263        // runtime too — null slots take the reserved class id 0, so ONE
3264        // sidecar compare covers both the type mismatch and the null trap.
3265        assert!(guards.tables[0].runtime_type_check, "#676 runtime check");
3266        assert_eq!(guards.type_ids_byte_offset, Some(16), "4 pointer slots");
3267        assert_eq!(
3268            guards.type_ids_image,
3269            vec![0, 1, 0, 2],
3270            "nulls at 0/2 carry the reserved id 0; $t slot 1 = class 1, \
3271             $u slot 3 = class 2"
3272        );
3273    }
3274
3275    /// #676: the heterogeneous type-id sidecar — structural duplicate types
3276    /// share one class id (the meld 31-decls/25-distinct shape), null slots
3277    /// take the reserved id 0, and the sidecar base is the total pointer
3278    /// region size. A module with NO heterogeneous table gets NO sidecar
3279    /// (empty image, `None` offset) — homogeneous modules stay untouched.
3280    #[test]
3281    fn test_call_indirect_guards_heterogeneous_sidecar_676() {
3282        let wat = r#"
3283            (module
3284                (type $bin (func (param i32 i32) (result i32)))
3285                (type $un (func (param i32) (result i32)))
3286                (type $bin2 (func (param i32 i32) (result i32)))
3287                (table 5 5 funcref)
3288                (func $add (type $bin) (i32.add (local.get 0) (local.get 1)))
3289                (func $neg (type $un) (i32.sub (i32.const 0) (local.get 0)))
3290                (func $sub (type $bin2) (i32.sub (local.get 0) (local.get 1)))
3291                (elem (i32.const 0) func $add $neg $sub)
3292                (func (export "via2") (param i32 i32) (result i32)
3293                    (call_indirect (type $bin)
3294                        (local.get 0) (i32.const 3) (local.get 1)))
3295                (func (export "via1") (param i32 i32) (result i32)
3296                    (call_indirect (type $un) (local.get 0) (local.get 1)))
3297            )
3298        "#;
3299        let wasm = wat::parse_str(wat).expect("parse");
3300        let module = decode_wasm_module(&wasm).expect("decode");
3301        let guards = module.call_indirect_guards();
3302        assert!(guards.tables[0].runtime_type_check);
3303        assert_eq!(
3304            guards.type_class_ids,
3305            vec![1, 2, 1],
3306            "$bin2 is a structural duplicate of $bin — one class id (#676)"
3307        );
3308        assert_eq!(
3309            guards.type_ids_image,
3310            vec![1, 2, 1, 0, 0],
3311            "slots: $add(bin)=1, $neg(un)=2, $sub(bin2 ≡ bin)=1, null, null"
3312        );
3313        assert_eq!(
3314            guards.type_ids_byte_offset,
3315            Some(20),
3316            "sidecar starts after the 5 pointer words"
3317        );
3318
3319        // Homogeneous module → NO sidecar, no runtime check anywhere.
3320        let wat = r#"
3321            (module
3322                (type $t (func (param i32) (result i32)))
3323                (table 2 2 funcref)
3324                (func $f0 (type $t) (local.get 0))
3325                (func $f1 (type $t) (i32.const 7))
3326                (elem (i32.const 0) func $f0 $f1)
3327                (func (export "via") (param i32 i32) (result i32)
3328                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3329            )
3330        "#;
3331        let wasm = wat::parse_str(wat).expect("parse");
3332        let module = decode_wasm_module(&wasm).expect("decode");
3333        let guards = module.call_indirect_guards();
3334        assert!(!guards.tables[0].runtime_type_check);
3335        assert_eq!(guards.type_ids_byte_offset, None, "no heterogeneous table");
3336        assert!(guards.type_ids_image.is_empty());
3337        assert!(guards.type_class_ids.is_empty());
3338    }
3339
3340    /// #642: no table at all → no compile-time bound → table_size None and
3341    /// every type rejected (the lowering declines).
3342    #[test]
3343    fn test_call_indirect_guards_no_table_642() {
3344        let wat = r#"
3345            (module
3346                (func (export "f") (param i32) (result i32) (local.get 0))
3347            )
3348        "#;
3349        let wasm = wat::parse_str(wat).expect("parse");
3350        let module = decode_wasm_module(&wasm).expect("decode");
3351        assert_eq!(module.table_size, None);
3352        assert!(module.table_sizes.is_empty(), "#650: no tables declared");
3353        let guards = module.call_indirect_guards();
3354        assert!(
3355            guards.tables.is_empty(),
3356            "no table → no guard entry → every call_indirect declines"
3357        );
3358    }
3359
3360    /// #642: duplicate-but-structurally-identical types stay interchangeable —
3361    /// the closed-world check compares SIGNATURES, not type indices.
3362    #[test]
3363    fn test_call_indirect_guards_duplicate_types_verified_642() {
3364        let wat = r#"
3365            (module
3366                (type $a (func (result i32)))
3367                (type $b (func (result i32)))
3368                (table 1 funcref)
3369                (elem (i32.const 0) $f)
3370                (func $f (type $a) (i32.const 7))
3371                (func (export "run") (param i32) (result i32)
3372                    (call_indirect (type $b) (local.get 0)))
3373            )
3374        "#;
3375        let wasm = wat::parse_str(wat).expect("parse");
3376        let module = decode_wasm_module(&wasm).expect("decode");
3377        let guards = module.call_indirect_guards();
3378        // $f has type $a; the call expects $b — structurally identical, so
3379        // BOTH type indices must verify. (A third type — the export's
3380        // (i32)->i32 — is correctly rejected: different signature.)
3381        assert_eq!(
3382            &guards.tables[0].type_reject[0..2],
3383            &[None, None],
3384            "structural signature comparison must accept duplicate types: {:?}",
3385            guards.tables[0].type_reject
3386        );
3387        assert!(
3388            guards.tables[0].type_reject[2].is_some(),
3389            "the structurally-different third type must still be rejected"
3390        );
3391    }
3392
3393    /// #650: TWO tables become a contiguous R11 region — table 0 at offset 0
3394    /// (byte-identical single-table degeneration), table 1 at
3395    /// `size(table 0) * 4`. Each table gets its OWN size, base offset, and
3396    /// per-type closed-world verdicts (segments only poison the table they
3397    /// target).
3398    #[test]
3399    fn test_call_indirect_guards_multi_table_650() {
3400        // The #650 repro shape: overlapping indices, distinct functions —
3401        // table0[1] != table1[1] (the aliasing canary).
3402        let wat = r#"
3403            (module
3404                (type $t (func (param i32) (result i32)))
3405                (type $u (func (param i32 i32) (result i32)))
3406                (table $t0 3 3 funcref)
3407                (table $t1 2 2 funcref)
3408                (func $a0 (type $t) (i32.add (local.get 0) (i32.const 100)))
3409                (func $a1 (type $t) (i32.add (local.get 0) (i32.const 200)))
3410                (func $a2 (type $t) (i32.add (local.get 0) (i32.const 300)))
3411                (func $b0 (type $u) (i32.add (local.get 0) (local.get 1)))
3412                (func $b1 (type $u) (i32.sub (local.get 0) (local.get 1)))
3413                (elem (table $t0) (i32.const 0) func $a0 $a1 $a2)
3414                (elem (table $t1) (i32.const 0) func $b0 $b1)
3415                (func (export "f") (param i32 i32) (result i32)
3416                    (call_indirect $t1 (type $u)
3417                        (local.get 0) (i32.const 10) (local.get 1)))
3418            )
3419        "#;
3420        let wasm = wat::parse_str(wat).expect("parse");
3421        let module = decode_wasm_module(&wasm).expect("decode");
3422        assert_eq!(module.table_sizes, vec![Some(3), Some(2)]);
3423        assert_eq!(module.table_size, Some(3), "compat accessor = table 0");
3424        assert_eq!(
3425            module.elem_segments[0].table_index, 0,
3426            "segment 0 targets table 0"
3427        );
3428        assert_eq!(
3429            module.elem_segments[1],
3430            ElemSegmentInfo {
3431                table_index: 1,
3432                offset: Some(0),
3433                funcs: Some(vec![3, 4]),
3434            },
3435            "segment 1 is statically attributed to table 1 (#650)"
3436        );
3437
3438        let guards = module.call_indirect_guards();
3439        assert_eq!(guards.tables.len(), 2);
3440        assert_eq!(guards.tables[0].table_size, Some(3));
3441        assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3442        assert_eq!(guards.tables[1].table_size, Some(2));
3443        assert_eq!(
3444            guards.tables[1].base_byte_offset,
3445            Some(12),
3446            "table 1 base = size(table 0) * 4 within the contiguous R11 region"
3447        );
3448        // Table 0 is homogeneous in $t (type 0); table 1 in $u (type 1) —
3449        // each verifies ITS type and rejects the other's.
3450        assert_eq!(guards.tables[0].type_reject[0], None, "table 0 vs $t");
3451        assert!(guards.tables[0].type_reject[1].is_some(), "table 0 vs $u");
3452        assert!(guards.tables[1].type_reject[0].is_some(), "table 1 vs $t");
3453        assert_eq!(guards.tables[1].type_reject[1], None, "table 1 vs $u");
3454    }
3455
3456    /// #650: an unknown-size table (growable import) declines ITSELF and
3457    /// makes every LATER table's base offset non-constant — but a table
3458    /// BEFORE it is unaffected.
3459    #[test]
3460    fn test_call_indirect_guards_unknown_size_poisons_later_bases_650() {
3461        let wat = r#"
3462            (module
3463                (type $t (func (result i32)))
3464                (import "env" "tbl" (table 4 funcref))
3465                (table $d 1 1 funcref)
3466                (func $f (type $t) (i32.const 7))
3467                (elem (table $d) (i32.const 0) func $f)
3468                (func (export "run") (param i32) (result i32)
3469                    (call_indirect $d (type $t) (local.get 0)))
3470            )
3471        "#;
3472        let wasm = wat::parse_str(wat).expect("parse");
3473        let module = decode_wasm_module(&wasm).expect("decode");
3474        assert_eq!(
3475            module.table_sizes,
3476            vec![None, Some(1)],
3477            "growable import (no max) has no sound compile-time size"
3478        );
3479        let guards = module.call_indirect_guards();
3480        assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3481        assert!(
3482            guards.tables[0].type_reject.iter().all(|r| r.is_some()),
3483            "unknown-size table rejects every type"
3484        );
3485        assert_eq!(
3486            guards.tables[1].base_byte_offset, None,
3487            "a later table's base is not a compile-time constant when a \
3488             preceding table's size is unknown (#650)"
3489        );
3490        assert_eq!(guards.tables[1].table_size, Some(1));
3491    }
3492}