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