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