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 WASM global's declaration — its initial value and mutability (#237).
51/// Needed so the native-pointer ABI can recognize a global whose initializer is
52/// a linear-memory address (e.g. `$__stack_pointer = 65536`) and make it
53/// `__synth_wasm_data`-relative, rather than reading it from an R9 globals table
54/// the self-contained drop-in object can't rely on.
55#[derive(Debug, Clone)]
56pub struct WasmGlobal {
57    /// Global index (defined globals; imported globals are not counted here).
58    pub index: u32,
59    /// The `i32.const` initializer value (other init exprs decode to `None`).
60    pub init_i32: Option<i32>,
61    /// Whether the global is mutable.
62    pub mutable: bool,
63}
64
65impl WasmMemory {
66    /// Get initial size in bytes
67    pub fn initial_bytes(&self) -> u32 {
68        self.initial_pages * 65536
69    }
70
71    /// Get maximum size in bytes (or initial if not specified)
72    pub fn max_bytes(&self) -> u32 {
73        self.max_pages.unwrap_or(self.initial_pages) * 65536
74    }
75}
76
77/// Decoded WASM module with functions and memory
78#[derive(Debug, Clone)]
79pub struct DecodedModule {
80    /// Decoded functions
81    pub functions: Vec<FunctionOps>,
82    /// Linear memories
83    pub memories: Vec<WasmMemory>,
84    /// Data segments (offset, data) for memory initialization
85    pub data_segments: Vec<(u32, Vec<u8>)>,
86    /// Import entries (module name, field name, kind)
87    pub imports: Vec<ImportEntry>,
88    /// Number of imported functions (for distinguishing import calls from local calls)
89    pub num_imported_funcs: u32,
90    /// AAPCS integer-argument count per function, indexed by the *full* WASM
91    /// function index (imported functions first, then locally-defined ones).
92    /// Used by the backend to marshal call arguments into R0–R3 (issue #195).
93    /// Counts every parameter as one slot (i64/f64 over-counted — see the
94    /// backend's `set_func_arg_counts` scope note).
95    pub func_arg_counts: Vec<u32>,
96    /// AAPCS integer-argument count per *function type*, indexed by type index.
97    /// Used by `call_indirect`, whose callee arg count comes from the static
98    /// type index (issue #195).
99    pub type_arg_counts: Vec<u32>,
100    /// #311: whether each *function* (full index, imports first) returns i64 —
101    /// the call lowering must tag the result as a register PAIR (r0:r1) or the
102    /// hi half is invisible to liveness and the next constant clobbers it.
103    pub func_ret_i64: Vec<bool>,
104    /// #311: whether each *function type* returns i64 (for `call_indirect`).
105    pub type_ret_i64: Vec<bool>,
106    /// #359: declared parameter widths per *function* (full index, imports
107    /// first): `func_params_i64[f][k]` is true when param `k` is i64/f64. The
108    /// AAPCS stack-argument path needs the declared widths — op-stream inference
109    /// can't see an unused i64 param that still shifts the incoming-stack layout.
110    pub func_params_i64: Vec<Vec<bool>>,
111    /// Defined globals with their initializers (#237). Empty if the module has
112    /// no global section. Used by the native-pointer ABI to make a global whose
113    /// initializer is a linear-memory address (e.g. `$__stack_pointer`)
114    /// self-contained rather than table-relative.
115    pub globals: Vec<WasmGlobal>,
116    /// Function indices that populate any table via an element segment (#275).
117    /// These are the possible `call_indirect` targets — a function reached only
118    /// through the table is invisible to direct-`call` reachability, so the
119    /// whole-graph closure must treat every table entry as reachable once any
120    /// reachable function performs a `call_indirect`. Empty for modules with no
121    /// element section (every leaf/direct-call module), keeping output identical.
122    pub elem_func_indices: Vec<u32>,
123    /// VCR-PERF-002 Phase 1 (#494): proven invariants from loom's `wsc.facts`
124    /// custom section, keyed by `(function index, value id)` — see
125    /// `docs/design/wsc-facts-encoding.md` (schema v1) and
126    /// [`crate::wsc_facts::parse_wsc_facts`]. FAIL-SAFE by contract (loom#231
127    /// Q4): a missing/unparseable section or unknown version yields the empty
128    /// vec, unknown fact kinds are skipped — never a decode error. Phase 1 is
129    /// ingestion only: NO codegen path consumes these yet, so emitted bytes
130    /// are unchanged whether or not a module carries the section.
131    pub wsc_facts: Vec<crate::wsc_facts::WscFact>,
132}
133
134/// Decode a WASM binary and extract functions, memory, and data segments
135pub fn decode_wasm_module(wasm_bytes: &[u8]) -> Result<DecodedModule> {
136    let mut functions = Vec::new();
137    let mut memories = Vec::new();
138    let mut data_segments = Vec::new();
139    let mut globals: Vec<WasmGlobal> = Vec::new();
140    let mut imports = Vec::new();
141    let mut func_index = 0u32;
142    let mut num_imported_funcs = 0u32;
143    let mut export_names: HashMap<u32, String> = HashMap::new();
144    // #195: per-type AAPCS arg count (indexed by type index) and per-function
145    // arg count (indexed by full function index: imports first, then locals).
146    let mut type_arg_counts: Vec<u32> = Vec::new();
147    let mut func_arg_counts: Vec<u32> = Vec::new();
148    let mut type_ret_i64: Vec<bool> = Vec::new();
149    let mut func_ret_i64: Vec<bool> = Vec::new();
150    // #359: declared param widths per type / per function (full index).
151    let mut type_params_i64: Vec<Vec<bool>> = Vec::new();
152    let mut func_params_i64: Vec<Vec<bool>> = Vec::new();
153    // #509: (param_count, result_count) per type index, for FuncType blocktypes.
154    let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
155    let mut elem_func_indices: Vec<u32> = Vec::new();
156    // #394 Tier-1.x: function index → developer-facing name from the wasm
157    // `name` custom section (function-names subsection). Applied to
158    // `FunctionOps.debug_name` after the parse loop — the custom section
159    // conventionally trails the code section, so the entries are not yet
160    // available when each `CodeSectionEntry` is decoded.
161    let mut name_section_names: HashMap<u32, String> = HashMap::new();
162    // VCR-PERF-002 Phase 1 (#494): facts from loom's `wsc.facts` custom
163    // section. `None` until (and unless) the first such section is seen —
164    // duplicates are ignored (one prover, one section; encoding doc rule).
165    let mut wsc_facts: Option<Vec<crate::wsc_facts::WscFact>> = None;
166
167    for payload in Parser::new(0).parse_all(wasm_bytes) {
168        let payload = payload.context("Failed to parse WASM payload")?;
169
170        match payload {
171            Payload::TypeSection(reader) => {
172                // Record the parameter count of each function type so calls can
173                // marshal the right number of arguments (issue #195).
174                for rec_group in reader {
175                    let rec_group = rec_group.context("Failed to parse type")?;
176                    for sub_ty in rec_group.types() {
177                        // #509: blocktype arity per type index (saturated u8 —
178                        // >255 params/results is far beyond anything the
179                        // selector supports anyway, and the selector declines
180                        // rather than trusting a saturated count).
181                        type_block_arity.push(match &sub_ty.composite_type.inner {
182                            wasmparser::CompositeInnerType::Func(f) => (
183                                u8::try_from(f.params().len()).unwrap_or(u8::MAX),
184                                u8::try_from(f.results().len()).unwrap_or(u8::MAX),
185                            ),
186                            _ => (u8::MAX, u8::MAX),
187                        });
188                        let (count, ret_i64, params_i64) = match &sub_ty.composite_type.inner {
189                            wasmparser::CompositeInnerType::Func(func_ty) => (
190                                func_ty.params().len() as u32,
191                                func_ty
192                                    .results()
193                                    .first()
194                                    .is_some_and(|t| *t == wasmparser::ValType::I64),
195                                // #359: i64/f64 params occupy 8 bytes / a register
196                                // pair under AAPCS. f32/f64 are not in scope for the
197                                // stack-arg path (refused), but mark both 64-bit
198                                // float and i64 so the guard catches them.
199                                func_ty
200                                    .params()
201                                    .iter()
202                                    .map(|t| {
203                                        matches!(
204                                            t,
205                                            wasmparser::ValType::I64 | wasmparser::ValType::F64
206                                        )
207                                    })
208                                    .collect::<Vec<bool>>(),
209                            ),
210                            _ => (0, false, Vec::new()),
211                        };
212                        type_arg_counts.push(count);
213                        type_ret_i64.push(ret_i64);
214                        type_params_i64.push(params_i64);
215                    }
216                }
217            }
218            Payload::ImportSection(reader) => {
219                // wasmparser 0.221+ groups imports (the "compact imports"
220                // proposal): the section reader yields `Imports` groups, each of
221                // which may expand to several `Import`s. `into_imports()`
222                // flattens groups back to individual `Import`s (preserving the
223                // module/name/ty fields), keeping the per-import loop intact.
224                for import in reader.into_imports() {
225                    let import = import.context("Failed to parse import")?;
226                    let (kind, idx) = match import.ty {
227                        wasmparser::TypeRef::Func(type_idx) => {
228                            let idx = num_imported_funcs;
229                            num_imported_funcs += 1;
230                            // Record the imported function's arg count at its
231                            // full function index (imports come first).
232                            func_arg_counts
233                                .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
234                            func_ret_i64.push(
235                                type_ret_i64
236                                    .get(type_idx as usize)
237                                    .copied()
238                                    .unwrap_or(false),
239                            );
240                            func_params_i64.push(
241                                type_params_i64
242                                    .get(type_idx as usize)
243                                    .cloned()
244                                    .unwrap_or_default(),
245                            );
246                            (ImportKind::Function(type_idx), idx)
247                        }
248                        wasmparser::TypeRef::Memory(_) => (ImportKind::Memory, 0),
249                        wasmparser::TypeRef::Table(_) => (ImportKind::Table, 0),
250                        wasmparser::TypeRef::Global(_) => (ImportKind::Global, 0),
251                        _ => continue,
252                    };
253                    imports.push(ImportEntry {
254                        module: import.module.to_string(),
255                        name: import.name.to_string(),
256                        kind,
257                        index: idx,
258                    });
259                }
260            }
261            Payload::FunctionSection(reader) => {
262                // Each entry gives the type index of a locally-defined function,
263                // in order. Their full function indices follow the imports, so
264                // appending to `func_arg_counts` keeps it indexed by full index
265                // (issue #195).
266                for ty in reader {
267                    let type_idx = ty.context("Failed to parse function type index")?;
268                    func_arg_counts
269                        .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
270                    func_ret_i64.push(
271                        type_ret_i64
272                            .get(type_idx as usize)
273                            .copied()
274                            .unwrap_or(false),
275                    );
276                    func_params_i64.push(
277                        type_params_i64
278                            .get(type_idx as usize)
279                            .cloned()
280                            .unwrap_or_default(),
281                    );
282                }
283            }
284            Payload::MemorySection(reader) => {
285                for (idx, memory) in reader.into_iter().enumerate() {
286                    let mem = memory.context("Failed to parse memory")?;
287                    memories.push(WasmMemory {
288                        index: idx as u32,
289                        initial_pages: mem.initial as u32,
290                        max_pages: mem.maximum.map(|m| m as u32),
291                        shared: mem.shared,
292                    });
293                }
294            }
295            Payload::GlobalSection(reader) => {
296                // #237: capture each defined global's i32 initializer + mutability.
297                // The init is a const expr; we only decode a leading `i32.const`
298                // (the shape `$__stack_pointer`/data-layout globals use). Anything
299                // else (global.get, f32/f64, etc.) records `init_i32: None` and is
300                // left to the table-relative path.
301                for (idx, global) in reader.into_iter().enumerate() {
302                    let global = global.context("Failed to parse global")?;
303                    let mut init_i32 = None;
304                    let mut ops = global.init_expr.get_operators_reader();
305                    if let Ok(wasmparser::Operator::I32Const { value }) = ops.read() {
306                        init_i32 = Some(value);
307                    }
308                    globals.push(WasmGlobal {
309                        index: idx as u32,
310                        init_i32,
311                        mutable: global.ty.mutable,
312                    });
313                }
314            }
315            Payload::DataSection(reader) => {
316                for data in reader {
317                    let data = data.context("Failed to parse data segment")?;
318                    if let wasmparser::DataKind::Active {
319                        memory_index: 0,
320                        offset_expr,
321                    } = data.kind
322                    {
323                        let mut ops = offset_expr.get_operators_reader();
324                        if let Ok(wasmparser::Operator::I32Const { value }) = ops.read() {
325                            data_segments.push((value as u32, data.data.to_vec()));
326                        }
327                    }
328                }
329            }
330            Payload::ElementSection(reader) => {
331                // #275: collect every function index that initializes a table.
332                // These are the `call_indirect` targets the direct-call closure
333                // cannot see; `reachable_from_exports` unions them in when a
334                // reachable function does a `call_indirect`. Both element forms
335                // are handled: a flat function-index list, and the const-expr
336                // form whose `ref.func` entries name the functions.
337                for elem in reader {
338                    let elem = elem.context("Failed to parse element segment")?;
339                    match elem.items {
340                        wasmparser::ElementItems::Functions(funcs) => {
341                            for f in funcs {
342                                elem_func_indices
343                                    .push(f.context("Failed to parse element func index")?);
344                            }
345                        }
346                        wasmparser::ElementItems::Expressions(_, exprs) => {
347                            for expr in exprs {
348                                let expr = expr.context("Failed to parse element expr")?;
349                                for op in expr.get_operators_reader() {
350                                    if let wasmparser::Operator::RefFunc { function_index } =
351                                        op.context("Failed to parse element op")?
352                                    {
353                                        elem_func_indices.push(function_index);
354                                    }
355                                }
356                            }
357                        }
358                    }
359                }
360            }
361            Payload::ExportSection(exports) => {
362                for export in exports {
363                    let export = export.context("Failed to parse export")?;
364                    if export.kind == ExternalKind::Func {
365                        export_names.insert(export.index, export.name.to_string());
366                    }
367                }
368            }
369            Payload::CodeSectionEntry(body) => {
370                let (ops, op_offsets, block_arity, unsupported) =
371                    decode_function_body(&body, &type_block_arity)?;
372                let actual_index = num_imported_funcs + func_index;
373                let export_name = export_names.get(&actual_index).cloned();
374
375                functions.push(FunctionOps {
376                    index: actual_index,
377                    export_name,
378                    debug_name: None, // filled from the `name` section after the loop
379                    ops,
380                    op_offsets,
381                    unsupported,
382                    block_arity,
383                });
384                func_index += 1;
385            }
386            Payload::CustomSection(c) => {
387                // #394 Tier-1.x: the wasm `name` custom section.
388                if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
389                    parse_name_section_func_names(reader, &mut name_section_names);
390                }
391                // VCR-PERF-002 Phase 1 (#494): loom's `wsc.facts` section.
392                // `parse_wsc_facts` is TOTAL (fail-safe skew, loom#231 Q4):
393                // any malformed payload decodes to the empty fact list WITH a
394                // stderr diagnostic, never an error — facts are optional
395                // accelerators and must not be able to change a compilation
396                // outcome. First section wins.
397                if c.name() == crate::wsc_facts::WSC_FACTS_SECTION_NAME && wsc_facts.is_none() {
398                    let parsed = crate::wsc_facts::parse_wsc_facts(c.data());
399                    if let Some(reason) = &parsed.section_ignored {
400                        eprintln!(
401                            "warning: ignoring unparseable `wsc.facts` custom section \
402                             ({reason}) — facts are optional accelerators, compilation \
403                             is unaffected (#494 fail-safe skew rule)"
404                        );
405                    } else if parsed.records_skipped > 0 {
406                        eprintln!(
407                            "warning: skipped {} unknown/undecodable `wsc.facts` \
408                             record(s) (likely a newer loom emitter); {} known fact(s) \
409                             kept, compilation is unaffected (#494 fail-safe skew rule)",
410                            parsed.records_skipped,
411                            parsed.facts.len()
412                        );
413                    }
414                    wsc_facts = Some(parsed.facts);
415                }
416            }
417            _ => {}
418        }
419    }
420
421    apply_name_section(&mut functions, &name_section_names);
422
423    Ok(DecodedModule {
424        functions,
425        memories,
426        data_segments,
427        imports,
428        num_imported_funcs,
429        func_arg_counts,
430        type_arg_counts,
431        func_ret_i64,
432        type_ret_i64,
433        func_params_i64,
434        globals,
435        elem_func_indices,
436        wsc_facts: wsc_facts.unwrap_or_default(),
437    })
438}
439
440/// Parse the function-names subsection of a wasm `name` custom section into
441/// `out` (function index → developer-facing name, e.g.
442/// `core::panicking::panic_fmt::h...`). Best-effort by design: the section is
443/// DEBUG METADATA only, so a malformed entry is skipped rather than failing the
444/// compile — no codegen path depends on it (#394 Tier-1.x).
445fn parse_name_section_func_names(
446    reader: wasmparser::NameSectionReader<'_>,
447    out: &mut HashMap<u32, String>,
448) {
449    for subsection in reader.into_iter().flatten() {
450        if let wasmparser::Name::Function(map) = subsection {
451            for naming in map.into_iter().flatten() {
452                out.insert(naming.index, naming.name.to_string());
453            }
454        }
455    }
456}
457
458/// Fill each function's `debug_name` from the `name`-section map (keyed by the
459/// FULL function index, imports first — the same index space `FunctionOps.index`
460/// uses). A function without an entry keeps `None` (⇒ `func_N` downstream).
461fn apply_name_section(functions: &mut [FunctionOps], names: &HashMap<u32, String>) {
462    if names.is_empty() {
463        return;
464    }
465    for f in functions {
466        f.debug_name = names.get(&f.index).cloned();
467    }
468}
469
470/// Decode a WASM binary and extract all function bodies as WasmOp sequences
471pub fn decode_wasm_functions(wasm_bytes: &[u8]) -> Result<Vec<FunctionOps>> {
472    let mut functions = Vec::new();
473    let mut func_index = 0u32;
474    let mut num_imported_funcs = 0u32;
475    let mut export_names: HashMap<u32, String> = HashMap::new();
476    let mut name_section_names: HashMap<u32, String> = HashMap::new();
477    // #509: (param_count, result_count) per type index, for FuncType blocktypes.
478    let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
479
480    for payload in Parser::new(0).parse_all(wasm_bytes) {
481        let payload = payload.context("Failed to parse WASM payload")?;
482
483        match payload {
484            Payload::TypeSection(reader) => {
485                // #509: the blocktype-arity side-table needs the type section
486                // for `BlockType::FuncType(i)` lookups (the wasm binary format
487                // places types before code, so the table is complete before any
488                // `CodeSectionEntry` is decoded).
489                for rec_group in reader {
490                    let rec_group = rec_group.context("Failed to parse type")?;
491                    for sub_ty in rec_group.types() {
492                        type_block_arity.push(match &sub_ty.composite_type.inner {
493                            wasmparser::CompositeInnerType::Func(f) => (
494                                u8::try_from(f.params().len()).unwrap_or(u8::MAX),
495                                u8::try_from(f.results().len()).unwrap_or(u8::MAX),
496                            ),
497                            _ => (u8::MAX, u8::MAX),
498                        });
499                    }
500                }
501            }
502            Payload::ImportSection(imports) => {
503                // wasmparser 0.221+ compact-imports grouping — flatten groups
504                // to individual imports (see the ImportSection handler above).
505                for import in imports.into_imports() {
506                    let import = import.context("Failed to parse import")?;
507                    if matches!(import.ty, wasmparser::TypeRef::Func(_)) {
508                        num_imported_funcs += 1;
509                    }
510                }
511            }
512            Payload::ExportSection(exports) => {
513                for export in exports {
514                    let export = export.context("Failed to parse export")?;
515                    if export.kind == ExternalKind::Func {
516                        export_names.insert(export.index, export.name.to_string());
517                    }
518                }
519            }
520            Payload::CodeSectionEntry(body) => {
521                let (ops, op_offsets, block_arity, unsupported) =
522                    decode_function_body(&body, &type_block_arity)?;
523                let actual_index = num_imported_funcs + func_index;
524                let export_name = export_names.get(&actual_index).cloned();
525
526                functions.push(FunctionOps {
527                    index: actual_index,
528                    export_name,
529                    debug_name: None, // filled from the `name` section after the loop
530                    ops,
531                    op_offsets,
532                    unsupported,
533                    block_arity,
534                });
535                func_index += 1;
536            }
537            Payload::CustomSection(c) => {
538                // #394 Tier-1.x: the wasm `name` custom section.
539                if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
540                    parse_name_section_func_names(reader, &mut name_section_names);
541                }
542            }
543            _ => {}
544        }
545    }
546
547    apply_name_section(&mut functions, &name_section_names);
548
549    Ok(functions)
550}
551
552/// Decoded function with its WasmOp sequence
553#[derive(Debug, Clone)]
554pub struct FunctionOps {
555    /// Function index in the module (includes imported functions)
556    pub index: u32,
557    /// Export name if this function is exported
558    pub export_name: Option<String>,
559    /// #394 Tier-1.x: the function's developer-facing name from the wasm `name`
560    /// custom section (function-names subsection), e.g.
561    /// `core::panicking::panic_fmt::h6651313c3e2c6c2f` — present for INTERNAL
562    /// (non-exported) functions too, unlike `export_name`. DEBUG METADATA only:
563    /// consumed by the `--debug-line` `DW_TAG_subprogram` emit (name priority:
564    /// name-section > export name > `func_N`); no codegen or symbol-table path
565    /// reads it, so emitted `.text`/`.symtab` are unchanged (frozen-safe).
566    /// `None` when the module has no `name` section or no entry for this index.
567    pub debug_name: Option<String>,
568    /// The WASM operations in this function body
569    pub ops: Vec<WasmOp>,
570    /// VCR-DBG-001 step 1 (#394): module-relative wasm byte offset of each op in
571    /// `ops` (same index → same op). This is the address space DWARF-for-wasm
572    /// `.debug_line` keys on, so it is the bridge from synth's op-index
573    /// `source_line` to the input wasm's DWARF (wasm-offset → source). PURELY
574    /// ADDITIVE metadata: no codegen path reads it, so emitted `.text` is
575    /// unchanged and the frozen fixtures stay bit-identical. Empty until consumed
576    /// by the DWARF emitter (Tier 1).
577    pub op_offsets: Vec<u32>,
578    /// `Some(reason)` when the body contained a value-affecting operator the
579    /// decoder cannot lower (e.g. scalar f32/f64 — #369, bulk-memory
580    /// memory.copy/fill). Such an op would otherwise be silently *dropped*
581    /// (`convert_operator` → `None`), leaving the operand stack wrong and the
582    /// function a silent miscompile. The compile path LOUD-SKIPS a flagged
583    /// function (diagnostic + symbol absent → link error names it) instead —
584    /// the #180/#185 "unsupported op must Err, never silently continue"
585    /// contract. `None` once every op decoded or was intentionally ignorable
586    /// (Nop/Unreachable).
587    pub unsupported: Option<String>,
588    /// #509: blocktype arity side-table — `(param_count, result_count)` of the
589    /// k-th `Block`/`Loop`/`If` op in `ops`, in order of appearance.
590    /// ORDINAL-keyed, not op-index-keyed, on purpose: the backend may rewrite
591    /// the op stream before selection (e.g. the #539 `i32.const 0; memory.grow`
592    /// → `memory.size` fold), which shifts op indices but never adds/removes
593    /// control ops, so the ordinal stays aligned. `BlockType::Empty → (0,0)`,
594    /// `ValType → (0,1)`, `FuncType(i) →` counts from the type section
595    /// (saturated to u8; an unresolvable type index records `(u8::MAX,
596    /// u8::MAX)` so the selector declines loudly instead of miscompiling).
597    /// This is what lets the direct selector land a value carried by
598    /// `br`/`br_if`/`br_table` in the target block's designated result
599    /// register instead of dropping it — `WasmOp::Block/Loop/If` stay bare
600    /// unit variants (zero ripple through the backends' match sites), and an
601    /// empty table (hand-built op streams in unit tests) keeps the legacy
602    /// void-block lowering.
603    pub block_arity: Vec<(u8, u8)>,
604}
605
606/// #509: `(param_count, result_count)` of a wasm blocktype, for the
607/// [`FunctionOps::block_arity`] side-table. `type_block_arity` is the type
608/// section's per-type-index counts (needed for the `FuncType` form); a missing
609/// entry saturates to `(u8::MAX, u8::MAX)` so downstream declines loudly.
610fn blocktype_arity(bt: &wasmparser::BlockType, type_block_arity: &[(u8, u8)]) -> (u8, u8) {
611    match bt {
612        wasmparser::BlockType::Empty => (0, 0),
613        wasmparser::BlockType::Type(_) => (0, 1),
614        wasmparser::BlockType::FuncType(i) => type_block_arity
615            .get(*i as usize)
616            .copied()
617            .unwrap_or((u8::MAX, u8::MAX)),
618    }
619}
620
621/// The per-function payload [`decode_function_body`] extracts: `(ops,
622/// op_offsets, block_arity, unsupported)` — see the matching
623/// [`FunctionOps`] fields for each component's contract.
624type DecodedBody = (Vec<WasmOp>, Vec<u32>, Vec<(u8, u8)>, Option<String>);
625
626/// Decode a single function body to WasmOp sequence.
627///
628/// Returns the ops plus `Some(reason)` if any operator was a value-affecting
629/// op the decoder cannot lower (so the function must be loud-skipped, #369 —
630/// not silently miscompiled by dropping the op).
631fn decode_function_body(
632    body: &wasmparser::FunctionBody,
633    type_block_arity: &[(u8, u8)],
634) -> Result<DecodedBody> {
635    let mut ops = Vec::new();
636    // VCR-DBG-001 step 1: parallel to `ops` — the module-relative wasm byte
637    // offset of each emitted op (the DWARF-for-wasm address space). Captured via
638    // the offset-aware reader; pushed only when an op is pushed, so indices stay
639    // aligned with `ops`. Additive metadata, no codegen consumer ⇒ frozen-safe.
640    let mut op_offsets = Vec::new();
641    // #509: ordinal blocktype-arity side-table — one entry per Block/Loop/If in
642    // `ops` order (see `FunctionOps::block_arity`).
643    let mut block_arity: Vec<(u8, u8)> = Vec::new();
644    let mut unsupported: Option<String> = None;
645
646    let ops_reader = body.get_operators_reader()?;
647    for item in ops_reader.into_iter_with_offsets() {
648        let (op, offset) = item.context("Failed to read operator")?;
649
650        if let Some(wasm_op) = convert_operator(&op) {
651            // #509: capture the blocktype arity BEFORE the enum flattens it away
652            // (`WasmOp::Block/Loop/If` are unit variants by design).
653            if let wasmparser::Operator::Block { blockty }
654            | wasmparser::Operator::Loop { blockty }
655            | wasmparser::Operator::If { blockty } = &op
656            {
657                block_arity.push(blocktype_arity(blockty, type_block_arity));
658            }
659            ops.push(wasm_op);
660            op_offsets.push(offset as u32);
661        } else if unsupported.is_none() && !is_intentionally_ignored(&op) {
662            // The op was DROPPED by `convert_operator` (`_ => None`) and is not
663            // an intentional no-op (Nop/Unreachable) — record it so the
664            // function is loud-skipped rather than silently miscompiled (#369).
665            unsupported = Some(format!("{op:?}"));
666        }
667    }
668
669    Ok((ops, op_offsets, block_arity, unsupported))
670}
671
672/// Operators that `convert_operator` returns `None` for *on purpose* — they
673/// carry no value-affecting semantics for our backend, so dropping them is
674/// correct (NOT a silent miscompile). Everything else that decodes to `None`
675/// is an unsupported op that must loud-skip its function (#369).
676fn is_intentionally_ignored(op: &wasmparser::Operator) -> bool {
677    use wasmparser::Operator::*;
678    matches!(op, Nop | Unreachable)
679}
680
681/// Convert a wasmparser Operator to our WasmOp enum
682fn convert_operator(op: &wasmparser::Operator) -> Option<WasmOp> {
683    use wasmparser::Operator::*;
684
685    match op {
686        // Constants
687        I32Const { value } => Some(WasmOp::I32Const(*value)),
688
689        // i32 Arithmetic
690        I32Add => Some(WasmOp::I32Add),
691        I32Sub => Some(WasmOp::I32Sub),
692        I32Mul => Some(WasmOp::I32Mul),
693        I32DivS => Some(WasmOp::I32DivS),
694        I32DivU => Some(WasmOp::I32DivU),
695        I32RemS => Some(WasmOp::I32RemS),
696        I32RemU => Some(WasmOp::I32RemU),
697
698        // i64 Constants
699        I64Const { value } => Some(WasmOp::I64Const(*value)),
700
701        // i64 Arithmetic
702        I64Add => Some(WasmOp::I64Add),
703        I64Sub => Some(WasmOp::I64Sub),
704        I64Mul => Some(WasmOp::I64Mul),
705        I64DivS => Some(WasmOp::I64DivS),
706        I64DivU => Some(WasmOp::I64DivU),
707        I64RemS => Some(WasmOp::I64RemS),
708        I64RemU => Some(WasmOp::I64RemU),
709
710        // i64 Bitwise
711        I64And => Some(WasmOp::I64And),
712        I64Or => Some(WasmOp::I64Or),
713        I64Xor => Some(WasmOp::I64Xor),
714        I64Shl => Some(WasmOp::I64Shl),
715        I64ShrS => Some(WasmOp::I64ShrS),
716        I64ShrU => Some(WasmOp::I64ShrU),
717        I64Rotl => Some(WasmOp::I64Rotl),
718        I64Rotr => Some(WasmOp::I64Rotr),
719        I64Clz => Some(WasmOp::I64Clz),
720        I64Ctz => Some(WasmOp::I64Ctz),
721        I64Popcnt => Some(WasmOp::I64Popcnt),
722        I64Extend8S => Some(WasmOp::I64Extend8S),
723        I64Extend16S => Some(WasmOp::I64Extend16S),
724        I64Extend32S => Some(WasmOp::I64Extend32S),
725        // i32<->i64 width conversions. Previously UNMAPPED → silently dropped,
726        // which left an i32 value as a 64-bit operand with a garbage high half
727        // (harmless when a following `i64.shl 32` discards it, but a latent
728        // miscompile for extend-then-arithmetic, and it breaks width-correct
729        // register allocation). (#204)
730        I64ExtendI32U => Some(WasmOp::I64ExtendI32U),
731        I64ExtendI32S => Some(WasmOp::I64ExtendI32S),
732        I32WrapI64 => Some(WasmOp::I32WrapI64),
733
734        // i64 Comparison
735        I64Eqz => Some(WasmOp::I64Eqz),
736        I64Eq => Some(WasmOp::I64Eq),
737        I64Ne => Some(WasmOp::I64Ne),
738        I64LtS => Some(WasmOp::I64LtS),
739        I64LtU => Some(WasmOp::I64LtU),
740        I64LeS => Some(WasmOp::I64LeS),
741        I64LeU => Some(WasmOp::I64LeU),
742        I64GtS => Some(WasmOp::I64GtS),
743        I64GtU => Some(WasmOp::I64GtU),
744        I64GeS => Some(WasmOp::I64GeS),
745        I64GeU => Some(WasmOp::I64GeU),
746
747        // Bitwise
748        I32And => Some(WasmOp::I32And),
749        I32Or => Some(WasmOp::I32Or),
750        I32Xor => Some(WasmOp::I32Xor),
751        I32Shl => Some(WasmOp::I32Shl),
752        I32ShrS => Some(WasmOp::I32ShrS),
753        I32ShrU => Some(WasmOp::I32ShrU),
754        I32Rotl => Some(WasmOp::I32Rotl),
755        I32Rotr => Some(WasmOp::I32Rotr),
756        I32Clz => Some(WasmOp::I32Clz),
757        I32Ctz => Some(WasmOp::I32Ctz),
758        I32Popcnt => Some(WasmOp::I32Popcnt),
759        I32Extend8S => Some(WasmOp::I32Extend8S),
760        I32Extend16S => Some(WasmOp::I32Extend16S),
761
762        // Comparison
763        I32Eqz => Some(WasmOp::I32Eqz),
764        I32Eq => Some(WasmOp::I32Eq),
765        I32Ne => Some(WasmOp::I32Ne),
766        I32LtS => Some(WasmOp::I32LtS),
767        I32LtU => Some(WasmOp::I32LtU),
768        I32LeS => Some(WasmOp::I32LeS),
769        I32LeU => Some(WasmOp::I32LeU),
770        I32GtS => Some(WasmOp::I32GtS),
771        I32GtU => Some(WasmOp::I32GtU),
772        I32GeS => Some(WasmOp::I32GeS),
773        I32GeU => Some(WasmOp::I32GeU),
774
775        // Memory
776        I32Load { memarg } => Some(WasmOp::I32Load {
777            offset: memarg.offset as u32,
778            align: memarg.align as u32,
779        }),
780        I32Store { memarg } => Some(WasmOp::I32Store {
781            offset: memarg.offset as u32,
782            align: memarg.align as u32,
783        }),
784        // #372: full-width i64 load/store. The selector already lowers these to
785        // a lo/hi i32 register-pair access (`generate_i64_load/store_with_bounds_check`,
786        // reusing the #171 pair regalloc) — only the decoder arm was missing, so
787        // `i64.load`/`i64.store` fell through `_ => None` and (since v0.11.46)
788        // loud-skipped their function. The narrow forms (I64Load8.. / I64Store32)
789        // were already decoded below.
790        I64Load { memarg } => Some(WasmOp::I64Load {
791            offset: memarg.offset as u32,
792            align: memarg.align as u32,
793        }),
794        I64Store { memarg } => Some(WasmOp::I64Store {
795            offset: memarg.offset as u32,
796            align: memarg.align as u32,
797        }),
798
799        // Sub-word loads (i32)
800        I32Load8S { memarg } => Some(WasmOp::I32Load8S {
801            offset: memarg.offset as u32,
802            align: memarg.align as u32,
803        }),
804        I32Load8U { memarg } => Some(WasmOp::I32Load8U {
805            offset: memarg.offset as u32,
806            align: memarg.align as u32,
807        }),
808        I32Load16S { memarg } => Some(WasmOp::I32Load16S {
809            offset: memarg.offset as u32,
810            align: memarg.align as u32,
811        }),
812        I32Load16U { memarg } => Some(WasmOp::I32Load16U {
813            offset: memarg.offset as u32,
814            align: memarg.align as u32,
815        }),
816
817        // Sub-word stores (i32)
818        I32Store8 { memarg } => Some(WasmOp::I32Store8 {
819            offset: memarg.offset as u32,
820            align: memarg.align as u32,
821        }),
822        I32Store16 { memarg } => Some(WasmOp::I32Store16 {
823            offset: memarg.offset as u32,
824            align: memarg.align as u32,
825        }),
826
827        // Local/Global
828        LocalGet { local_index } => Some(WasmOp::LocalGet(*local_index)),
829        LocalSet { local_index } => Some(WasmOp::LocalSet(*local_index)),
830        LocalTee { local_index } => Some(WasmOp::LocalTee(*local_index)),
831        GlobalGet { global_index } => Some(WasmOp::GlobalGet(*global_index)),
832        GlobalSet { global_index } => Some(WasmOp::GlobalSet(*global_index)),
833
834        // Control flow
835        Block { .. } => Some(WasmOp::Block),
836        Loop { .. } => Some(WasmOp::Loop),
837        Br { relative_depth } => Some(WasmOp::Br(*relative_depth)),
838        BrIf { relative_depth } => Some(WasmOp::BrIf(*relative_depth)),
839        // br_table: indexed multi-way branch. Previously UNMAPPED → silently
840        // dropped, so the selector never emitted the index dispatch and control
841        // fell straight into the first table arm — every br_table behaved as if
842        // it always took target 0 (gale's binary-sem WAKE path never fired). The
843        // jump-table relative depths + default depth are preserved in order.
844        BrTable { targets } => {
845            let default = targets.default();
846            let tgts: Vec<u32> = targets.targets().filter_map(Result::ok).collect();
847            Some(WasmOp::BrTable {
848                targets: tgts,
849                default,
850            })
851        }
852        Return => Some(WasmOp::Return),
853        Call { function_index } => Some(WasmOp::Call(*function_index)),
854        CallIndirect {
855            type_index,
856            table_index,
857            ..
858        } => Some(WasmOp::CallIndirect {
859            type_index: *type_index,
860            table_index: *table_index,
861        }),
862
863        // End is needed for control flow pattern matching
864        End => Some(WasmOp::End),
865
866        // Nop/Unreachable - skip these
867        Nop | Unreachable => None,
868
869        // Drop is needed for br_if pattern matching
870        Drop => Some(WasmOp::Drop),
871
872        // Select
873        Select => Some(WasmOp::Select),
874
875        // If/Else - simplified handling
876        If { .. } => Some(WasmOp::If),
877        Else => Some(WasmOp::Else),
878
879        // i64 sub-word loads
880        I64Load8S { memarg } => Some(WasmOp::I64Load8S {
881            offset: memarg.offset as u32,
882            align: memarg.align as u32,
883        }),
884        I64Load8U { memarg } => Some(WasmOp::I64Load8U {
885            offset: memarg.offset as u32,
886            align: memarg.align as u32,
887        }),
888        I64Load16S { memarg } => Some(WasmOp::I64Load16S {
889            offset: memarg.offset as u32,
890            align: memarg.align as u32,
891        }),
892        I64Load16U { memarg } => Some(WasmOp::I64Load16U {
893            offset: memarg.offset as u32,
894            align: memarg.align as u32,
895        }),
896        I64Load32S { memarg } => Some(WasmOp::I64Load32S {
897            offset: memarg.offset as u32,
898            align: memarg.align as u32,
899        }),
900        I64Load32U { memarg } => Some(WasmOp::I64Load32U {
901            offset: memarg.offset as u32,
902            align: memarg.align as u32,
903        }),
904
905        // i64 sub-word stores
906        I64Store8 { memarg } => Some(WasmOp::I64Store8 {
907            offset: memarg.offset as u32,
908            align: memarg.align as u32,
909        }),
910        I64Store16 { memarg } => Some(WasmOp::I64Store16 {
911            offset: memarg.offset as u32,
912            align: memarg.align as u32,
913        }),
914        I64Store32 { memarg } => Some(WasmOp::I64Store32 {
915            offset: memarg.offset as u32,
916            align: memarg.align as u32,
917        }),
918
919        // Memory management
920        MemorySize { mem, .. } => Some(WasmOp::MemorySize(*mem)),
921        MemoryGrow { mem, .. } => Some(WasmOp::MemoryGrow(*mem)),
922
923        // Bulk memory (#374). The backend supports a single linear memory
924        // (memory 0); any non-zero memory index falls through to `_ => None` and
925        // loud-skips the function (GI-FPU-001 honesty contract) rather than
926        // miscompiling a multi-memory copy. memory.copy reads dst/src memories;
927        // memory.fill one. The selector lowers these to a bounds-checked byte
928        // loop (see select_with_stack).
929        MemoryCopy {
930            dst_mem: 0,
931            src_mem: 0,
932        } => Some(WasmOp::MemoryCopy),
933        MemoryFill { mem: 0 } => Some(WasmOp::MemoryFill),
934
935        // ========================================================================
936        // v128 SIMD operations (WASM SIMD proposal, 0xFD prefix)
937        // ========================================================================
938        V128Const { value } => {
939            let mut bytes = [0u8; 16];
940            bytes.copy_from_slice(value.bytes());
941            Some(WasmOp::V128Const(bytes))
942        }
943        V128Load { memarg } => Some(WasmOp::V128Load {
944            offset: memarg.offset as u32,
945            align: memarg.align as u32,
946        }),
947        V128Store { memarg } => Some(WasmOp::V128Store {
948            offset: memarg.offset as u32,
949            align: memarg.align as u32,
950        }),
951
952        // v128 bitwise
953        V128And => Some(WasmOp::V128And),
954        V128Or => Some(WasmOp::V128Or),
955        V128Xor => Some(WasmOp::V128Xor),
956        V128Not => Some(WasmOp::V128Not),
957        V128AndNot => Some(WasmOp::V128AndNot),
958
959        // i8x16
960        I8x16Add => Some(WasmOp::I8x16Add),
961        I8x16Sub => Some(WasmOp::I8x16Sub),
962        I8x16Neg => Some(WasmOp::I8x16Neg),
963        I8x16Eq => Some(WasmOp::I8x16Eq),
964        I8x16Ne => Some(WasmOp::I8x16Ne),
965        I8x16LtS => Some(WasmOp::I8x16LtS),
966        I8x16LtU => Some(WasmOp::I8x16LtU),
967        I8x16GtS => Some(WasmOp::I8x16GtS),
968        I8x16GtU => Some(WasmOp::I8x16GtU),
969        I8x16LeS => Some(WasmOp::I8x16LeS),
970        I8x16LeU => Some(WasmOp::I8x16LeU),
971        I8x16GeS => Some(WasmOp::I8x16GeS),
972        I8x16GeU => Some(WasmOp::I8x16GeU),
973        I8x16Splat => Some(WasmOp::I8x16Splat),
974        I8x16ExtractLaneS { lane } => Some(WasmOp::I8x16ExtractLaneS(*lane)),
975        I8x16ExtractLaneU { lane } => Some(WasmOp::I8x16ExtractLaneU(*lane)),
976        I8x16ReplaceLane { lane } => Some(WasmOp::I8x16ReplaceLane(*lane)),
977        I8x16Shuffle { lanes } => Some(WasmOp::I8x16Shuffle(*lanes)),
978        I8x16Swizzle => Some(WasmOp::I8x16Swizzle),
979
980        // i16x8
981        I16x8Add => Some(WasmOp::I16x8Add),
982        I16x8Sub => Some(WasmOp::I16x8Sub),
983        I16x8Mul => Some(WasmOp::I16x8Mul),
984        I16x8Neg => Some(WasmOp::I16x8Neg),
985        I16x8Eq => Some(WasmOp::I16x8Eq),
986        I16x8Ne => Some(WasmOp::I16x8Ne),
987        I16x8LtS => Some(WasmOp::I16x8LtS),
988        I16x8LtU => Some(WasmOp::I16x8LtU),
989        I16x8GtS => Some(WasmOp::I16x8GtS),
990        I16x8GtU => Some(WasmOp::I16x8GtU),
991        I16x8LeS => Some(WasmOp::I16x8LeS),
992        I16x8LeU => Some(WasmOp::I16x8LeU),
993        I16x8GeS => Some(WasmOp::I16x8GeS),
994        I16x8GeU => Some(WasmOp::I16x8GeU),
995        I16x8Splat => Some(WasmOp::I16x8Splat),
996        I16x8ExtractLaneS { lane } => Some(WasmOp::I16x8ExtractLaneS(*lane)),
997        I16x8ExtractLaneU { lane } => Some(WasmOp::I16x8ExtractLaneU(*lane)),
998        I16x8ReplaceLane { lane } => Some(WasmOp::I16x8ReplaceLane(*lane)),
999
1000        // i32x4
1001        I32x4Add => Some(WasmOp::I32x4Add),
1002        I32x4Sub => Some(WasmOp::I32x4Sub),
1003        I32x4Mul => Some(WasmOp::I32x4Mul),
1004        I32x4Neg => Some(WasmOp::I32x4Neg),
1005        I32x4Eq => Some(WasmOp::I32x4Eq),
1006        I32x4Ne => Some(WasmOp::I32x4Ne),
1007        I32x4LtS => Some(WasmOp::I32x4LtS),
1008        I32x4LtU => Some(WasmOp::I32x4LtU),
1009        I32x4GtS => Some(WasmOp::I32x4GtS),
1010        I32x4GtU => Some(WasmOp::I32x4GtU),
1011        I32x4LeS => Some(WasmOp::I32x4LeS),
1012        I32x4LeU => Some(WasmOp::I32x4LeU),
1013        I32x4GeS => Some(WasmOp::I32x4GeS),
1014        I32x4GeU => Some(WasmOp::I32x4GeU),
1015        I32x4Splat => Some(WasmOp::I32x4Splat),
1016        I32x4ExtractLane { lane } => Some(WasmOp::I32x4ExtractLane(*lane)),
1017        I32x4ReplaceLane { lane } => Some(WasmOp::I32x4ReplaceLane(*lane)),
1018
1019        // i64x2
1020        I64x2Add => Some(WasmOp::I64x2Add),
1021        I64x2Sub => Some(WasmOp::I64x2Sub),
1022        I64x2Mul => Some(WasmOp::I64x2Mul),
1023        I64x2Neg => Some(WasmOp::I64x2Neg),
1024        I64x2Eq => Some(WasmOp::I64x2Eq),
1025        I64x2Ne => Some(WasmOp::I64x2Ne),
1026        I64x2LtS => Some(WasmOp::I64x2LtS),
1027        I64x2GtS => Some(WasmOp::I64x2GtS),
1028        I64x2LeS => Some(WasmOp::I64x2LeS),
1029        I64x2GeS => Some(WasmOp::I64x2GeS),
1030        I64x2Splat => Some(WasmOp::I64x2Splat),
1031        I64x2ExtractLane { lane } => Some(WasmOp::I64x2ExtractLane(*lane)),
1032        I64x2ReplaceLane { lane } => Some(WasmOp::I64x2ReplaceLane(*lane)),
1033
1034        // f32x4
1035        F32x4Add => Some(WasmOp::F32x4Add),
1036        F32x4Sub => Some(WasmOp::F32x4Sub),
1037        F32x4Mul => Some(WasmOp::F32x4Mul),
1038        F32x4Div => Some(WasmOp::F32x4Div),
1039        F32x4Abs => Some(WasmOp::F32x4Abs),
1040        F32x4Neg => Some(WasmOp::F32x4Neg),
1041        F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
1042        F32x4Eq => Some(WasmOp::F32x4Eq),
1043        F32x4Ne => Some(WasmOp::F32x4Ne),
1044        F32x4Lt => Some(WasmOp::F32x4Lt),
1045        F32x4Le => Some(WasmOp::F32x4Le),
1046        F32x4Gt => Some(WasmOp::F32x4Gt),
1047        F32x4Ge => Some(WasmOp::F32x4Ge),
1048        F32x4Splat => Some(WasmOp::F32x4Splat),
1049        F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
1050        F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
1051
1052        // Other operators not yet supported
1053        _ => None,
1054    }
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059    use super::*;
1060
1061    #[test]
1062    fn test_decode_simple_add() {
1063        let wat = r#"
1064            (module
1065                (func (export "add") (param i32 i32) (result i32)
1066                    local.get 0
1067                    local.get 1
1068                    i32.add
1069                )
1070            )
1071        "#;
1072
1073        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1074        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1075
1076        assert_eq!(functions.len(), 1);
1077        assert_eq!(functions[0].index, 0);
1078        assert_eq!(functions[0].export_name, Some("add".to_string()));
1079        assert_eq!(
1080            functions[0].ops,
1081            vec![
1082                WasmOp::LocalGet(0),
1083                WasmOp::LocalGet(1),
1084                WasmOp::I32Add,
1085                WasmOp::End
1086            ]
1087        );
1088    }
1089
1090    /// #204 regression: `i64.extend_i32_u`, `i64.extend_i32_s` and
1091    /// `i32.wrap_i64` must DECODE (they were previously unmapped → silently
1092    /// dropped by `convert_operator`, leaving an i32 value as a 64-bit operand
1093    /// with a garbage high half — the root cause of gale's miscompiled
1094    /// `(new_count << 32)` pack). The decoder must surface all three.
1095    #[test]
1096    fn test_decode_i64_i32_width_conversions() {
1097        let wat = r#"
1098            (module
1099                (func (export "conv") (param i32 i64) (result i32)
1100                    local.get 0
1101                    i64.extend_i32_u
1102                    local.get 0
1103                    i64.extend_i32_s
1104                    i64.add
1105                    local.get 1
1106                    i64.add
1107                    i32.wrap_i64
1108                )
1109            )
1110        "#;
1111        let wasm = wat::parse_str(wat).expect("parse");
1112        let functions = decode_wasm_functions(&wasm).expect("decode");
1113        let ops = &functions[0].ops;
1114        assert!(
1115            ops.contains(&WasmOp::I64ExtendI32U),
1116            "i64.extend_i32_u must decode (not be dropped): {ops:?}"
1117        );
1118        assert!(
1119            ops.contains(&WasmOp::I64ExtendI32S),
1120            "i64.extend_i32_s must decode (not be dropped): {ops:?}"
1121        );
1122        assert!(
1123            ops.contains(&WasmOp::I32WrapI64),
1124            "i32.wrap_i64 must decode (not be dropped): {ops:?}"
1125        );
1126    }
1127
1128    /// #204 WAKE-path regression: `br_table` must DECODE (it was unmapped in
1129    /// `convert_operator` → silently dropped, so the selector emitted no index
1130    /// dispatch and every `br_table` fell through to target 0 — gale's binary
1131    /// semaphore never took its WAKE branch). Targets + default are preserved.
1132    #[test]
1133    fn test_decode_br_table() {
1134        let wat = r#"
1135            (module
1136                (func (export "bt") (param i32) (result i32)
1137                    (block (block (block
1138                        local.get 0
1139                        br_table 2 0 1 2)
1140                      i32.const 30 return)
1141                      i32.const 20 return)
1142                    i32.const 10))
1143        "#;
1144        let wasm = wat::parse_str(wat).expect("parse");
1145        let functions = decode_wasm_functions(&wasm).expect("decode");
1146        let bt = functions[0]
1147            .ops
1148            .iter()
1149            .find_map(|o| match o {
1150                WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
1151                _ => None,
1152            })
1153            .expect("br_table must decode (not be dropped)");
1154        assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
1155        assert_eq!(bt.1, 2, "br_table default preserved");
1156    }
1157
1158    #[test]
1159    fn test_decode_arithmetic() {
1160        let wat = r#"
1161            (module
1162                (func (export "calc") (result i32)
1163                    i32.const 5
1164                    i32.const 3
1165                    i32.mul
1166                    i32.const 2
1167                    i32.add
1168                )
1169            )
1170        "#;
1171
1172        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1173        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1174
1175        assert_eq!(functions.len(), 1);
1176        assert_eq!(functions[0].export_name, Some("calc".to_string()));
1177        assert_eq!(
1178            functions[0].ops,
1179            vec![
1180                WasmOp::I32Const(5),
1181                WasmOp::I32Const(3),
1182                WasmOp::I32Mul,
1183                WasmOp::I32Const(2),
1184                WasmOp::I32Add,
1185                WasmOp::End,
1186            ]
1187        );
1188    }
1189
1190    #[test]
1191    fn test_decode_multi_function_module() {
1192        let wat = r#"
1193            (module
1194                (func $helper)
1195                (func (export "add") (param i32 i32) (result i32)
1196                    local.get 0
1197                    local.get 1
1198                    i32.add
1199                )
1200                (func (export "sub") (param i32 i32) (result i32)
1201                    local.get 0
1202                    local.get 1
1203                    i32.sub
1204                )
1205            )
1206        "#;
1207
1208        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1209        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1210
1211        assert_eq!(functions.len(), 3);
1212        assert_eq!(functions[0].index, 0);
1213        assert_eq!(functions[0].export_name, None);
1214        assert_eq!(functions[1].index, 1);
1215        assert_eq!(functions[1].export_name, Some("add".to_string()));
1216        assert_eq!(functions[2].index, 2);
1217        assert_eq!(functions[2].export_name, Some("sub".to_string()));
1218    }
1219
1220    #[test]
1221    fn test_decode_module_with_imports() {
1222        let wat = r#"
1223            (module
1224                (import "env" "log" (func $log (param i32)))
1225                (import "env" "memory" (memory 1))
1226                (func (export "run") (param i32)
1227                    local.get 0
1228                    call 0
1229                )
1230            )
1231        "#;
1232
1233        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1234        let module = decode_wasm_module(&wasm).expect("Failed to decode");
1235
1236        // Should have 2 imports (1 func, 1 memory)
1237        assert_eq!(module.imports.len(), 2);
1238        assert_eq!(module.num_imported_funcs, 1);
1239
1240        // First import is the function
1241        assert_eq!(module.imports[0].module, "env");
1242        assert_eq!(module.imports[0].name, "log");
1243        assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
1244
1245        // Second import is memory
1246        assert_eq!(module.imports[1].module, "env");
1247        assert_eq!(module.imports[1].name, "memory");
1248        assert_eq!(module.imports[1].kind, ImportKind::Memory);
1249
1250        // Should have 1 local function (index 1, because import is index 0)
1251        assert_eq!(module.functions.len(), 1);
1252        assert_eq!(module.functions[0].index, 1);
1253        assert_eq!(module.functions[0].export_name, Some("run".to_string()));
1254    }
1255
1256    #[test]
1257    fn test_find_function_by_export_name() {
1258        let wat = r#"
1259            (module
1260                (func $helper)
1261                (func (export "add") (param i32 i32) (result i32)
1262                    local.get 0
1263                    local.get 1
1264                    i32.add
1265                )
1266            )
1267        "#;
1268
1269        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1270        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1271
1272        let add_func = functions
1273            .iter()
1274            .find(|f| f.export_name.as_deref() == Some("add"))
1275            .expect("Should find 'add' function");
1276
1277        assert_eq!(add_func.index, 1);
1278        assert!(add_func.ops.contains(&WasmOp::I32Add));
1279    }
1280
1281    #[test]
1282    fn test_decode_subword_loads() {
1283        let wat = r#"
1284            (module
1285                (memory 1)
1286                (func (export "test") (param i32) (result i32)
1287                    local.get 0
1288                    i32.load8_u
1289                )
1290            )
1291        "#;
1292
1293        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1294        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1295
1296        assert_eq!(functions.len(), 1);
1297        assert!(functions[0].ops.contains(&WasmOp::I32Load8U {
1298            offset: 0,
1299            align: 0,
1300        }));
1301    }
1302
1303    #[test]
1304    fn test_decode_subword_stores() {
1305        let wat = r#"
1306            (module
1307                (memory 1)
1308                (func (export "test") (param i32 i32)
1309                    local.get 0
1310                    local.get 1
1311                    i32.store8
1312                )
1313            )
1314        "#;
1315
1316        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1317        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1318
1319        assert_eq!(functions.len(), 1);
1320        assert!(functions[0].ops.contains(&WasmOp::I32Store8 {
1321            offset: 0,
1322            align: 0,
1323        }));
1324    }
1325
1326    #[test]
1327    fn test_decode_memory_size_grow() {
1328        let wat = r#"
1329            (module
1330                (memory 1)
1331                (func (export "test") (result i32)
1332                    memory.size
1333                )
1334            )
1335        "#;
1336
1337        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1338        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1339
1340        assert_eq!(functions.len(), 1);
1341        assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
1342    }
1343
1344    #[test]
1345    fn test_decode_memory_grow() {
1346        let wat = r#"
1347            (module
1348                (memory 1)
1349                (func (export "test") (param i32) (result i32)
1350                    local.get 0
1351                    memory.grow
1352                )
1353            )
1354        "#;
1355
1356        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1357        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1358
1359        assert_eq!(functions.len(), 1);
1360        assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
1361    }
1362
1363    #[test]
1364    fn test_decode_bulk_memory_374() {
1365        // #374: memory.copy / memory.fill on the single linear memory decode to
1366        // the new WasmOp variants (was `_ => None` -> loud-skip).
1367        let wat = r#"
1368            (module
1369                (memory 1)
1370                (func (export "cpy") (param i32 i32 i32)
1371                    local.get 0 local.get 1 local.get 2 memory.copy)
1372                (func (export "fil") (param i32 i32 i32)
1373                    local.get 0 local.get 1 local.get 2 memory.fill)
1374            )
1375        "#;
1376        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1377        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1378        assert_eq!(functions.len(), 2);
1379        assert!(functions[0].ops.contains(&WasmOp::MemoryCopy));
1380        assert!(functions[1].ops.contains(&WasmOp::MemoryFill));
1381        // Neither function is flagged unsupported (they now lower).
1382        assert!(functions[0].unsupported.is_none());
1383        assert!(functions[1].unsupported.is_none());
1384    }
1385
1386    #[test]
1387    fn test_decode_i64_subword_loads() {
1388        let wat = r#"
1389            (module
1390                (memory 1)
1391                (func (export "test") (param i32) (result i64)
1392                    local.get 0
1393                    i64.load8_s
1394                )
1395            )
1396        "#;
1397
1398        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1399        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1400
1401        assert_eq!(functions.len(), 1);
1402        assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
1403            offset: 0,
1404            align: 0,
1405        }));
1406    }
1407
1408    #[test]
1409    fn test_decode_all_subword_memory_ops() {
1410        // Test that all sub-word operations are decoded from WAT
1411        let wat = r#"
1412            (module
1413                (memory 1)
1414                (func (export "test") (param i32)
1415                    ;; i32 sub-word loads
1416                    local.get 0
1417                    i32.load8_s
1418                    drop
1419                    local.get 0
1420                    i32.load8_u
1421                    drop
1422                    local.get 0
1423                    i32.load16_s
1424                    drop
1425                    local.get 0
1426                    i32.load16_u
1427                    drop
1428
1429                    ;; i32 sub-word stores
1430                    local.get 0
1431                    i32.const 42
1432                    i32.store8
1433                    local.get 0
1434                    i32.const 42
1435                    i32.store16
1436
1437                    ;; i64 sub-word loads
1438                    local.get 0
1439                    i64.load8_s
1440                    drop
1441                    local.get 0
1442                    i64.load8_u
1443                    drop
1444                    local.get 0
1445                    i64.load16_s
1446                    drop
1447                    local.get 0
1448                    i64.load16_u
1449                    drop
1450                    local.get 0
1451                    i64.load32_s
1452                    drop
1453                    local.get 0
1454                    i64.load32_u
1455                    drop
1456
1457                    ;; i64 sub-word stores
1458                    local.get 0
1459                    i64.const 42
1460                    i64.store8
1461                    local.get 0
1462                    i64.const 42
1463                    i64.store16
1464                    local.get 0
1465                    i64.const 42
1466                    i64.store32
1467                )
1468            )
1469        "#;
1470
1471        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1472        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1473
1474        assert_eq!(functions.len(), 1);
1475        let ops = &functions[0].ops;
1476
1477        // Verify i32 sub-word ops are present
1478        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
1479        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
1480        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
1481        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
1482        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
1483        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
1484
1485        // Verify i64 sub-word ops are present
1486        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
1487        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
1488        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
1489        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
1490        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
1491        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
1492        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
1493        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
1494        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
1495    }
1496
1497    #[test]
1498    fn test_decode_simd_i32x4_add() {
1499        let wat = r#"
1500            (module
1501                (func (export "add_v128") (param v128 v128) (result v128)
1502                    local.get 0
1503                    local.get 1
1504                    i32x4.add
1505                )
1506            )
1507        "#;
1508
1509        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1510        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1511
1512        assert_eq!(functions.len(), 1);
1513        assert!(
1514            functions[0].ops.contains(&WasmOp::I32x4Add),
1515            "Should decode i32x4.add: {:?}",
1516            functions[0].ops
1517        );
1518    }
1519
1520    #[test]
1521    fn test_decode_simd_v128_const() {
1522        let wat = r#"
1523            (module
1524                (func (export "const_v128") (result v128)
1525                    v128.const i32x4 1 2 3 4
1526                )
1527            )
1528        "#;
1529
1530        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1531        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1532
1533        assert_eq!(functions.len(), 1);
1534        assert!(
1535            functions[0]
1536                .ops
1537                .iter()
1538                .any(|o| matches!(o, WasmOp::V128Const(_))),
1539            "Should decode v128.const: {:?}",
1540            functions[0].ops
1541        );
1542    }
1543
1544    #[test]
1545    fn test_decode_simd_v128_load_store() {
1546        let wat = r#"
1547            (module
1548                (memory 1)
1549                (func (export "load_store") (param i32)
1550                    local.get 0
1551                    v128.load
1552                    local.get 0
1553                    v128.store
1554                )
1555            )
1556        "#;
1557
1558        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1559        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1560
1561        assert_eq!(functions.len(), 1);
1562        let ops = &functions[0].ops;
1563        assert!(
1564            ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
1565            "Should decode v128.load"
1566        );
1567        assert!(
1568            ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
1569            "Should decode v128.store"
1570        );
1571    }
1572
1573    #[test]
1574    fn test_decode_simd_bitwise_ops() {
1575        let wat = r#"
1576            (module
1577                (func (export "bitwise") (param v128 v128) (result v128)
1578                    local.get 0
1579                    local.get 1
1580                    v128.and
1581                )
1582            )
1583        "#;
1584
1585        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1586        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1587
1588        assert_eq!(functions.len(), 1);
1589        assert!(functions[0].ops.contains(&WasmOp::V128And));
1590    }
1591
1592    #[test]
1593    fn test_decode_simd_splat() {
1594        let wat = r#"
1595            (module
1596                (func (export "splat") (param i32) (result v128)
1597                    local.get 0
1598                    i32x4.splat
1599                )
1600            )
1601        "#;
1602
1603        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1604        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1605
1606        assert_eq!(functions.len(), 1);
1607        assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
1608    }
1609
1610    #[test]
1611    fn test_decode_simd_extract_lane() {
1612        let wat = r#"
1613            (module
1614                (func (export "extract") (param v128) (result i32)
1615                    local.get 0
1616                    i32x4.extract_lane 2
1617                )
1618            )
1619        "#;
1620
1621        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1622        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1623
1624        assert_eq!(functions.len(), 1);
1625        assert!(
1626            functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
1627            "Should decode i32x4.extract_lane 2"
1628        );
1629    }
1630
1631    #[test]
1632    fn test_decode_simd_f32x4_arithmetic() {
1633        let wat = r#"
1634            (module
1635                (func (export "f32x4_add") (param v128 v128) (result v128)
1636                    local.get 0
1637                    local.get 1
1638                    f32x4.add
1639                )
1640            )
1641        "#;
1642
1643        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1644        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1645
1646        assert_eq!(functions.len(), 1);
1647        assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
1648    }
1649
1650    #[test]
1651    fn test_369_scalar_float_op_flags_function_unsupported_not_dropped() {
1652        // #369: a scalar f32/f64 op the decoder can't lower must FLAG the
1653        // function (-> loud skip), never be silently dropped (which left a
1654        // `mov r0,r1` wrong-value stub). A pure-integer function stays clean.
1655        let wat = r#"
1656            (module
1657                (func (export "fadd") (param f32 f32) (result f32)
1658                    local.get 0 local.get 1 f32.add)
1659                (func (export "iadd") (param i32 i32) (result i32)
1660                    local.get 0 local.get 1 i32.add))
1661        "#;
1662        let wasm = wat::parse_str(wat).expect("parse");
1663        let functions = decode_wasm_functions(&wasm).expect("decode");
1664        let fadd = functions
1665            .iter()
1666            .find(|f| f.export_name.as_deref() == Some("fadd"))
1667            .unwrap();
1668        let iadd = functions
1669            .iter()
1670            .find(|f| f.export_name.as_deref() == Some("iadd"))
1671            .unwrap();
1672        assert!(
1673            fadd.unsupported.is_some(),
1674            "f32.add must flag the function unsupported (loud-skip), got {:?}",
1675            fadd.unsupported
1676        );
1677        assert!(
1678            fadd.unsupported.as_deref().unwrap().contains("F32Add"),
1679            "diagnostic should name the op: {:?}",
1680            fadd.unsupported
1681        );
1682        assert!(
1683            iadd.unsupported.is_none(),
1684            "a pure-integer function must NOT be flagged: {:?}",
1685            iadd.unsupported
1686        );
1687    }
1688
1689    #[test]
1690    fn test_decode_simd_multiple_ops() {
1691        let wat = r#"
1692            (module
1693                (func (export "simd_ops") (param v128 v128 v128) (result v128)
1694                    ;; (a + b) * c
1695                    local.get 0
1696                    local.get 1
1697                    i32x4.add
1698                    local.get 2
1699                    i32x4.mul
1700                )
1701            )
1702        "#;
1703
1704        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1705        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1706
1707        assert_eq!(functions.len(), 1);
1708        let ops = &functions[0].ops;
1709        assert!(ops.contains(&WasmOp::I32x4Add));
1710        assert!(ops.contains(&WasmOp::I32x4Mul));
1711    }
1712
1713    /// VCR-DBG-001 step 1 (#394): the decoder records a module-relative wasm byte
1714    /// offset per emitted op — the DWARF-for-wasm address space that bridges
1715    /// synth's op-index `source_line` to the input wasm's `.debug_line`. Purely
1716    /// additive metadata (no codegen consumer ⇒ frozen fixtures byte-identical,
1717    /// verified separately); this test pins the structural invariants.
1718    #[test]
1719    fn test_decode_records_aligned_increasing_op_offsets_dbg001() {
1720        let wat = r#"
1721            (module
1722                (func (export "f") (param i32 i32) (result i32)
1723                    local.get 0
1724                    local.get 1
1725                    i32.add
1726                    i32.const 7
1727                    i32.mul))
1728        "#;
1729        let wasm = wat::parse_str(wat).expect("parse WAT");
1730        let functions = decode_wasm_functions(&wasm).expect("decode");
1731        let f = &functions[0];
1732
1733        // One offset per emitted op, index-aligned with `ops`.
1734        assert_eq!(
1735            f.op_offsets.len(),
1736            f.ops.len(),
1737            "op_offsets must be parallel to ops"
1738        );
1739        assert!(!f.op_offsets.is_empty());
1740
1741        // Byte offsets are strictly increasing through the body (each op consumes
1742        // at least one byte) and module-relative (well past the header).
1743        assert!(
1744            f.op_offsets.windows(2).all(|w| w[1] > w[0]),
1745            "wasm byte offsets must strictly increase: {:?}",
1746            f.op_offsets
1747        );
1748        assert!(
1749            f.op_offsets[0] >= 8,
1750            "module-relative offset is past the 8-byte wasm header"
1751        );
1752    }
1753
1754    /// #237: the decoder captures a global's `i32.const` initializer + mutability,
1755    /// so the native-pointer ABI can recognize the stack-pointer global.
1756    #[test]
1757    fn test_decode_captures_global_initializer() {
1758        let wat = r#"
1759            (module
1760                (memory 2)
1761                (global $__stack_pointer (mut i32) (i32.const 65536))
1762                (global $immutable_const i32 (i32.const 7))
1763                (func (export "f") (result i32) global.get 0)
1764            )
1765        "#;
1766        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1767        let module = decode_wasm_module(&wasm).expect("Failed to decode");
1768
1769        assert_eq!(module.globals.len(), 2, "both globals captured");
1770        let sp = &module.globals[0];
1771        assert_eq!(sp.index, 0);
1772        assert_eq!(sp.init_i32, Some(65536), "stack-pointer init captured");
1773        assert!(sp.mutable, "stack pointer is mutable");
1774        let c = &module.globals[1];
1775        assert_eq!(c.init_i32, Some(7));
1776        assert!(!c.mutable, "second global is immutable");
1777    }
1778
1779    /// #509: the decoder records `(param_count, result_count)` for every
1780    /// `Block`/`Loop`/`If`, ordinal-keyed in op order, covering all three
1781    /// blocktype encodings: `Empty → (0,0)`, `ValType → (0,1)`, and
1782    /// `FuncType(i) →` counts from the type section (here a multi-result
1783    /// block, which wat encodes as a functype blocktype).
1784    #[test]
1785    fn test_decode_records_block_arity_side_table_509() {
1786        let wat = r#"
1787            (module
1788                (func (export "f") (param i32) (result i32)
1789                    (block (result i32)
1790                        (block (nop))
1791                        (local.get 0)
1792                        (if (result i32)
1793                            (then (i32.const 1))
1794                            (else (i32.const 2)))))
1795                (func (export "g") (result i32)
1796                    (block (result i32 i32)
1797                        (i32.const 1) (i32.const 2))
1798                    i32.add)
1799                (func (export "h") (param i32) (result i32)
1800                    (local.get 0)
1801                    (loop (param i32) (result i32))))
1802        "#;
1803        let wasm = wat::parse_str(wat).expect("parse WAT");
1804
1805        // Both decode entry points must produce the same side-table.
1806        for functions in [
1807            decode_wasm_functions(&wasm).expect("decode"),
1808            decode_wasm_module(&wasm).expect("decode").functions,
1809        ] {
1810            // f: Block(result i32), Block(void), If(result i32) — in op order.
1811            assert_eq!(
1812                functions[0].block_arity,
1813                vec![(0, 1), (0, 0), (0, 1)],
1814                "f: ValType/Empty/ValType blocktypes"
1815            );
1816            // g: one multi-result block via a FuncType blocktype.
1817            assert_eq!(
1818                functions[1].block_arity,
1819                vec![(0, 2)],
1820                "g: functype blocktype result count from the type section"
1821            );
1822            // h: a parameterized loop — the input arity is what a br to the
1823            // header would carry (the #509 loud-decline discriminator).
1824            assert_eq!(
1825                functions[2].block_arity,
1826                vec![(1, 1)],
1827                "h: loop params captured"
1828            );
1829        }
1830    }
1831}