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