Skip to main content

synth_core/
wasm_decoder.rs

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