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