Skip to main content

synth_core/
wasm_decoder.rs

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