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