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