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