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    /// Defined globals with their initializers (#237). Empty if the module has
101    /// no global section. Used by the native-pointer ABI to make a global whose
102    /// initializer is a linear-memory address (e.g. `$__stack_pointer`)
103    /// self-contained rather than table-relative.
104    pub globals: Vec<WasmGlobal>,
105    /// Function indices that populate any table via an element segment (#275).
106    /// These are the possible `call_indirect` targets — a function reached only
107    /// through the table is invisible to direct-`call` reachability, so the
108    /// whole-graph closure must treat every table entry as reachable once any
109    /// reachable function performs a `call_indirect`. Empty for modules with no
110    /// element section (every leaf/direct-call module), keeping output identical.
111    pub elem_func_indices: Vec<u32>,
112}
113
114/// Decode a WASM binary and extract functions, memory, and data segments
115pub fn decode_wasm_module(wasm_bytes: &[u8]) -> Result<DecodedModule> {
116    let mut functions = Vec::new();
117    let mut memories = Vec::new();
118    let mut data_segments = Vec::new();
119    let mut globals: Vec<WasmGlobal> = Vec::new();
120    let mut imports = Vec::new();
121    let mut func_index = 0u32;
122    let mut num_imported_funcs = 0u32;
123    let mut export_names: HashMap<u32, String> = HashMap::new();
124    // #195: per-type AAPCS arg count (indexed by type index) and per-function
125    // arg count (indexed by full function index: imports first, then locals).
126    let mut type_arg_counts: Vec<u32> = Vec::new();
127    let mut func_arg_counts: Vec<u32> = Vec::new();
128    let mut elem_func_indices: Vec<u32> = Vec::new();
129
130    for payload in Parser::new(0).parse_all(wasm_bytes) {
131        let payload = payload.context("Failed to parse WASM payload")?;
132
133        match payload {
134            Payload::TypeSection(reader) => {
135                // Record the parameter count of each function type so calls can
136                // marshal the right number of arguments (issue #195).
137                for rec_group in reader {
138                    let rec_group = rec_group.context("Failed to parse type")?;
139                    for sub_ty in rec_group.types() {
140                        let count = match &sub_ty.composite_type.inner {
141                            wasmparser::CompositeInnerType::Func(func_ty) => {
142                                func_ty.params().len() as u32
143                            }
144                            _ => 0,
145                        };
146                        type_arg_counts.push(count);
147                    }
148                }
149            }
150            Payload::ImportSection(reader) => {
151                for import in reader {
152                    let import = import.context("Failed to parse import")?;
153                    let (kind, idx) = match import.ty {
154                        wasmparser::TypeRef::Func(type_idx) => {
155                            let idx = num_imported_funcs;
156                            num_imported_funcs += 1;
157                            // Record the imported function's arg count at its
158                            // full function index (imports come first).
159                            func_arg_counts
160                                .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
161                            (ImportKind::Function(type_idx), idx)
162                        }
163                        wasmparser::TypeRef::Memory(_) => (ImportKind::Memory, 0),
164                        wasmparser::TypeRef::Table(_) => (ImportKind::Table, 0),
165                        wasmparser::TypeRef::Global(_) => (ImportKind::Global, 0),
166                        _ => continue,
167                    };
168                    imports.push(ImportEntry {
169                        module: import.module.to_string(),
170                        name: import.name.to_string(),
171                        kind,
172                        index: idx,
173                    });
174                }
175            }
176            Payload::FunctionSection(reader) => {
177                // Each entry gives the type index of a locally-defined function,
178                // in order. Their full function indices follow the imports, so
179                // appending to `func_arg_counts` keeps it indexed by full index
180                // (issue #195).
181                for ty in reader {
182                    let type_idx = ty.context("Failed to parse function type index")?;
183                    func_arg_counts
184                        .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
185                }
186            }
187            Payload::MemorySection(reader) => {
188                for (idx, memory) in reader.into_iter().enumerate() {
189                    let mem = memory.context("Failed to parse memory")?;
190                    memories.push(WasmMemory {
191                        index: idx as u32,
192                        initial_pages: mem.initial as u32,
193                        max_pages: mem.maximum.map(|m| m as u32),
194                        shared: mem.shared,
195                    });
196                }
197            }
198            Payload::GlobalSection(reader) => {
199                // #237: capture each defined global's i32 initializer + mutability.
200                // The init is a const expr; we only decode a leading `i32.const`
201                // (the shape `$__stack_pointer`/data-layout globals use). Anything
202                // else (global.get, f32/f64, etc.) records `init_i32: None` and is
203                // left to the table-relative path.
204                for (idx, global) in reader.into_iter().enumerate() {
205                    let global = global.context("Failed to parse global")?;
206                    let mut init_i32 = None;
207                    let mut ops = global.init_expr.get_operators_reader();
208                    if let Ok(wasmparser::Operator::I32Const { value }) = ops.read() {
209                        init_i32 = Some(value);
210                    }
211                    globals.push(WasmGlobal {
212                        index: idx as u32,
213                        init_i32,
214                        mutable: global.ty.mutable,
215                    });
216                }
217            }
218            Payload::DataSection(reader) => {
219                for data in reader {
220                    let data = data.context("Failed to parse data segment")?;
221                    if let wasmparser::DataKind::Active {
222                        memory_index: 0,
223                        offset_expr,
224                    } = data.kind
225                    {
226                        let mut ops = offset_expr.get_operators_reader();
227                        if let Ok(wasmparser::Operator::I32Const { value }) = ops.read() {
228                            data_segments.push((value as u32, data.data.to_vec()));
229                        }
230                    }
231                }
232            }
233            Payload::ElementSection(reader) => {
234                // #275: collect every function index that initializes a table.
235                // These are the `call_indirect` targets the direct-call closure
236                // cannot see; `reachable_from_exports` unions them in when a
237                // reachable function does a `call_indirect`. Both element forms
238                // are handled: a flat function-index list, and the const-expr
239                // form whose `ref.func` entries name the functions.
240                for elem in reader {
241                    let elem = elem.context("Failed to parse element segment")?;
242                    match elem.items {
243                        wasmparser::ElementItems::Functions(funcs) => {
244                            for f in funcs {
245                                elem_func_indices
246                                    .push(f.context("Failed to parse element func index")?);
247                            }
248                        }
249                        wasmparser::ElementItems::Expressions(_, exprs) => {
250                            for expr in exprs {
251                                let expr = expr.context("Failed to parse element expr")?;
252                                for op in expr.get_operators_reader() {
253                                    if let wasmparser::Operator::RefFunc { function_index } =
254                                        op.context("Failed to parse element op")?
255                                    {
256                                        elem_func_indices.push(function_index);
257                                    }
258                                }
259                            }
260                        }
261                    }
262                }
263            }
264            Payload::ExportSection(exports) => {
265                for export in exports {
266                    let export = export.context("Failed to parse export")?;
267                    if export.kind == ExternalKind::Func {
268                        export_names.insert(export.index, export.name.to_string());
269                    }
270                }
271            }
272            Payload::CodeSectionEntry(body) => {
273                let ops = decode_function_body(&body)?;
274                let actual_index = num_imported_funcs + func_index;
275                let export_name = export_names.get(&actual_index).cloned();
276
277                functions.push(FunctionOps {
278                    index: actual_index,
279                    export_name,
280                    ops,
281                });
282                func_index += 1;
283            }
284            _ => {}
285        }
286    }
287
288    Ok(DecodedModule {
289        functions,
290        memories,
291        data_segments,
292        imports,
293        num_imported_funcs,
294        func_arg_counts,
295        type_arg_counts,
296        globals,
297        elem_func_indices,
298    })
299}
300
301/// Decode a WASM binary and extract all function bodies as WasmOp sequences
302pub fn decode_wasm_functions(wasm_bytes: &[u8]) -> Result<Vec<FunctionOps>> {
303    let mut functions = Vec::new();
304    let mut func_index = 0u32;
305    let mut num_imported_funcs = 0u32;
306    let mut export_names: HashMap<u32, String> = HashMap::new();
307
308    for payload in Parser::new(0).parse_all(wasm_bytes) {
309        let payload = payload.context("Failed to parse WASM payload")?;
310
311        match payload {
312            Payload::ImportSection(imports) => {
313                for import in imports {
314                    let import = import.context("Failed to parse import")?;
315                    if matches!(import.ty, wasmparser::TypeRef::Func(_)) {
316                        num_imported_funcs += 1;
317                    }
318                }
319            }
320            Payload::ExportSection(exports) => {
321                for export in exports {
322                    let export = export.context("Failed to parse export")?;
323                    if export.kind == ExternalKind::Func {
324                        export_names.insert(export.index, export.name.to_string());
325                    }
326                }
327            }
328            Payload::CodeSectionEntry(body) => {
329                let ops = decode_function_body(&body)?;
330                let actual_index = num_imported_funcs + func_index;
331                let export_name = export_names.get(&actual_index).cloned();
332
333                functions.push(FunctionOps {
334                    index: actual_index,
335                    export_name,
336                    ops,
337                });
338                func_index += 1;
339            }
340            _ => {}
341        }
342    }
343
344    Ok(functions)
345}
346
347/// Decoded function with its WasmOp sequence
348#[derive(Debug, Clone)]
349pub struct FunctionOps {
350    /// Function index in the module (includes imported functions)
351    pub index: u32,
352    /// Export name if this function is exported
353    pub export_name: Option<String>,
354    /// The WASM operations in this function body
355    pub ops: Vec<WasmOp>,
356}
357
358/// Decode a single function body to WasmOp sequence
359fn decode_function_body(body: &wasmparser::FunctionBody) -> Result<Vec<WasmOp>> {
360    let mut ops = Vec::new();
361
362    let ops_reader = body.get_operators_reader()?;
363    for op_result in ops_reader {
364        let op = op_result.context("Failed to read operator")?;
365
366        if let Some(wasm_op) = convert_operator(&op) {
367            ops.push(wasm_op);
368        }
369    }
370
371    Ok(ops)
372}
373
374/// Convert a wasmparser Operator to our WasmOp enum
375fn convert_operator(op: &wasmparser::Operator) -> Option<WasmOp> {
376    use wasmparser::Operator::*;
377
378    match op {
379        // Constants
380        I32Const { value } => Some(WasmOp::I32Const(*value)),
381
382        // i32 Arithmetic
383        I32Add => Some(WasmOp::I32Add),
384        I32Sub => Some(WasmOp::I32Sub),
385        I32Mul => Some(WasmOp::I32Mul),
386        I32DivS => Some(WasmOp::I32DivS),
387        I32DivU => Some(WasmOp::I32DivU),
388        I32RemS => Some(WasmOp::I32RemS),
389        I32RemU => Some(WasmOp::I32RemU),
390
391        // i64 Constants
392        I64Const { value } => Some(WasmOp::I64Const(*value)),
393
394        // i64 Arithmetic
395        I64Add => Some(WasmOp::I64Add),
396        I64Sub => Some(WasmOp::I64Sub),
397        I64Mul => Some(WasmOp::I64Mul),
398        I64DivS => Some(WasmOp::I64DivS),
399        I64DivU => Some(WasmOp::I64DivU),
400        I64RemS => Some(WasmOp::I64RemS),
401        I64RemU => Some(WasmOp::I64RemU),
402
403        // i64 Bitwise
404        I64And => Some(WasmOp::I64And),
405        I64Or => Some(WasmOp::I64Or),
406        I64Xor => Some(WasmOp::I64Xor),
407        I64Shl => Some(WasmOp::I64Shl),
408        I64ShrS => Some(WasmOp::I64ShrS),
409        I64ShrU => Some(WasmOp::I64ShrU),
410        I64Rotl => Some(WasmOp::I64Rotl),
411        I64Rotr => Some(WasmOp::I64Rotr),
412        I64Clz => Some(WasmOp::I64Clz),
413        I64Ctz => Some(WasmOp::I64Ctz),
414        I64Popcnt => Some(WasmOp::I64Popcnt),
415        I64Extend8S => Some(WasmOp::I64Extend8S),
416        I64Extend16S => Some(WasmOp::I64Extend16S),
417        I64Extend32S => Some(WasmOp::I64Extend32S),
418        // i32<->i64 width conversions. Previously UNMAPPED → silently dropped,
419        // which left an i32 value as a 64-bit operand with a garbage high half
420        // (harmless when a following `i64.shl 32` discards it, but a latent
421        // miscompile for extend-then-arithmetic, and it breaks width-correct
422        // register allocation). (#204)
423        I64ExtendI32U => Some(WasmOp::I64ExtendI32U),
424        I64ExtendI32S => Some(WasmOp::I64ExtendI32S),
425        I32WrapI64 => Some(WasmOp::I32WrapI64),
426
427        // i64 Comparison
428        I64Eqz => Some(WasmOp::I64Eqz),
429        I64Eq => Some(WasmOp::I64Eq),
430        I64Ne => Some(WasmOp::I64Ne),
431        I64LtS => Some(WasmOp::I64LtS),
432        I64LtU => Some(WasmOp::I64LtU),
433        I64LeS => Some(WasmOp::I64LeS),
434        I64LeU => Some(WasmOp::I64LeU),
435        I64GtS => Some(WasmOp::I64GtS),
436        I64GtU => Some(WasmOp::I64GtU),
437        I64GeS => Some(WasmOp::I64GeS),
438        I64GeU => Some(WasmOp::I64GeU),
439
440        // Bitwise
441        I32And => Some(WasmOp::I32And),
442        I32Or => Some(WasmOp::I32Or),
443        I32Xor => Some(WasmOp::I32Xor),
444        I32Shl => Some(WasmOp::I32Shl),
445        I32ShrS => Some(WasmOp::I32ShrS),
446        I32ShrU => Some(WasmOp::I32ShrU),
447        I32Rotl => Some(WasmOp::I32Rotl),
448        I32Rotr => Some(WasmOp::I32Rotr),
449        I32Clz => Some(WasmOp::I32Clz),
450        I32Ctz => Some(WasmOp::I32Ctz),
451        I32Popcnt => Some(WasmOp::I32Popcnt),
452        I32Extend8S => Some(WasmOp::I32Extend8S),
453        I32Extend16S => Some(WasmOp::I32Extend16S),
454
455        // Comparison
456        I32Eqz => Some(WasmOp::I32Eqz),
457        I32Eq => Some(WasmOp::I32Eq),
458        I32Ne => Some(WasmOp::I32Ne),
459        I32LtS => Some(WasmOp::I32LtS),
460        I32LtU => Some(WasmOp::I32LtU),
461        I32LeS => Some(WasmOp::I32LeS),
462        I32LeU => Some(WasmOp::I32LeU),
463        I32GtS => Some(WasmOp::I32GtS),
464        I32GtU => Some(WasmOp::I32GtU),
465        I32GeS => Some(WasmOp::I32GeS),
466        I32GeU => Some(WasmOp::I32GeU),
467
468        // Memory
469        I32Load { memarg } => Some(WasmOp::I32Load {
470            offset: memarg.offset as u32,
471            align: memarg.align as u32,
472        }),
473        I32Store { memarg } => Some(WasmOp::I32Store {
474            offset: memarg.offset as u32,
475            align: memarg.align as u32,
476        }),
477
478        // Sub-word loads (i32)
479        I32Load8S { memarg } => Some(WasmOp::I32Load8S {
480            offset: memarg.offset as u32,
481            align: memarg.align as u32,
482        }),
483        I32Load8U { memarg } => Some(WasmOp::I32Load8U {
484            offset: memarg.offset as u32,
485            align: memarg.align as u32,
486        }),
487        I32Load16S { memarg } => Some(WasmOp::I32Load16S {
488            offset: memarg.offset as u32,
489            align: memarg.align as u32,
490        }),
491        I32Load16U { memarg } => Some(WasmOp::I32Load16U {
492            offset: memarg.offset as u32,
493            align: memarg.align as u32,
494        }),
495
496        // Sub-word stores (i32)
497        I32Store8 { memarg } => Some(WasmOp::I32Store8 {
498            offset: memarg.offset as u32,
499            align: memarg.align as u32,
500        }),
501        I32Store16 { memarg } => Some(WasmOp::I32Store16 {
502            offset: memarg.offset as u32,
503            align: memarg.align as u32,
504        }),
505
506        // Local/Global
507        LocalGet { local_index } => Some(WasmOp::LocalGet(*local_index)),
508        LocalSet { local_index } => Some(WasmOp::LocalSet(*local_index)),
509        LocalTee { local_index } => Some(WasmOp::LocalTee(*local_index)),
510        GlobalGet { global_index } => Some(WasmOp::GlobalGet(*global_index)),
511        GlobalSet { global_index } => Some(WasmOp::GlobalSet(*global_index)),
512
513        // Control flow
514        Block { .. } => Some(WasmOp::Block),
515        Loop { .. } => Some(WasmOp::Loop),
516        Br { relative_depth } => Some(WasmOp::Br(*relative_depth)),
517        BrIf { relative_depth } => Some(WasmOp::BrIf(*relative_depth)),
518        // br_table: indexed multi-way branch. Previously UNMAPPED → silently
519        // dropped, so the selector never emitted the index dispatch and control
520        // fell straight into the first table arm — every br_table behaved as if
521        // it always took target 0 (gale's binary-sem WAKE path never fired). The
522        // jump-table relative depths + default depth are preserved in order.
523        BrTable { targets } => {
524            let default = targets.default();
525            let tgts: Vec<u32> = targets.targets().filter_map(Result::ok).collect();
526            Some(WasmOp::BrTable {
527                targets: tgts,
528                default,
529            })
530        }
531        Return => Some(WasmOp::Return),
532        Call { function_index } => Some(WasmOp::Call(*function_index)),
533        CallIndirect {
534            type_index,
535            table_index,
536            ..
537        } => Some(WasmOp::CallIndirect {
538            type_index: *type_index,
539            table_index: *table_index,
540        }),
541
542        // End is needed for control flow pattern matching
543        End => Some(WasmOp::End),
544
545        // Nop/Unreachable - skip these
546        Nop | Unreachable => None,
547
548        // Drop is needed for br_if pattern matching
549        Drop => Some(WasmOp::Drop),
550
551        // Select
552        Select => Some(WasmOp::Select),
553
554        // If/Else - simplified handling
555        If { .. } => Some(WasmOp::If),
556        Else => Some(WasmOp::Else),
557
558        // i64 sub-word loads
559        I64Load8S { memarg } => Some(WasmOp::I64Load8S {
560            offset: memarg.offset as u32,
561            align: memarg.align as u32,
562        }),
563        I64Load8U { memarg } => Some(WasmOp::I64Load8U {
564            offset: memarg.offset as u32,
565            align: memarg.align as u32,
566        }),
567        I64Load16S { memarg } => Some(WasmOp::I64Load16S {
568            offset: memarg.offset as u32,
569            align: memarg.align as u32,
570        }),
571        I64Load16U { memarg } => Some(WasmOp::I64Load16U {
572            offset: memarg.offset as u32,
573            align: memarg.align as u32,
574        }),
575        I64Load32S { memarg } => Some(WasmOp::I64Load32S {
576            offset: memarg.offset as u32,
577            align: memarg.align as u32,
578        }),
579        I64Load32U { memarg } => Some(WasmOp::I64Load32U {
580            offset: memarg.offset as u32,
581            align: memarg.align as u32,
582        }),
583
584        // i64 sub-word stores
585        I64Store8 { memarg } => Some(WasmOp::I64Store8 {
586            offset: memarg.offset as u32,
587            align: memarg.align as u32,
588        }),
589        I64Store16 { memarg } => Some(WasmOp::I64Store16 {
590            offset: memarg.offset as u32,
591            align: memarg.align as u32,
592        }),
593        I64Store32 { memarg } => Some(WasmOp::I64Store32 {
594            offset: memarg.offset as u32,
595            align: memarg.align as u32,
596        }),
597
598        // Memory management
599        MemorySize { mem, .. } => Some(WasmOp::MemorySize(*mem)),
600        MemoryGrow { mem, .. } => Some(WasmOp::MemoryGrow(*mem)),
601
602        // ========================================================================
603        // v128 SIMD operations (WASM SIMD proposal, 0xFD prefix)
604        // ========================================================================
605        V128Const { value } => {
606            let mut bytes = [0u8; 16];
607            bytes.copy_from_slice(value.bytes());
608            Some(WasmOp::V128Const(bytes))
609        }
610        V128Load { memarg } => Some(WasmOp::V128Load {
611            offset: memarg.offset as u32,
612            align: memarg.align as u32,
613        }),
614        V128Store { memarg } => Some(WasmOp::V128Store {
615            offset: memarg.offset as u32,
616            align: memarg.align as u32,
617        }),
618
619        // v128 bitwise
620        V128And => Some(WasmOp::V128And),
621        V128Or => Some(WasmOp::V128Or),
622        V128Xor => Some(WasmOp::V128Xor),
623        V128Not => Some(WasmOp::V128Not),
624        V128AndNot => Some(WasmOp::V128AndNot),
625
626        // i8x16
627        I8x16Add => Some(WasmOp::I8x16Add),
628        I8x16Sub => Some(WasmOp::I8x16Sub),
629        I8x16Neg => Some(WasmOp::I8x16Neg),
630        I8x16Eq => Some(WasmOp::I8x16Eq),
631        I8x16Ne => Some(WasmOp::I8x16Ne),
632        I8x16LtS => Some(WasmOp::I8x16LtS),
633        I8x16LtU => Some(WasmOp::I8x16LtU),
634        I8x16GtS => Some(WasmOp::I8x16GtS),
635        I8x16GtU => Some(WasmOp::I8x16GtU),
636        I8x16LeS => Some(WasmOp::I8x16LeS),
637        I8x16LeU => Some(WasmOp::I8x16LeU),
638        I8x16GeS => Some(WasmOp::I8x16GeS),
639        I8x16GeU => Some(WasmOp::I8x16GeU),
640        I8x16Splat => Some(WasmOp::I8x16Splat),
641        I8x16ExtractLaneS { lane } => Some(WasmOp::I8x16ExtractLaneS(*lane)),
642        I8x16ExtractLaneU { lane } => Some(WasmOp::I8x16ExtractLaneU(*lane)),
643        I8x16ReplaceLane { lane } => Some(WasmOp::I8x16ReplaceLane(*lane)),
644        I8x16Shuffle { lanes } => Some(WasmOp::I8x16Shuffle(*lanes)),
645        I8x16Swizzle => Some(WasmOp::I8x16Swizzle),
646
647        // i16x8
648        I16x8Add => Some(WasmOp::I16x8Add),
649        I16x8Sub => Some(WasmOp::I16x8Sub),
650        I16x8Mul => Some(WasmOp::I16x8Mul),
651        I16x8Neg => Some(WasmOp::I16x8Neg),
652        I16x8Eq => Some(WasmOp::I16x8Eq),
653        I16x8Ne => Some(WasmOp::I16x8Ne),
654        I16x8LtS => Some(WasmOp::I16x8LtS),
655        I16x8LtU => Some(WasmOp::I16x8LtU),
656        I16x8GtS => Some(WasmOp::I16x8GtS),
657        I16x8GtU => Some(WasmOp::I16x8GtU),
658        I16x8LeS => Some(WasmOp::I16x8LeS),
659        I16x8LeU => Some(WasmOp::I16x8LeU),
660        I16x8GeS => Some(WasmOp::I16x8GeS),
661        I16x8GeU => Some(WasmOp::I16x8GeU),
662        I16x8Splat => Some(WasmOp::I16x8Splat),
663        I16x8ExtractLaneS { lane } => Some(WasmOp::I16x8ExtractLaneS(*lane)),
664        I16x8ExtractLaneU { lane } => Some(WasmOp::I16x8ExtractLaneU(*lane)),
665        I16x8ReplaceLane { lane } => Some(WasmOp::I16x8ReplaceLane(*lane)),
666
667        // i32x4
668        I32x4Add => Some(WasmOp::I32x4Add),
669        I32x4Sub => Some(WasmOp::I32x4Sub),
670        I32x4Mul => Some(WasmOp::I32x4Mul),
671        I32x4Neg => Some(WasmOp::I32x4Neg),
672        I32x4Eq => Some(WasmOp::I32x4Eq),
673        I32x4Ne => Some(WasmOp::I32x4Ne),
674        I32x4LtS => Some(WasmOp::I32x4LtS),
675        I32x4LtU => Some(WasmOp::I32x4LtU),
676        I32x4GtS => Some(WasmOp::I32x4GtS),
677        I32x4GtU => Some(WasmOp::I32x4GtU),
678        I32x4LeS => Some(WasmOp::I32x4LeS),
679        I32x4LeU => Some(WasmOp::I32x4LeU),
680        I32x4GeS => Some(WasmOp::I32x4GeS),
681        I32x4GeU => Some(WasmOp::I32x4GeU),
682        I32x4Splat => Some(WasmOp::I32x4Splat),
683        I32x4ExtractLane { lane } => Some(WasmOp::I32x4ExtractLane(*lane)),
684        I32x4ReplaceLane { lane } => Some(WasmOp::I32x4ReplaceLane(*lane)),
685
686        // i64x2
687        I64x2Add => Some(WasmOp::I64x2Add),
688        I64x2Sub => Some(WasmOp::I64x2Sub),
689        I64x2Mul => Some(WasmOp::I64x2Mul),
690        I64x2Neg => Some(WasmOp::I64x2Neg),
691        I64x2Eq => Some(WasmOp::I64x2Eq),
692        I64x2Ne => Some(WasmOp::I64x2Ne),
693        I64x2LtS => Some(WasmOp::I64x2LtS),
694        I64x2GtS => Some(WasmOp::I64x2GtS),
695        I64x2LeS => Some(WasmOp::I64x2LeS),
696        I64x2GeS => Some(WasmOp::I64x2GeS),
697        I64x2Splat => Some(WasmOp::I64x2Splat),
698        I64x2ExtractLane { lane } => Some(WasmOp::I64x2ExtractLane(*lane)),
699        I64x2ReplaceLane { lane } => Some(WasmOp::I64x2ReplaceLane(*lane)),
700
701        // f32x4
702        F32x4Add => Some(WasmOp::F32x4Add),
703        F32x4Sub => Some(WasmOp::F32x4Sub),
704        F32x4Mul => Some(WasmOp::F32x4Mul),
705        F32x4Div => Some(WasmOp::F32x4Div),
706        F32x4Abs => Some(WasmOp::F32x4Abs),
707        F32x4Neg => Some(WasmOp::F32x4Neg),
708        F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
709        F32x4Eq => Some(WasmOp::F32x4Eq),
710        F32x4Ne => Some(WasmOp::F32x4Ne),
711        F32x4Lt => Some(WasmOp::F32x4Lt),
712        F32x4Le => Some(WasmOp::F32x4Le),
713        F32x4Gt => Some(WasmOp::F32x4Gt),
714        F32x4Ge => Some(WasmOp::F32x4Ge),
715        F32x4Splat => Some(WasmOp::F32x4Splat),
716        F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
717        F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
718
719        // Other operators not yet supported
720        _ => None,
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727
728    #[test]
729    fn test_decode_simple_add() {
730        let wat = r#"
731            (module
732                (func (export "add") (param i32 i32) (result i32)
733                    local.get 0
734                    local.get 1
735                    i32.add
736                )
737            )
738        "#;
739
740        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
741        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
742
743        assert_eq!(functions.len(), 1);
744        assert_eq!(functions[0].index, 0);
745        assert_eq!(functions[0].export_name, Some("add".to_string()));
746        assert_eq!(
747            functions[0].ops,
748            vec![
749                WasmOp::LocalGet(0),
750                WasmOp::LocalGet(1),
751                WasmOp::I32Add,
752                WasmOp::End
753            ]
754        );
755    }
756
757    /// #204 regression: `i64.extend_i32_u`, `i64.extend_i32_s` and
758    /// `i32.wrap_i64` must DECODE (they were previously unmapped → silently
759    /// dropped by `convert_operator`, leaving an i32 value as a 64-bit operand
760    /// with a garbage high half — the root cause of gale's miscompiled
761    /// `(new_count << 32)` pack). The decoder must surface all three.
762    #[test]
763    fn test_decode_i64_i32_width_conversions() {
764        let wat = r#"
765            (module
766                (func (export "conv") (param i32 i64) (result i32)
767                    local.get 0
768                    i64.extend_i32_u
769                    local.get 0
770                    i64.extend_i32_s
771                    i64.add
772                    local.get 1
773                    i64.add
774                    i32.wrap_i64
775                )
776            )
777        "#;
778        let wasm = wat::parse_str(wat).expect("parse");
779        let functions = decode_wasm_functions(&wasm).expect("decode");
780        let ops = &functions[0].ops;
781        assert!(
782            ops.contains(&WasmOp::I64ExtendI32U),
783            "i64.extend_i32_u must decode (not be dropped): {ops:?}"
784        );
785        assert!(
786            ops.contains(&WasmOp::I64ExtendI32S),
787            "i64.extend_i32_s must decode (not be dropped): {ops:?}"
788        );
789        assert!(
790            ops.contains(&WasmOp::I32WrapI64),
791            "i32.wrap_i64 must decode (not be dropped): {ops:?}"
792        );
793    }
794
795    /// #204 WAKE-path regression: `br_table` must DECODE (it was unmapped in
796    /// `convert_operator` → silently dropped, so the selector emitted no index
797    /// dispatch and every `br_table` fell through to target 0 — gale's binary
798    /// semaphore never took its WAKE branch). Targets + default are preserved.
799    #[test]
800    fn test_decode_br_table() {
801        let wat = r#"
802            (module
803                (func (export "bt") (param i32) (result i32)
804                    (block (block (block
805                        local.get 0
806                        br_table 2 0 1 2)
807                      i32.const 30 return)
808                      i32.const 20 return)
809                    i32.const 10))
810        "#;
811        let wasm = wat::parse_str(wat).expect("parse");
812        let functions = decode_wasm_functions(&wasm).expect("decode");
813        let bt = functions[0]
814            .ops
815            .iter()
816            .find_map(|o| match o {
817                WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
818                _ => None,
819            })
820            .expect("br_table must decode (not be dropped)");
821        assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
822        assert_eq!(bt.1, 2, "br_table default preserved");
823    }
824
825    #[test]
826    fn test_decode_arithmetic() {
827        let wat = r#"
828            (module
829                (func (export "calc") (result i32)
830                    i32.const 5
831                    i32.const 3
832                    i32.mul
833                    i32.const 2
834                    i32.add
835                )
836            )
837        "#;
838
839        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
840        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
841
842        assert_eq!(functions.len(), 1);
843        assert_eq!(functions[0].export_name, Some("calc".to_string()));
844        assert_eq!(
845            functions[0].ops,
846            vec![
847                WasmOp::I32Const(5),
848                WasmOp::I32Const(3),
849                WasmOp::I32Mul,
850                WasmOp::I32Const(2),
851                WasmOp::I32Add,
852                WasmOp::End,
853            ]
854        );
855    }
856
857    #[test]
858    fn test_decode_multi_function_module() {
859        let wat = r#"
860            (module
861                (func $helper)
862                (func (export "add") (param i32 i32) (result i32)
863                    local.get 0
864                    local.get 1
865                    i32.add
866                )
867                (func (export "sub") (param i32 i32) (result i32)
868                    local.get 0
869                    local.get 1
870                    i32.sub
871                )
872            )
873        "#;
874
875        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
876        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
877
878        assert_eq!(functions.len(), 3);
879        assert_eq!(functions[0].index, 0);
880        assert_eq!(functions[0].export_name, None);
881        assert_eq!(functions[1].index, 1);
882        assert_eq!(functions[1].export_name, Some("add".to_string()));
883        assert_eq!(functions[2].index, 2);
884        assert_eq!(functions[2].export_name, Some("sub".to_string()));
885    }
886
887    #[test]
888    fn test_decode_module_with_imports() {
889        let wat = r#"
890            (module
891                (import "env" "log" (func $log (param i32)))
892                (import "env" "memory" (memory 1))
893                (func (export "run") (param i32)
894                    local.get 0
895                    call 0
896                )
897            )
898        "#;
899
900        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
901        let module = decode_wasm_module(&wasm).expect("Failed to decode");
902
903        // Should have 2 imports (1 func, 1 memory)
904        assert_eq!(module.imports.len(), 2);
905        assert_eq!(module.num_imported_funcs, 1);
906
907        // First import is the function
908        assert_eq!(module.imports[0].module, "env");
909        assert_eq!(module.imports[0].name, "log");
910        assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
911
912        // Second import is memory
913        assert_eq!(module.imports[1].module, "env");
914        assert_eq!(module.imports[1].name, "memory");
915        assert_eq!(module.imports[1].kind, ImportKind::Memory);
916
917        // Should have 1 local function (index 1, because import is index 0)
918        assert_eq!(module.functions.len(), 1);
919        assert_eq!(module.functions[0].index, 1);
920        assert_eq!(module.functions[0].export_name, Some("run".to_string()));
921    }
922
923    #[test]
924    fn test_find_function_by_export_name() {
925        let wat = r#"
926            (module
927                (func $helper)
928                (func (export "add") (param i32 i32) (result i32)
929                    local.get 0
930                    local.get 1
931                    i32.add
932                )
933            )
934        "#;
935
936        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
937        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
938
939        let add_func = functions
940            .iter()
941            .find(|f| f.export_name.as_deref() == Some("add"))
942            .expect("Should find 'add' function");
943
944        assert_eq!(add_func.index, 1);
945        assert!(add_func.ops.contains(&WasmOp::I32Add));
946    }
947
948    #[test]
949    fn test_decode_subword_loads() {
950        let wat = r#"
951            (module
952                (memory 1)
953                (func (export "test") (param i32) (result i32)
954                    local.get 0
955                    i32.load8_u
956                )
957            )
958        "#;
959
960        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
961        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
962
963        assert_eq!(functions.len(), 1);
964        assert!(functions[0].ops.contains(&WasmOp::I32Load8U {
965            offset: 0,
966            align: 0,
967        }));
968    }
969
970    #[test]
971    fn test_decode_subword_stores() {
972        let wat = r#"
973            (module
974                (memory 1)
975                (func (export "test") (param i32 i32)
976                    local.get 0
977                    local.get 1
978                    i32.store8
979                )
980            )
981        "#;
982
983        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
984        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
985
986        assert_eq!(functions.len(), 1);
987        assert!(functions[0].ops.contains(&WasmOp::I32Store8 {
988            offset: 0,
989            align: 0,
990        }));
991    }
992
993    #[test]
994    fn test_decode_memory_size_grow() {
995        let wat = r#"
996            (module
997                (memory 1)
998                (func (export "test") (result i32)
999                    memory.size
1000                )
1001            )
1002        "#;
1003
1004        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1005        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1006
1007        assert_eq!(functions.len(), 1);
1008        assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
1009    }
1010
1011    #[test]
1012    fn test_decode_memory_grow() {
1013        let wat = r#"
1014            (module
1015                (memory 1)
1016                (func (export "test") (param i32) (result i32)
1017                    local.get 0
1018                    memory.grow
1019                )
1020            )
1021        "#;
1022
1023        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1024        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1025
1026        assert_eq!(functions.len(), 1);
1027        assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
1028    }
1029
1030    #[test]
1031    fn test_decode_i64_subword_loads() {
1032        let wat = r#"
1033            (module
1034                (memory 1)
1035                (func (export "test") (param i32) (result i64)
1036                    local.get 0
1037                    i64.load8_s
1038                )
1039            )
1040        "#;
1041
1042        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1043        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1044
1045        assert_eq!(functions.len(), 1);
1046        assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
1047            offset: 0,
1048            align: 0,
1049        }));
1050    }
1051
1052    #[test]
1053    fn test_decode_all_subword_memory_ops() {
1054        // Test that all sub-word operations are decoded from WAT
1055        let wat = r#"
1056            (module
1057                (memory 1)
1058                (func (export "test") (param i32)
1059                    ;; i32 sub-word loads
1060                    local.get 0
1061                    i32.load8_s
1062                    drop
1063                    local.get 0
1064                    i32.load8_u
1065                    drop
1066                    local.get 0
1067                    i32.load16_s
1068                    drop
1069                    local.get 0
1070                    i32.load16_u
1071                    drop
1072
1073                    ;; i32 sub-word stores
1074                    local.get 0
1075                    i32.const 42
1076                    i32.store8
1077                    local.get 0
1078                    i32.const 42
1079                    i32.store16
1080
1081                    ;; i64 sub-word loads
1082                    local.get 0
1083                    i64.load8_s
1084                    drop
1085                    local.get 0
1086                    i64.load8_u
1087                    drop
1088                    local.get 0
1089                    i64.load16_s
1090                    drop
1091                    local.get 0
1092                    i64.load16_u
1093                    drop
1094                    local.get 0
1095                    i64.load32_s
1096                    drop
1097                    local.get 0
1098                    i64.load32_u
1099                    drop
1100
1101                    ;; i64 sub-word stores
1102                    local.get 0
1103                    i64.const 42
1104                    i64.store8
1105                    local.get 0
1106                    i64.const 42
1107                    i64.store16
1108                    local.get 0
1109                    i64.const 42
1110                    i64.store32
1111                )
1112            )
1113        "#;
1114
1115        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1116        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1117
1118        assert_eq!(functions.len(), 1);
1119        let ops = &functions[0].ops;
1120
1121        // Verify i32 sub-word ops are present
1122        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
1123        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
1124        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
1125        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
1126        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
1127        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
1128
1129        // Verify i64 sub-word ops are present
1130        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
1131        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
1132        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
1133        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
1134        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
1135        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
1136        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
1137        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
1138        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
1139    }
1140
1141    #[test]
1142    fn test_decode_simd_i32x4_add() {
1143        let wat = r#"
1144            (module
1145                (func (export "add_v128") (param v128 v128) (result v128)
1146                    local.get 0
1147                    local.get 1
1148                    i32x4.add
1149                )
1150            )
1151        "#;
1152
1153        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1154        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1155
1156        assert_eq!(functions.len(), 1);
1157        assert!(
1158            functions[0].ops.contains(&WasmOp::I32x4Add),
1159            "Should decode i32x4.add: {:?}",
1160            functions[0].ops
1161        );
1162    }
1163
1164    #[test]
1165    fn test_decode_simd_v128_const() {
1166        let wat = r#"
1167            (module
1168                (func (export "const_v128") (result v128)
1169                    v128.const i32x4 1 2 3 4
1170                )
1171            )
1172        "#;
1173
1174        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1175        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1176
1177        assert_eq!(functions.len(), 1);
1178        assert!(
1179            functions[0]
1180                .ops
1181                .iter()
1182                .any(|o| matches!(o, WasmOp::V128Const(_))),
1183            "Should decode v128.const: {:?}",
1184            functions[0].ops
1185        );
1186    }
1187
1188    #[test]
1189    fn test_decode_simd_v128_load_store() {
1190        let wat = r#"
1191            (module
1192                (memory 1)
1193                (func (export "load_store") (param i32)
1194                    local.get 0
1195                    v128.load
1196                    local.get 0
1197                    v128.store
1198                )
1199            )
1200        "#;
1201
1202        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1203        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1204
1205        assert_eq!(functions.len(), 1);
1206        let ops = &functions[0].ops;
1207        assert!(
1208            ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
1209            "Should decode v128.load"
1210        );
1211        assert!(
1212            ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
1213            "Should decode v128.store"
1214        );
1215    }
1216
1217    #[test]
1218    fn test_decode_simd_bitwise_ops() {
1219        let wat = r#"
1220            (module
1221                (func (export "bitwise") (param v128 v128) (result v128)
1222                    local.get 0
1223                    local.get 1
1224                    v128.and
1225                )
1226            )
1227        "#;
1228
1229        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1230        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1231
1232        assert_eq!(functions.len(), 1);
1233        assert!(functions[0].ops.contains(&WasmOp::V128And));
1234    }
1235
1236    #[test]
1237    fn test_decode_simd_splat() {
1238        let wat = r#"
1239            (module
1240                (func (export "splat") (param i32) (result v128)
1241                    local.get 0
1242                    i32x4.splat
1243                )
1244            )
1245        "#;
1246
1247        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1248        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1249
1250        assert_eq!(functions.len(), 1);
1251        assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
1252    }
1253
1254    #[test]
1255    fn test_decode_simd_extract_lane() {
1256        let wat = r#"
1257            (module
1258                (func (export "extract") (param v128) (result i32)
1259                    local.get 0
1260                    i32x4.extract_lane 2
1261                )
1262            )
1263        "#;
1264
1265        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1266        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1267
1268        assert_eq!(functions.len(), 1);
1269        assert!(
1270            functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
1271            "Should decode i32x4.extract_lane 2"
1272        );
1273    }
1274
1275    #[test]
1276    fn test_decode_simd_f32x4_arithmetic() {
1277        let wat = r#"
1278            (module
1279                (func (export "f32x4_add") (param v128 v128) (result v128)
1280                    local.get 0
1281                    local.get 1
1282                    f32x4.add
1283                )
1284            )
1285        "#;
1286
1287        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1288        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1289
1290        assert_eq!(functions.len(), 1);
1291        assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
1292    }
1293
1294    #[test]
1295    fn test_decode_simd_multiple_ops() {
1296        let wat = r#"
1297            (module
1298                (func (export "simd_ops") (param v128 v128 v128) (result v128)
1299                    ;; (a + b) * c
1300                    local.get 0
1301                    local.get 1
1302                    i32x4.add
1303                    local.get 2
1304                    i32x4.mul
1305                )
1306            )
1307        "#;
1308
1309        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1310        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1311
1312        assert_eq!(functions.len(), 1);
1313        let ops = &functions[0].ops;
1314        assert!(ops.contains(&WasmOp::I32x4Add));
1315        assert!(ops.contains(&WasmOp::I32x4Mul));
1316    }
1317
1318    /// #237: the decoder captures a global's `i32.const` initializer + mutability,
1319    /// so the native-pointer ABI can recognize the stack-pointer global.
1320    #[test]
1321    fn test_decode_captures_global_initializer() {
1322        let wat = r#"
1323            (module
1324                (memory 2)
1325                (global $__stack_pointer (mut i32) (i32.const 65536))
1326                (global $immutable_const i32 (i32.const 7))
1327                (func (export "f") (result i32) global.get 0)
1328            )
1329        "#;
1330        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1331        let module = decode_wasm_module(&wasm).expect("Failed to decode");
1332
1333        assert_eq!(module.globals.len(), 2, "both globals captured");
1334        let sp = &module.globals[0];
1335        assert_eq!(sp.index, 0);
1336        assert_eq!(sp.init_i32, Some(65536), "stack-pointer init captured");
1337        assert!(sp.mutable, "stack pointer is mutable");
1338        let c = &module.globals[1];
1339        assert_eq!(c.init_i32, Some(7));
1340        assert!(!c.mutable, "second global is immutable");
1341    }
1342}