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