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