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    /// #643: byte width of the global's storage slot, from its declared value
64    /// type — 4 for i32/f32, 8 for i64/f64, 16 for v128. The globals table is
65    /// laid out by SUMMING these widths (not `index * 4`): an i64 global needs
66    /// room for both words, and every later global's offset shifts with it.
67    pub slot_bytes: u32,
68}
69
70impl WasmMemory {
71    /// Get initial size in bytes
72    pub fn initial_bytes(&self) -> u32 {
73        self.initial_pages * 65536
74    }
75
76    /// Get maximum size in bytes (or initial if not specified)
77    pub fn max_bytes(&self) -> u32 {
78        self.max_pages.unwrap_or(self.initial_pages) * 65536
79    }
80}
81
82/// #642: one element segment's statically-decoded shape — see
83/// [`DecodedModule::elem_segments`].
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct ElemSegmentInfo {
86    /// Const i32 offset of an ACTIVE table-0 segment; `None` = placement not
87    /// statically verifiable (passive/declared segment, non-const offset, or
88    /// a table other than 0).
89    pub offset: Option<u32>,
90    /// The segment's function indices in slot order; `None` = contents not
91    /// statically verifiable (an entry was not a plain `ref.func`).
92    pub funcs: Option<Vec<u32>>,
93}
94
95/// #642: everything the `call_indirect` lowering needs to emit its guards —
96/// computed once per module by [`DecodedModule::call_indirect_guards`] and
97/// threaded to the instruction selector via `CompileConfig`.
98///
99/// WASM Core §4.4.8 requires `call_indirect` to trap when `index >=
100/// table.size` and when the callee's type does not match the instruction's
101/// expected type. synth's table is a raw array of 4-byte code pointers
102/// (linked at R11 by the runtime/harness) with no size field and no type
103/// ids, so:
104///  - the BOUNDS check is emitted at runtime against the compile-time
105///    `table_size` immediate (sound: the table cannot change size —
106///    `table.grow`/`table.set` are unsupported ops whose functions
107///    loud-skip at decode), and
108///  - the TYPE check is discharged at COMPILE time: for expected type `t`,
109///    `type_reject[t]` is `None` only when every slot of the table is
110///    verifiably initialized with a function whose signature structurally
111///    equals type `t` (the closed-world property — no runtime mismatch is
112///    then possible). Otherwise it holds the reason, and the lowering
113///    declines LOUDLY rather than emit an unchecked indirect branch.
114#[derive(Debug, Clone, Default, PartialEq, Eq)]
115pub struct CallIndirectGuards {
116    /// Compile-time size of table 0 (entries); `None` = no sound bound known.
117    pub table_size: Option<u32>,
118    /// Per type index: `None` = closed-world type property VERIFIED for this
119    /// expected type; `Some(reason)` = not verifiable (the lowering declines).
120    pub type_reject: Vec<Option<String>>,
121}
122
123/// Decoded WASM module with functions and memory
124#[derive(Debug, Clone)]
125pub struct DecodedModule {
126    /// Decoded functions
127    pub functions: Vec<FunctionOps>,
128    /// Linear memories
129    pub memories: Vec<WasmMemory>,
130    /// Data segments (offset, data) for memory initialization
131    pub data_segments: Vec<(u32, Vec<u8>)>,
132    /// Import entries (module name, field name, kind)
133    pub imports: Vec<ImportEntry>,
134    /// Number of imported functions (for distinguishing import calls from local calls)
135    pub num_imported_funcs: u32,
136    /// AAPCS integer-argument count per function, indexed by the *full* WASM
137    /// function index (imported functions first, then locally-defined ones).
138    /// Used by the backend to marshal call arguments into R0–R3 (issue #195).
139    /// Counts every parameter as one slot (i64/f64 over-counted — see the
140    /// backend's `set_func_arg_counts` scope note).
141    pub func_arg_counts: Vec<u32>,
142    /// AAPCS integer-argument count per *function type*, indexed by type index.
143    /// Used by `call_indirect`, whose callee arg count comes from the static
144    /// type index (issue #195).
145    pub type_arg_counts: Vec<u32>,
146    /// #311: whether each *function* (full index, imports first) returns i64 —
147    /// the call lowering must tag the result as a register PAIR (r0:r1) or the
148    /// hi half is invisible to liveness and the next constant clobbers it.
149    pub func_ret_i64: Vec<bool>,
150    /// #311: whether each *function type* returns i64 (for `call_indirect`).
151    pub type_ret_i64: Vec<bool>,
152    /// #359: declared parameter widths per *function* (full index, imports
153    /// first): `func_params_i64[f][k]` is true when param `k` is i64/f64. The
154    /// AAPCS stack-argument path needs the declared widths — op-stream inference
155    /// can't see an unused i64 param that still shifts the incoming-stack layout.
156    pub func_params_i64: Vec<Vec<bool>>,
157    /// Defined globals with their initializers (#237). Empty if the module has
158    /// no global section. Used by the native-pointer ABI to make a global whose
159    /// initializer is a linear-memory address (e.g. `$__stack_pointer`)
160    /// self-contained rather than table-relative.
161    pub globals: Vec<WasmGlobal>,
162    /// Function indices that populate any table via an element segment (#275).
163    /// These are the possible `call_indirect` targets — a function reached only
164    /// through the table is invisible to direct-`call` reachability, so the
165    /// whole-graph closure must treat every table entry as reachable once any
166    /// reachable function performs a `call_indirect`. Empty for modules with no
167    /// element section (every leaf/direct-call module), keeping output identical.
168    pub elem_func_indices: Vec<u32>,
169    /// #642: compile-time size (in entries) of table 0, from the table section
170    /// or an imported table whose limits pin it exactly (`max == initial`).
171    /// `None` = no table, or an imported table with growable limits — the
172    /// `call_indirect` bounds guard then has no sound immediate and the
173    /// lowering declines. A DEFINED table's size is exact: `table.grow`/
174    /// `table.set` are unsupported ops (their functions loud-skip at decode),
175    /// so nothing synth compiles can resize or retype the table.
176    pub table_size: Option<u32>,
177    /// #642: per element segment, everything the closed-world `call_indirect`
178    /// type check needs. `offset` is the const i32 placement of an ACTIVE
179    /// table-0 segment (`None` = passive/declared/non-const offset/other
180    /// table — statically unverifiable placement); `funcs` are the segment's
181    /// function indices in slot order (`None` = an entry was not a plain
182    /// `ref.func`, e.g. `ref.null` — statically unverifiable contents).
183    pub elem_segments: Vec<ElemSegmentInfo>,
184    /// #642: type index per function, indexed by the FULL function index
185    /// (imports first, then locally-defined ones).
186    pub func_type_indices: Vec<u32>,
187    /// #642: canonical structural signature per type index (params/results
188    /// rendered as a string) — used for the closed-world `call_indirect` type
189    /// check, which must compare SIGNATURES, not raw type indices (a module
190    /// may carry structurally-identical duplicate types).
191    pub type_signatures: Vec<String>,
192    /// VCR-PERF-002 Phase 1 (#494): proven invariants from loom's `wsc.facts`
193    /// custom section, keyed by `(function index, value id)` — see
194    /// `docs/design/wsc-facts-encoding.md` (schema v1) and
195    /// [`crate::wsc_facts::parse_wsc_facts`]. FAIL-SAFE by contract (loom#231
196    /// Q4): a missing/unparseable section or unknown version yields the empty
197    /// vec, unknown fact kinds are skipped — never a decode error. Phase 1 is
198    /// ingestion only: NO codegen path consumes these yet, so emitted bytes
199    /// are unchanged whether or not a module carries the section.
200    pub wsc_facts: Vec<crate::wsc_facts::WscFact>,
201}
202
203impl DecodedModule {
204    /// #642: compute the `call_indirect` guard inputs — the compile-time table
205    /// size for the runtime bounds check, and the per-expected-type
206    /// closed-world verdict that discharges the type check at compile time.
207    /// See [`CallIndirectGuards`] for the soundness argument.
208    pub fn call_indirect_guards(&self) -> CallIndirectGuards {
209        let n_types = self.type_signatures.len();
210        let reject_all = |table_size: Option<u32>, reason: String| CallIndirectGuards {
211            table_size,
212            type_reject: vec![Some(reason); n_types],
213        };
214
215        let Some(size) = self.table_size else {
216            return reject_all(
217                None,
218                "module has no table with a compile-time-fixed size".to_string(),
219            );
220        };
221
222        // Reconstruct the table image: slot -> initializing function index.
223        let mut slots: Vec<Option<u32>> = vec![None; size as usize];
224        for seg in &self.elem_segments {
225            let (Some(off), Some(funcs)) = (seg.offset, seg.funcs.as_ref()) else {
226                return reject_all(
227                    Some(size),
228                    "element segment is not statically verifiable (passive/declared \
229                     segment, non-const offset, non-zero table, or non-`ref.func` \
230                     entry)"
231                        .to_string(),
232                );
233            };
234            for (k, &f) in funcs.iter().enumerate() {
235                let Some(slot) = slots.get_mut(off as usize + k) else {
236                    return reject_all(
237                        Some(size),
238                        format!(
239                            "element segment (offset {off}, {} entries) writes past the \
240                             declared table size {size}",
241                            funcs.len()
242                        ),
243                    );
244                };
245                *slot = Some(f);
246            }
247        }
248        // An uninitialized slot is a null funcref: calling it must trap, and
249        // the raw code-pointer table has no runtime representation of null to
250        // check against — so it breaks the closed world for EVERY type.
251        if let Some(i) = slots.iter().position(|s| s.is_none()) {
252            return reject_all(
253                Some(size),
254                format!(
255                    "table slot {i} is uninitialized (null funcref) — a \
256                     `call_indirect` reaching it must trap, and the raw \
257                     code-pointer table has no runtime null/type id to check"
258                ),
259            );
260        }
261
262        let type_reject = (0..n_types)
263            .map(|t| {
264                for f in slots.iter().flatten() {
265                    let Some(&fty) = self.func_type_indices.get(*f as usize) else {
266                        return Some(format!(
267                            "table entry references function {f} with no known type"
268                        ));
269                    };
270                    if self.type_signatures.get(fty as usize) != self.type_signatures.get(t) {
271                        return Some(format!(
272                            "table entry (function {f}, type {fty}) has a different \
273                             signature than expected type {t} — a runtime type check \
274                             is not implementable on the raw code-pointer table"
275                        ));
276                    }
277                }
278                None
279            })
280            .collect();
281
282        CallIndirectGuards {
283            table_size: Some(size),
284            type_reject,
285        }
286    }
287}
288
289/// Decode a WASM binary and extract functions, memory, and data segments
290pub fn decode_wasm_module(wasm_bytes: &[u8]) -> Result<DecodedModule> {
291    let mut functions = Vec::new();
292    let mut memories = Vec::new();
293    let mut data_segments = Vec::new();
294    let mut globals: Vec<WasmGlobal> = Vec::new();
295    let mut imports = Vec::new();
296    let mut func_index = 0u32;
297    let mut num_imported_funcs = 0u32;
298    let mut export_names: HashMap<u32, String> = HashMap::new();
299    // #195: per-type AAPCS arg count (indexed by type index) and per-function
300    // arg count (indexed by full function index: imports first, then locals).
301    let mut type_arg_counts: Vec<u32> = Vec::new();
302    let mut func_arg_counts: Vec<u32> = Vec::new();
303    let mut type_ret_i64: Vec<bool> = Vec::new();
304    let mut func_ret_i64: Vec<bool> = Vec::new();
305    // #359: declared param widths per type / per function (full index).
306    let mut type_params_i64: Vec<Vec<bool>> = Vec::new();
307    let mut func_params_i64: Vec<Vec<bool>> = Vec::new();
308    // #509: (param_count, result_count) per type index, for FuncType blocktypes.
309    let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
310    let mut elem_func_indices: Vec<u32> = Vec::new();
311    // #642: call_indirect guard inputs — table 0's fixed size, per-segment
312    // static shapes, per-function type index, per-type canonical signature.
313    let mut table_size: Option<u32> = None;
314    let mut saw_table = false;
315    let mut elem_segments: Vec<ElemSegmentInfo> = Vec::new();
316    let mut func_type_indices: Vec<u32> = Vec::new();
317    let mut type_signatures: Vec<String> = Vec::new();
318    // #394 Tier-1.x: function index → developer-facing name from the wasm
319    // `name` custom section (function-names subsection). Applied to
320    // `FunctionOps.debug_name` after the parse loop — the custom section
321    // conventionally trails the code section, so the entries are not yet
322    // available when each `CodeSectionEntry` is decoded.
323    let mut name_section_names: HashMap<u32, String> = HashMap::new();
324    // VCR-PERF-002 Phase 1 (#494): facts from loom's `wsc.facts` custom
325    // section. `None` until (and unless) the first such section is seen —
326    // duplicates are ignored (one prover, one section; encoding doc rule).
327    let mut wsc_facts: Option<Vec<crate::wsc_facts::WscFact>> = None;
328
329    for payload in Parser::new(0).parse_all(wasm_bytes) {
330        let payload = payload.context("Failed to parse WASM payload")?;
331
332        match payload {
333            Payload::TypeSection(reader) => {
334                // Record the parameter count of each function type so calls can
335                // marshal the right number of arguments (issue #195).
336                for rec_group in reader {
337                    let rec_group = rec_group.context("Failed to parse type")?;
338                    for sub_ty in rec_group.types() {
339                        // #509: blocktype arity per type index (saturated u8 —
340                        // >255 params/results is far beyond anything the
341                        // selector supports anyway, and the selector declines
342                        // rather than trusting a saturated count).
343                        type_block_arity.push(match &sub_ty.composite_type.inner {
344                            wasmparser::CompositeInnerType::Func(f) => (
345                                u8::try_from(f.params().len()).unwrap_or(u8::MAX),
346                                u8::try_from(f.results().len()).unwrap_or(u8::MAX),
347                            ),
348                            _ => (u8::MAX, u8::MAX),
349                        });
350                        let (count, ret_i64, params_i64) = match &sub_ty.composite_type.inner {
351                            wasmparser::CompositeInnerType::Func(func_ty) => (
352                                func_ty.params().len() as u32,
353                                func_ty
354                                    .results()
355                                    .first()
356                                    .is_some_and(|t| *t == wasmparser::ValType::I64),
357                                // #359: i64/f64 params occupy 8 bytes / a register
358                                // pair under AAPCS. f32/f64 are not in scope for the
359                                // stack-arg path (refused), but mark both 64-bit
360                                // float and i64 so the guard catches them.
361                                func_ty
362                                    .params()
363                                    .iter()
364                                    .map(|t| {
365                                        matches!(
366                                            t,
367                                            wasmparser::ValType::I64 | wasmparser::ValType::F64
368                                        )
369                                    })
370                                    .collect::<Vec<bool>>(),
371                            ),
372                            _ => (0, false, Vec::new()),
373                        };
374                        type_arg_counts.push(count);
375                        type_ret_i64.push(ret_i64);
376                        type_params_i64.push(params_i64);
377                        // #642: canonical structural signature for the
378                        // closed-world call_indirect type check (compares
379                        // SIGNATURES so duplicate types stay interchangeable).
380                        type_signatures.push(match &sub_ty.composite_type.inner {
381                            wasmparser::CompositeInnerType::Func(f) => {
382                                format!("{:?}->{:?}", f.params(), f.results())
383                            }
384                            other => format!("non-func:{other:?}"),
385                        });
386                    }
387                }
388            }
389            Payload::ImportSection(reader) => {
390                // wasmparser 0.221+ groups imports (the "compact imports"
391                // proposal): the section reader yields `Imports` groups, each of
392                // which may expand to several `Import`s. `into_imports()`
393                // flattens groups back to individual `Import`s (preserving the
394                // module/name/ty fields), keeping the per-import loop intact.
395                for import in reader.into_imports() {
396                    let import = import.context("Failed to parse import")?;
397                    let (kind, idx) = match import.ty {
398                        wasmparser::TypeRef::Func(type_idx) => {
399                            let idx = num_imported_funcs;
400                            num_imported_funcs += 1;
401                            // Record the imported function's arg count at its
402                            // full function index (imports come first).
403                            func_type_indices.push(type_idx); // #642
404                            func_arg_counts
405                                .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
406                            func_ret_i64.push(
407                                type_ret_i64
408                                    .get(type_idx as usize)
409                                    .copied()
410                                    .unwrap_or(false),
411                            );
412                            func_params_i64.push(
413                                type_params_i64
414                                    .get(type_idx as usize)
415                                    .cloned()
416                                    .unwrap_or_default(),
417                            );
418                            (ImportKind::Function(type_idx), idx)
419                        }
420                        wasmparser::TypeRef::Memory(_) => (ImportKind::Memory, 0),
421                        wasmparser::TypeRef::Table(t) => {
422                            // #642: an imported table 0 only yields a SOUND
423                            // compile-time bound when its limits pin the size
424                            // exactly (max == initial) — a growable import
425                            // could be larger at runtime, and a bounds guard
426                            // against `initial` would trap spec-valid calls.
427                            if !saw_table {
428                                saw_table = true;
429                                table_size = match (u32::try_from(t.initial), t.maximum) {
430                                    (Ok(init), Some(max)) if u64::from(init) == max => Some(init),
431                                    _ => None,
432                                };
433                            }
434                            (ImportKind::Table, 0)
435                        }
436                        wasmparser::TypeRef::Global(_) => (ImportKind::Global, 0),
437                        _ => continue,
438                    };
439                    imports.push(ImportEntry {
440                        module: import.module.to_string(),
441                        name: import.name.to_string(),
442                        kind,
443                        index: idx,
444                    });
445                }
446            }
447            Payload::FunctionSection(reader) => {
448                // Each entry gives the type index of a locally-defined function,
449                // in order. Their full function indices follow the imports, so
450                // appending to `func_arg_counts` keeps it indexed by full index
451                // (issue #195).
452                for ty in reader {
453                    let type_idx = ty.context("Failed to parse function type index")?;
454                    func_type_indices.push(type_idx); // #642
455                    func_arg_counts
456                        .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
457                    func_ret_i64.push(
458                        type_ret_i64
459                            .get(type_idx as usize)
460                            .copied()
461                            .unwrap_or(false),
462                    );
463                    func_params_i64.push(
464                        type_params_i64
465                            .get(type_idx as usize)
466                            .cloned()
467                            .unwrap_or_default(),
468                    );
469                }
470            }
471            Payload::TableSection(reader) => {
472                // #642: a DEFINED table's compile-time size is exact — its
473                // initial size is its permanent size, because nothing synth
474                // compiles can resize it (`table.grow` is an unsupported op
475                // whose function loud-skips at decode). Only table 0 matters:
476                // the call_indirect lowering declines non-zero table indices.
477                for table in reader {
478                    let table = table.context("Failed to parse table")?;
479                    if !saw_table {
480                        saw_table = true;
481                        table_size = u32::try_from(table.ty.initial).ok();
482                    }
483                }
484            }
485            Payload::MemorySection(reader) => {
486                for (idx, memory) in reader.into_iter().enumerate() {
487                    let mem = memory.context("Failed to parse memory")?;
488                    memories.push(WasmMemory {
489                        index: idx as u32,
490                        initial_pages: mem.initial as u32,
491                        max_pages: mem.maximum.map(|m| m as u32),
492                        shared: mem.shared,
493                    });
494                }
495            }
496            Payload::GlobalSection(reader) => {
497                // #237: capture each defined global's i32 initializer + mutability.
498                // The init is a const expr; we only decode a leading `i32.const`
499                // (the shape `$__stack_pointer`/data-layout globals use). Anything
500                // else (global.get, f32/f64, etc.) records `init_i32: None` and is
501                // left to the table-relative path.
502                for (idx, global) in reader.into_iter().enumerate() {
503                    let global = global.context("Failed to parse global")?;
504                    let mut init_i32 = None;
505                    let mut ops = global.init_expr.get_operators_reader();
506                    if let Ok(wasmparser::Operator::I32Const { value }) = ops.read() {
507                        init_i32 = Some(value);
508                    }
509                    // #643: record the slot width from the DECLARED value type.
510                    // i64/f64 globals occupy 8 bytes (a register pair on the
511                    // 32-bit targets), v128 sixteen; laying every global out at
512                    // `index * 4` silently dropped the high word of every i64.
513                    let slot_bytes = match global.ty.content_type {
514                        wasmparser::ValType::I64 | wasmparser::ValType::F64 => 8,
515                        wasmparser::ValType::V128 => 16,
516                        _ => 4,
517                    };
518                    globals.push(WasmGlobal {
519                        index: idx as u32,
520                        init_i32,
521                        mutable: global.ty.mutable,
522                        slot_bytes,
523                    });
524                }
525            }
526            Payload::DataSection(reader) => {
527                for data in reader {
528                    let data = data.context("Failed to parse data segment")?;
529                    if let wasmparser::DataKind::Active {
530                        memory_index: 0,
531                        offset_expr,
532                    } = data.kind
533                    {
534                        let mut ops = offset_expr.get_operators_reader();
535                        if let Ok(wasmparser::Operator::I32Const { value }) = ops.read() {
536                            data_segments.push((value as u32, data.data.to_vec()));
537                        }
538                    }
539                }
540            }
541            Payload::ElementSection(reader) => {
542                // #275: collect every function index that initializes a table.
543                // These are the `call_indirect` targets the direct-call closure
544                // cannot see; `reachable_from_exports` unions them in when a
545                // reachable function does a `call_indirect`. Both element forms
546                // are handled: a flat function-index list, and the const-expr
547                // form whose `ref.func` entries name the functions.
548                for elem in reader {
549                    let elem = elem.context("Failed to parse element segment")?;
550                    // #642: the segment's static placement — a const i32
551                    // offset of an ACTIVE table-0 segment; anything else is
552                    // unverifiable and poisons the closed-world type check.
553                    let seg_offset: Option<u32> = match &elem.kind {
554                        wasmparser::ElementKind::Active {
555                            table_index,
556                            offset_expr,
557                        } if table_index.unwrap_or(0) == 0 => {
558                            let mut ops = offset_expr.get_operators_reader();
559                            match ops.read() {
560                                Ok(wasmparser::Operator::I32Const { value }) => {
561                                    u32::try_from(value).ok()
562                                }
563                                _ => None,
564                            }
565                        }
566                        _ => None,
567                    };
568                    let mut seg_funcs: Option<Vec<u32>> = Some(Vec::new());
569                    match elem.items {
570                        wasmparser::ElementItems::Functions(funcs) => {
571                            for f in funcs {
572                                let f = f.context("Failed to parse element func index")?;
573                                elem_func_indices.push(f);
574                                if let Some(v) = seg_funcs.as_mut() {
575                                    v.push(f);
576                                }
577                            }
578                        }
579                        wasmparser::ElementItems::Expressions(_, exprs) => {
580                            for expr in exprs {
581                                let expr = expr.context("Failed to parse element expr")?;
582                                // #642: an entry is verifiable only when it is
583                                // a single plain `ref.func` (reader yields the
584                                // op + the implicit `end`). `ref.null` or any
585                                // computed entry poisons the segment.
586                                let mut entry_func: Option<u32> = None;
587                                let mut plain = true;
588                                for (k, op) in expr.get_operators_reader().into_iter().enumerate() {
589                                    match (k, op.context("Failed to parse element op")?) {
590                                        (0, wasmparser::Operator::RefFunc { function_index }) => {
591                                            elem_func_indices.push(function_index);
592                                            entry_func = Some(function_index);
593                                        }
594                                        (_, wasmparser::Operator::End) => {}
595                                        (_, wasmparser::Operator::RefFunc { function_index }) => {
596                                            // Keep the pre-#642 reachability
597                                            // behaviour: every ref.func seen
598                                            // anywhere is a possible target.
599                                            elem_func_indices.push(function_index);
600                                            plain = false;
601                                        }
602                                        _ => plain = false,
603                                    }
604                                }
605                                match (plain, entry_func, seg_funcs.as_mut()) {
606                                    (true, Some(f), Some(v)) => v.push(f),
607                                    _ => seg_funcs = None,
608                                }
609                            }
610                        }
611                    }
612                    elem_segments.push(ElemSegmentInfo {
613                        offset: seg_offset,
614                        funcs: seg_funcs,
615                    });
616                }
617            }
618            Payload::ExportSection(exports) => {
619                for export in exports {
620                    let export = export.context("Failed to parse export")?;
621                    if export.kind == ExternalKind::Func {
622                        export_names.insert(export.index, export.name.to_string());
623                    }
624                }
625            }
626            Payload::CodeSectionEntry(body) => {
627                let (ops, op_offsets, block_arity, unsupported) =
628                    decode_function_body(&body, &type_block_arity)?;
629                let actual_index = num_imported_funcs + func_index;
630                let export_name = export_names.get(&actual_index).cloned();
631
632                functions.push(FunctionOps {
633                    index: actual_index,
634                    export_name,
635                    debug_name: None, // filled from the `name` section after the loop
636                    ops,
637                    op_offsets,
638                    unsupported,
639                    block_arity,
640                });
641                func_index += 1;
642            }
643            Payload::CustomSection(c) => {
644                // #394 Tier-1.x: the wasm `name` custom section.
645                if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
646                    parse_name_section_func_names(reader, &mut name_section_names);
647                }
648                // VCR-PERF-002 Phase 1 (#494): loom's `wsc.facts` section.
649                // `parse_wsc_facts` is TOTAL (fail-safe skew, loom#231 Q4):
650                // any malformed payload decodes to the empty fact list WITH a
651                // stderr diagnostic, never an error — facts are optional
652                // accelerators and must not be able to change a compilation
653                // outcome. First section wins.
654                if c.name() == crate::wsc_facts::WSC_FACTS_SECTION_NAME && wsc_facts.is_none() {
655                    let parsed = crate::wsc_facts::parse_wsc_facts(c.data());
656                    if let Some(reason) = &parsed.section_ignored {
657                        eprintln!(
658                            "warning: ignoring unparseable `wsc.facts` custom section \
659                             ({reason}) — facts are optional accelerators, compilation \
660                             is unaffected (#494 fail-safe skew rule)"
661                        );
662                    } else if parsed.records_skipped > 0 {
663                        eprintln!(
664                            "warning: skipped {} unknown/undecodable `wsc.facts` \
665                             record(s) (likely a newer loom emitter); {} known fact(s) \
666                             kept, compilation is unaffected (#494 fail-safe skew rule)",
667                            parsed.records_skipped,
668                            parsed.facts.len()
669                        );
670                    }
671                    wsc_facts = Some(parsed.facts);
672                }
673            }
674            _ => {}
675        }
676    }
677
678    apply_name_section(&mut functions, &name_section_names);
679
680    Ok(DecodedModule {
681        functions,
682        memories,
683        data_segments,
684        imports,
685        num_imported_funcs,
686        func_arg_counts,
687        type_arg_counts,
688        func_ret_i64,
689        type_ret_i64,
690        func_params_i64,
691        globals,
692        elem_func_indices,
693        table_size,
694        elem_segments,
695        func_type_indices,
696        type_signatures,
697        wsc_facts: wsc_facts.unwrap_or_default(),
698    })
699}
700
701/// Parse the function-names subsection of a wasm `name` custom section into
702/// `out` (function index → developer-facing name, e.g.
703/// `core::panicking::panic_fmt::h...`). Best-effort by design: the section is
704/// DEBUG METADATA only, so a malformed entry is skipped rather than failing the
705/// compile — no codegen path depends on it (#394 Tier-1.x).
706fn parse_name_section_func_names(
707    reader: wasmparser::NameSectionReader<'_>,
708    out: &mut HashMap<u32, String>,
709) {
710    for subsection in reader.into_iter().flatten() {
711        if let wasmparser::Name::Function(map) = subsection {
712            for naming in map.into_iter().flatten() {
713                out.insert(naming.index, naming.name.to_string());
714            }
715        }
716    }
717}
718
719/// Fill each function's `debug_name` from the `name`-section map (keyed by the
720/// FULL function index, imports first — the same index space `FunctionOps.index`
721/// uses). A function without an entry keeps `None` (⇒ `func_N` downstream).
722fn apply_name_section(functions: &mut [FunctionOps], names: &HashMap<u32, String>) {
723    if names.is_empty() {
724        return;
725    }
726    for f in functions {
727        f.debug_name = names.get(&f.index).cloned();
728    }
729}
730
731/// Decode a WASM binary and extract all function bodies as WasmOp sequences
732pub fn decode_wasm_functions(wasm_bytes: &[u8]) -> Result<Vec<FunctionOps>> {
733    let mut functions = Vec::new();
734    let mut func_index = 0u32;
735    let mut num_imported_funcs = 0u32;
736    let mut export_names: HashMap<u32, String> = HashMap::new();
737    let mut name_section_names: HashMap<u32, String> = HashMap::new();
738    // #509: (param_count, result_count) per type index, for FuncType blocktypes.
739    let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
740
741    for payload in Parser::new(0).parse_all(wasm_bytes) {
742        let payload = payload.context("Failed to parse WASM payload")?;
743
744        match payload {
745            Payload::TypeSection(reader) => {
746                // #509: the blocktype-arity side-table needs the type section
747                // for `BlockType::FuncType(i)` lookups (the wasm binary format
748                // places types before code, so the table is complete before any
749                // `CodeSectionEntry` is decoded).
750                for rec_group in reader {
751                    let rec_group = rec_group.context("Failed to parse type")?;
752                    for sub_ty in rec_group.types() {
753                        type_block_arity.push(match &sub_ty.composite_type.inner {
754                            wasmparser::CompositeInnerType::Func(f) => (
755                                u8::try_from(f.params().len()).unwrap_or(u8::MAX),
756                                u8::try_from(f.results().len()).unwrap_or(u8::MAX),
757                            ),
758                            _ => (u8::MAX, u8::MAX),
759                        });
760                    }
761                }
762            }
763            Payload::ImportSection(imports) => {
764                // wasmparser 0.221+ compact-imports grouping — flatten groups
765                // to individual imports (see the ImportSection handler above).
766                for import in imports.into_imports() {
767                    let import = import.context("Failed to parse import")?;
768                    if matches!(import.ty, wasmparser::TypeRef::Func(_)) {
769                        num_imported_funcs += 1;
770                    }
771                }
772            }
773            Payload::ExportSection(exports) => {
774                for export in exports {
775                    let export = export.context("Failed to parse export")?;
776                    if export.kind == ExternalKind::Func {
777                        export_names.insert(export.index, export.name.to_string());
778                    }
779                }
780            }
781            Payload::CodeSectionEntry(body) => {
782                let (ops, op_offsets, block_arity, unsupported) =
783                    decode_function_body(&body, &type_block_arity)?;
784                let actual_index = num_imported_funcs + func_index;
785                let export_name = export_names.get(&actual_index).cloned();
786
787                functions.push(FunctionOps {
788                    index: actual_index,
789                    export_name,
790                    debug_name: None, // filled from the `name` section after the loop
791                    ops,
792                    op_offsets,
793                    unsupported,
794                    block_arity,
795                });
796                func_index += 1;
797            }
798            Payload::CustomSection(c) => {
799                // #394 Tier-1.x: the wasm `name` custom section.
800                if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
801                    parse_name_section_func_names(reader, &mut name_section_names);
802                }
803            }
804            _ => {}
805        }
806    }
807
808    apply_name_section(&mut functions, &name_section_names);
809
810    Ok(functions)
811}
812
813/// Decoded function with its WasmOp sequence
814#[derive(Debug, Clone)]
815pub struct FunctionOps {
816    /// Function index in the module (includes imported functions)
817    pub index: u32,
818    /// Export name if this function is exported
819    pub export_name: Option<String>,
820    /// #394 Tier-1.x: the function's developer-facing name from the wasm `name`
821    /// custom section (function-names subsection), e.g.
822    /// `core::panicking::panic_fmt::h6651313c3e2c6c2f` — present for INTERNAL
823    /// (non-exported) functions too, unlike `export_name`. DEBUG METADATA only:
824    /// consumed by the `--debug-line` `DW_TAG_subprogram` emit (name priority:
825    /// name-section > export name > `func_N`); no codegen or symbol-table path
826    /// reads it, so emitted `.text`/`.symtab` are unchanged (frozen-safe).
827    /// `None` when the module has no `name` section or no entry for this index.
828    pub debug_name: Option<String>,
829    /// The WASM operations in this function body
830    pub ops: Vec<WasmOp>,
831    /// VCR-DBG-001 step 1 (#394): module-relative wasm byte offset of each op in
832    /// `ops` (same index → same op). This is the address space DWARF-for-wasm
833    /// `.debug_line` keys on, so it is the bridge from synth's op-index
834    /// `source_line` to the input wasm's DWARF (wasm-offset → source). PURELY
835    /// ADDITIVE metadata: no codegen path reads it, so emitted `.text` is
836    /// unchanged and the frozen fixtures stay bit-identical. Empty until consumed
837    /// by the DWARF emitter (Tier 1).
838    pub op_offsets: Vec<u32>,
839    /// `Some(reason)` when the body contained a value-affecting operator the
840    /// decoder cannot lower (e.g. scalar f32/f64 — #369, bulk-memory
841    /// memory.copy/fill). Such an op would otherwise be silently *dropped*
842    /// (`convert_operator` → `None`), leaving the operand stack wrong and the
843    /// function a silent miscompile. The compile path LOUD-SKIPS a flagged
844    /// function (diagnostic + symbol absent → link error names it) instead —
845    /// the #180/#185 "unsupported op must Err, never silently continue"
846    /// contract. `None` once every op decoded or was intentionally ignorable
847    /// (Nop/Unreachable).
848    pub unsupported: Option<String>,
849    /// #509: blocktype arity side-table — `(param_count, result_count)` of the
850    /// k-th `Block`/`Loop`/`If` op in `ops`, in order of appearance.
851    /// ORDINAL-keyed, not op-index-keyed, on purpose: the backend may rewrite
852    /// the op stream before selection (e.g. the #539 `i32.const 0; memory.grow`
853    /// → `memory.size` fold), which shifts op indices but never adds/removes
854    /// control ops, so the ordinal stays aligned. `BlockType::Empty → (0,0)`,
855    /// `ValType → (0,1)`, `FuncType(i) →` counts from the type section
856    /// (saturated to u8; an unresolvable type index records `(u8::MAX,
857    /// u8::MAX)` so the selector declines loudly instead of miscompiling).
858    /// This is what lets the direct selector land a value carried by
859    /// `br`/`br_if`/`br_table` in the target block's designated result
860    /// register instead of dropping it — `WasmOp::Block/Loop/If` stay bare
861    /// unit variants (zero ripple through the backends' match sites), and an
862    /// empty table (hand-built op streams in unit tests) keeps the legacy
863    /// void-block lowering.
864    pub block_arity: Vec<(u8, u8)>,
865}
866
867/// #509: `(param_count, result_count)` of a wasm blocktype, for the
868/// [`FunctionOps::block_arity`] side-table. `type_block_arity` is the type
869/// section's per-type-index counts (needed for the `FuncType` form); a missing
870/// entry saturates to `(u8::MAX, u8::MAX)` so downstream declines loudly.
871fn blocktype_arity(bt: &wasmparser::BlockType, type_block_arity: &[(u8, u8)]) -> (u8, u8) {
872    match bt {
873        wasmparser::BlockType::Empty => (0, 0),
874        wasmparser::BlockType::Type(_) => (0, 1),
875        wasmparser::BlockType::FuncType(i) => type_block_arity
876            .get(*i as usize)
877            .copied()
878            .unwrap_or((u8::MAX, u8::MAX)),
879    }
880}
881
882/// The per-function payload [`decode_function_body`] extracts: `(ops,
883/// op_offsets, block_arity, unsupported)` — see the matching
884/// [`FunctionOps`] fields for each component's contract.
885type DecodedBody = (Vec<WasmOp>, Vec<u32>, Vec<(u8, u8)>, Option<String>);
886
887/// Decode a single function body to WasmOp sequence.
888///
889/// Returns the ops plus `Some(reason)` if any operator was a value-affecting
890/// op the decoder cannot lower (so the function must be loud-skipped, #369 —
891/// not silently miscompiled by dropping the op).
892fn decode_function_body(
893    body: &wasmparser::FunctionBody,
894    type_block_arity: &[(u8, u8)],
895) -> Result<DecodedBody> {
896    let mut ops = Vec::new();
897    // VCR-DBG-001 step 1: parallel to `ops` — the module-relative wasm byte
898    // offset of each emitted op (the DWARF-for-wasm address space). Captured via
899    // the offset-aware reader; pushed only when an op is pushed, so indices stay
900    // aligned with `ops`. Additive metadata, no codegen consumer ⇒ frozen-safe.
901    let mut op_offsets = Vec::new();
902    // #509: ordinal blocktype-arity side-table — one entry per Block/Loop/If in
903    // `ops` order (see `FunctionOps::block_arity`).
904    let mut block_arity: Vec<(u8, u8)> = Vec::new();
905    let mut unsupported: Option<String> = None;
906
907    let ops_reader = body.get_operators_reader()?;
908    for item in ops_reader.into_iter_with_offsets() {
909        let (op, offset) = item.context("Failed to read operator")?;
910
911        if let Some(wasm_op) = convert_operator(&op) {
912            // #509: capture the blocktype arity BEFORE the enum flattens it away
913            // (`WasmOp::Block/Loop/If` are unit variants by design).
914            if let wasmparser::Operator::Block { blockty }
915            | wasmparser::Operator::Loop { blockty }
916            | wasmparser::Operator::If { blockty } = &op
917            {
918                block_arity.push(blocktype_arity(blockty, type_block_arity));
919            }
920            ops.push(wasm_op);
921            op_offsets.push(offset as u32);
922        } else if unsupported.is_none() && !is_intentionally_ignored(&op) {
923            // The op was DROPPED by `convert_operator` (`_ => None`) and is not
924            // an intentional no-op (Nop/Unreachable) — record it so the
925            // function is loud-skipped rather than silently miscompiled (#369).
926            unsupported = Some(format!("{op:?}"));
927        }
928    }
929
930    Ok((ops, op_offsets, block_arity, unsupported))
931}
932
933/// Operators that `convert_operator` returns `None` for *on purpose* — they
934/// carry no value-affecting semantics for our backend, so dropping them is
935/// correct (NOT a silent miscompile). Everything else that decodes to `None`
936/// is an unsupported op that must loud-skip its function (#369).
937fn is_intentionally_ignored(op: &wasmparser::Operator) -> bool {
938    use wasmparser::Operator::*;
939    matches!(op, Nop | Unreachable)
940}
941
942/// Convert a wasmparser Operator to our WasmOp enum
943fn convert_operator(op: &wasmparser::Operator) -> Option<WasmOp> {
944    use wasmparser::Operator::*;
945
946    match op {
947        // Constants
948        I32Const { value } => Some(WasmOp::I32Const(*value)),
949
950        // i32 Arithmetic
951        I32Add => Some(WasmOp::I32Add),
952        I32Sub => Some(WasmOp::I32Sub),
953        I32Mul => Some(WasmOp::I32Mul),
954        I32DivS => Some(WasmOp::I32DivS),
955        I32DivU => Some(WasmOp::I32DivU),
956        I32RemS => Some(WasmOp::I32RemS),
957        I32RemU => Some(WasmOp::I32RemU),
958
959        // i64 Constants
960        I64Const { value } => Some(WasmOp::I64Const(*value)),
961
962        // i64 Arithmetic
963        I64Add => Some(WasmOp::I64Add),
964        I64Sub => Some(WasmOp::I64Sub),
965        I64Mul => Some(WasmOp::I64Mul),
966        I64DivS => Some(WasmOp::I64DivS),
967        I64DivU => Some(WasmOp::I64DivU),
968        I64RemS => Some(WasmOp::I64RemS),
969        I64RemU => Some(WasmOp::I64RemU),
970
971        // i64 Bitwise
972        I64And => Some(WasmOp::I64And),
973        I64Or => Some(WasmOp::I64Or),
974        I64Xor => Some(WasmOp::I64Xor),
975        I64Shl => Some(WasmOp::I64Shl),
976        I64ShrS => Some(WasmOp::I64ShrS),
977        I64ShrU => Some(WasmOp::I64ShrU),
978        I64Rotl => Some(WasmOp::I64Rotl),
979        I64Rotr => Some(WasmOp::I64Rotr),
980        I64Clz => Some(WasmOp::I64Clz),
981        I64Ctz => Some(WasmOp::I64Ctz),
982        I64Popcnt => Some(WasmOp::I64Popcnt),
983        I64Extend8S => Some(WasmOp::I64Extend8S),
984        I64Extend16S => Some(WasmOp::I64Extend16S),
985        I64Extend32S => Some(WasmOp::I64Extend32S),
986        // i32<->i64 width conversions. Previously UNMAPPED → silently dropped,
987        // which left an i32 value as a 64-bit operand with a garbage high half
988        // (harmless when a following `i64.shl 32` discards it, but a latent
989        // miscompile for extend-then-arithmetic, and it breaks width-correct
990        // register allocation). (#204)
991        I64ExtendI32U => Some(WasmOp::I64ExtendI32U),
992        I64ExtendI32S => Some(WasmOp::I64ExtendI32S),
993        I32WrapI64 => Some(WasmOp::I32WrapI64),
994
995        // i64 Comparison
996        I64Eqz => Some(WasmOp::I64Eqz),
997        I64Eq => Some(WasmOp::I64Eq),
998        I64Ne => Some(WasmOp::I64Ne),
999        I64LtS => Some(WasmOp::I64LtS),
1000        I64LtU => Some(WasmOp::I64LtU),
1001        I64LeS => Some(WasmOp::I64LeS),
1002        I64LeU => Some(WasmOp::I64LeU),
1003        I64GtS => Some(WasmOp::I64GtS),
1004        I64GtU => Some(WasmOp::I64GtU),
1005        I64GeS => Some(WasmOp::I64GeS),
1006        I64GeU => Some(WasmOp::I64GeU),
1007
1008        // Bitwise
1009        I32And => Some(WasmOp::I32And),
1010        I32Or => Some(WasmOp::I32Or),
1011        I32Xor => Some(WasmOp::I32Xor),
1012        I32Shl => Some(WasmOp::I32Shl),
1013        I32ShrS => Some(WasmOp::I32ShrS),
1014        I32ShrU => Some(WasmOp::I32ShrU),
1015        I32Rotl => Some(WasmOp::I32Rotl),
1016        I32Rotr => Some(WasmOp::I32Rotr),
1017        I32Clz => Some(WasmOp::I32Clz),
1018        I32Ctz => Some(WasmOp::I32Ctz),
1019        I32Popcnt => Some(WasmOp::I32Popcnt),
1020        I32Extend8S => Some(WasmOp::I32Extend8S),
1021        I32Extend16S => Some(WasmOp::I32Extend16S),
1022
1023        // Comparison
1024        I32Eqz => Some(WasmOp::I32Eqz),
1025        I32Eq => Some(WasmOp::I32Eq),
1026        I32Ne => Some(WasmOp::I32Ne),
1027        I32LtS => Some(WasmOp::I32LtS),
1028        I32LtU => Some(WasmOp::I32LtU),
1029        I32LeS => Some(WasmOp::I32LeS),
1030        I32LeU => Some(WasmOp::I32LeU),
1031        I32GtS => Some(WasmOp::I32GtS),
1032        I32GtU => Some(WasmOp::I32GtU),
1033        I32GeS => Some(WasmOp::I32GeS),
1034        I32GeU => Some(WasmOp::I32GeU),
1035
1036        // Memory
1037        I32Load { memarg } => Some(WasmOp::I32Load {
1038            offset: memarg.offset as u32,
1039            align: memarg.align as u32,
1040        }),
1041        I32Store { memarg } => Some(WasmOp::I32Store {
1042            offset: memarg.offset as u32,
1043            align: memarg.align as u32,
1044        }),
1045        // #372: full-width i64 load/store. The selector already lowers these to
1046        // a lo/hi i32 register-pair access (`generate_i64_load/store_with_bounds_check`,
1047        // reusing the #171 pair regalloc) — only the decoder arm was missing, so
1048        // `i64.load`/`i64.store` fell through `_ => None` and (since v0.11.46)
1049        // loud-skipped their function. The narrow forms (I64Load8.. / I64Store32)
1050        // were already decoded below.
1051        I64Load { memarg } => Some(WasmOp::I64Load {
1052            offset: memarg.offset as u32,
1053            align: memarg.align as u32,
1054        }),
1055        I64Store { memarg } => Some(WasmOp::I64Store {
1056            offset: memarg.offset as u32,
1057            align: memarg.align as u32,
1058        }),
1059
1060        // Sub-word loads (i32)
1061        I32Load8S { memarg } => Some(WasmOp::I32Load8S {
1062            offset: memarg.offset as u32,
1063            align: memarg.align as u32,
1064        }),
1065        I32Load8U { memarg } => Some(WasmOp::I32Load8U {
1066            offset: memarg.offset as u32,
1067            align: memarg.align as u32,
1068        }),
1069        I32Load16S { memarg } => Some(WasmOp::I32Load16S {
1070            offset: memarg.offset as u32,
1071            align: memarg.align as u32,
1072        }),
1073        I32Load16U { memarg } => Some(WasmOp::I32Load16U {
1074            offset: memarg.offset as u32,
1075            align: memarg.align as u32,
1076        }),
1077
1078        // Sub-word stores (i32)
1079        I32Store8 { memarg } => Some(WasmOp::I32Store8 {
1080            offset: memarg.offset as u32,
1081            align: memarg.align as u32,
1082        }),
1083        I32Store16 { memarg } => Some(WasmOp::I32Store16 {
1084            offset: memarg.offset as u32,
1085            align: memarg.align as u32,
1086        }),
1087
1088        // Local/Global
1089        LocalGet { local_index } => Some(WasmOp::LocalGet(*local_index)),
1090        LocalSet { local_index } => Some(WasmOp::LocalSet(*local_index)),
1091        LocalTee { local_index } => Some(WasmOp::LocalTee(*local_index)),
1092        GlobalGet { global_index } => Some(WasmOp::GlobalGet(*global_index)),
1093        GlobalSet { global_index } => Some(WasmOp::GlobalSet(*global_index)),
1094
1095        // Control flow
1096        Block { .. } => Some(WasmOp::Block),
1097        Loop { .. } => Some(WasmOp::Loop),
1098        Br { relative_depth } => Some(WasmOp::Br(*relative_depth)),
1099        BrIf { relative_depth } => Some(WasmOp::BrIf(*relative_depth)),
1100        // br_table: indexed multi-way branch. Previously UNMAPPED → silently
1101        // dropped, so the selector never emitted the index dispatch and control
1102        // fell straight into the first table arm — every br_table behaved as if
1103        // it always took target 0 (gale's binary-sem WAKE path never fired). The
1104        // jump-table relative depths + default depth are preserved in order.
1105        BrTable { targets } => {
1106            let default = targets.default();
1107            let tgts: Vec<u32> = targets.targets().filter_map(Result::ok).collect();
1108            Some(WasmOp::BrTable {
1109                targets: tgts,
1110                default,
1111            })
1112        }
1113        Return => Some(WasmOp::Return),
1114        Call { function_index } => Some(WasmOp::Call(*function_index)),
1115        CallIndirect {
1116            type_index,
1117            table_index,
1118            ..
1119        } => Some(WasmOp::CallIndirect {
1120            type_index: *type_index,
1121            table_index: *table_index,
1122        }),
1123
1124        // End is needed for control flow pattern matching
1125        End => Some(WasmOp::End),
1126
1127        // Nop/Unreachable - skip these
1128        Nop | Unreachable => None,
1129
1130        // Drop is needed for br_if pattern matching
1131        Drop => Some(WasmOp::Drop),
1132
1133        // Select
1134        Select => Some(WasmOp::Select),
1135
1136        // If/Else - simplified handling
1137        If { .. } => Some(WasmOp::If),
1138        Else => Some(WasmOp::Else),
1139
1140        // i64 sub-word loads
1141        I64Load8S { memarg } => Some(WasmOp::I64Load8S {
1142            offset: memarg.offset as u32,
1143            align: memarg.align as u32,
1144        }),
1145        I64Load8U { memarg } => Some(WasmOp::I64Load8U {
1146            offset: memarg.offset as u32,
1147            align: memarg.align as u32,
1148        }),
1149        I64Load16S { memarg } => Some(WasmOp::I64Load16S {
1150            offset: memarg.offset as u32,
1151            align: memarg.align as u32,
1152        }),
1153        I64Load16U { memarg } => Some(WasmOp::I64Load16U {
1154            offset: memarg.offset as u32,
1155            align: memarg.align as u32,
1156        }),
1157        I64Load32S { memarg } => Some(WasmOp::I64Load32S {
1158            offset: memarg.offset as u32,
1159            align: memarg.align as u32,
1160        }),
1161        I64Load32U { memarg } => Some(WasmOp::I64Load32U {
1162            offset: memarg.offset as u32,
1163            align: memarg.align as u32,
1164        }),
1165
1166        // i64 sub-word stores
1167        I64Store8 { memarg } => Some(WasmOp::I64Store8 {
1168            offset: memarg.offset as u32,
1169            align: memarg.align as u32,
1170        }),
1171        I64Store16 { memarg } => Some(WasmOp::I64Store16 {
1172            offset: memarg.offset as u32,
1173            align: memarg.align as u32,
1174        }),
1175        I64Store32 { memarg } => Some(WasmOp::I64Store32 {
1176            offset: memarg.offset as u32,
1177            align: memarg.align as u32,
1178        }),
1179
1180        // Memory management
1181        MemorySize { mem, .. } => Some(WasmOp::MemorySize(*mem)),
1182        MemoryGrow { mem, .. } => Some(WasmOp::MemoryGrow(*mem)),
1183
1184        // Bulk memory (#374). The backend supports a single linear memory
1185        // (memory 0); any non-zero memory index falls through to `_ => None` and
1186        // loud-skips the function (GI-FPU-001 honesty contract) rather than
1187        // miscompiling a multi-memory copy. memory.copy reads dst/src memories;
1188        // memory.fill one. The selector lowers these to a bounds-checked byte
1189        // loop (see select_with_stack).
1190        MemoryCopy {
1191            dst_mem: 0,
1192            src_mem: 0,
1193        } => Some(WasmOp::MemoryCopy),
1194        MemoryFill { mem: 0 } => Some(WasmOp::MemoryFill),
1195
1196        // ========================================================================
1197        // v128 SIMD operations (WASM SIMD proposal, 0xFD prefix)
1198        // ========================================================================
1199        V128Const { value } => {
1200            let mut bytes = [0u8; 16];
1201            bytes.copy_from_slice(value.bytes());
1202            Some(WasmOp::V128Const(bytes))
1203        }
1204        V128Load { memarg } => Some(WasmOp::V128Load {
1205            offset: memarg.offset as u32,
1206            align: memarg.align as u32,
1207        }),
1208        V128Store { memarg } => Some(WasmOp::V128Store {
1209            offset: memarg.offset as u32,
1210            align: memarg.align as u32,
1211        }),
1212
1213        // v128 bitwise
1214        V128And => Some(WasmOp::V128And),
1215        V128Or => Some(WasmOp::V128Or),
1216        V128Xor => Some(WasmOp::V128Xor),
1217        V128Not => Some(WasmOp::V128Not),
1218        V128AndNot => Some(WasmOp::V128AndNot),
1219
1220        // i8x16
1221        I8x16Add => Some(WasmOp::I8x16Add),
1222        I8x16Sub => Some(WasmOp::I8x16Sub),
1223        I8x16Neg => Some(WasmOp::I8x16Neg),
1224        I8x16Eq => Some(WasmOp::I8x16Eq),
1225        I8x16Ne => Some(WasmOp::I8x16Ne),
1226        I8x16LtS => Some(WasmOp::I8x16LtS),
1227        I8x16LtU => Some(WasmOp::I8x16LtU),
1228        I8x16GtS => Some(WasmOp::I8x16GtS),
1229        I8x16GtU => Some(WasmOp::I8x16GtU),
1230        I8x16LeS => Some(WasmOp::I8x16LeS),
1231        I8x16LeU => Some(WasmOp::I8x16LeU),
1232        I8x16GeS => Some(WasmOp::I8x16GeS),
1233        I8x16GeU => Some(WasmOp::I8x16GeU),
1234        I8x16Splat => Some(WasmOp::I8x16Splat),
1235        I8x16ExtractLaneS { lane } => Some(WasmOp::I8x16ExtractLaneS(*lane)),
1236        I8x16ExtractLaneU { lane } => Some(WasmOp::I8x16ExtractLaneU(*lane)),
1237        I8x16ReplaceLane { lane } => Some(WasmOp::I8x16ReplaceLane(*lane)),
1238        I8x16Shuffle { lanes } => Some(WasmOp::I8x16Shuffle(*lanes)),
1239        I8x16Swizzle => Some(WasmOp::I8x16Swizzle),
1240
1241        // i16x8
1242        I16x8Add => Some(WasmOp::I16x8Add),
1243        I16x8Sub => Some(WasmOp::I16x8Sub),
1244        I16x8Mul => Some(WasmOp::I16x8Mul),
1245        I16x8Neg => Some(WasmOp::I16x8Neg),
1246        I16x8Eq => Some(WasmOp::I16x8Eq),
1247        I16x8Ne => Some(WasmOp::I16x8Ne),
1248        I16x8LtS => Some(WasmOp::I16x8LtS),
1249        I16x8LtU => Some(WasmOp::I16x8LtU),
1250        I16x8GtS => Some(WasmOp::I16x8GtS),
1251        I16x8GtU => Some(WasmOp::I16x8GtU),
1252        I16x8LeS => Some(WasmOp::I16x8LeS),
1253        I16x8LeU => Some(WasmOp::I16x8LeU),
1254        I16x8GeS => Some(WasmOp::I16x8GeS),
1255        I16x8GeU => Some(WasmOp::I16x8GeU),
1256        I16x8Splat => Some(WasmOp::I16x8Splat),
1257        I16x8ExtractLaneS { lane } => Some(WasmOp::I16x8ExtractLaneS(*lane)),
1258        I16x8ExtractLaneU { lane } => Some(WasmOp::I16x8ExtractLaneU(*lane)),
1259        I16x8ReplaceLane { lane } => Some(WasmOp::I16x8ReplaceLane(*lane)),
1260
1261        // i32x4
1262        I32x4Add => Some(WasmOp::I32x4Add),
1263        I32x4Sub => Some(WasmOp::I32x4Sub),
1264        I32x4Mul => Some(WasmOp::I32x4Mul),
1265        I32x4Neg => Some(WasmOp::I32x4Neg),
1266        I32x4Eq => Some(WasmOp::I32x4Eq),
1267        I32x4Ne => Some(WasmOp::I32x4Ne),
1268        I32x4LtS => Some(WasmOp::I32x4LtS),
1269        I32x4LtU => Some(WasmOp::I32x4LtU),
1270        I32x4GtS => Some(WasmOp::I32x4GtS),
1271        I32x4GtU => Some(WasmOp::I32x4GtU),
1272        I32x4LeS => Some(WasmOp::I32x4LeS),
1273        I32x4LeU => Some(WasmOp::I32x4LeU),
1274        I32x4GeS => Some(WasmOp::I32x4GeS),
1275        I32x4GeU => Some(WasmOp::I32x4GeU),
1276        I32x4Splat => Some(WasmOp::I32x4Splat),
1277        I32x4ExtractLane { lane } => Some(WasmOp::I32x4ExtractLane(*lane)),
1278        I32x4ReplaceLane { lane } => Some(WasmOp::I32x4ReplaceLane(*lane)),
1279
1280        // i64x2
1281        I64x2Add => Some(WasmOp::I64x2Add),
1282        I64x2Sub => Some(WasmOp::I64x2Sub),
1283        I64x2Mul => Some(WasmOp::I64x2Mul),
1284        I64x2Neg => Some(WasmOp::I64x2Neg),
1285        I64x2Eq => Some(WasmOp::I64x2Eq),
1286        I64x2Ne => Some(WasmOp::I64x2Ne),
1287        I64x2LtS => Some(WasmOp::I64x2LtS),
1288        I64x2GtS => Some(WasmOp::I64x2GtS),
1289        I64x2LeS => Some(WasmOp::I64x2LeS),
1290        I64x2GeS => Some(WasmOp::I64x2GeS),
1291        I64x2Splat => Some(WasmOp::I64x2Splat),
1292        I64x2ExtractLane { lane } => Some(WasmOp::I64x2ExtractLane(*lane)),
1293        I64x2ReplaceLane { lane } => Some(WasmOp::I64x2ReplaceLane(*lane)),
1294
1295        // f32x4
1296        F32x4Add => Some(WasmOp::F32x4Add),
1297        F32x4Sub => Some(WasmOp::F32x4Sub),
1298        F32x4Mul => Some(WasmOp::F32x4Mul),
1299        F32x4Div => Some(WasmOp::F32x4Div),
1300        F32x4Abs => Some(WasmOp::F32x4Abs),
1301        F32x4Neg => Some(WasmOp::F32x4Neg),
1302        F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
1303        F32x4Eq => Some(WasmOp::F32x4Eq),
1304        F32x4Ne => Some(WasmOp::F32x4Ne),
1305        F32x4Lt => Some(WasmOp::F32x4Lt),
1306        F32x4Le => Some(WasmOp::F32x4Le),
1307        F32x4Gt => Some(WasmOp::F32x4Gt),
1308        F32x4Ge => Some(WasmOp::F32x4Ge),
1309        F32x4Splat => Some(WasmOp::F32x4Splat),
1310        F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
1311        F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
1312
1313        // Other operators not yet supported
1314        _ => None,
1315    }
1316}
1317
1318#[cfg(test)]
1319mod tests {
1320    use super::*;
1321
1322    #[test]
1323    fn test_decode_simple_add() {
1324        let wat = r#"
1325            (module
1326                (func (export "add") (param i32 i32) (result i32)
1327                    local.get 0
1328                    local.get 1
1329                    i32.add
1330                )
1331            )
1332        "#;
1333
1334        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1335        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1336
1337        assert_eq!(functions.len(), 1);
1338        assert_eq!(functions[0].index, 0);
1339        assert_eq!(functions[0].export_name, Some("add".to_string()));
1340        assert_eq!(
1341            functions[0].ops,
1342            vec![
1343                WasmOp::LocalGet(0),
1344                WasmOp::LocalGet(1),
1345                WasmOp::I32Add,
1346                WasmOp::End
1347            ]
1348        );
1349    }
1350
1351    /// #204 regression: `i64.extend_i32_u`, `i64.extend_i32_s` and
1352    /// `i32.wrap_i64` must DECODE (they were previously unmapped → silently
1353    /// dropped by `convert_operator`, leaving an i32 value as a 64-bit operand
1354    /// with a garbage high half — the root cause of gale's miscompiled
1355    /// `(new_count << 32)` pack). The decoder must surface all three.
1356    #[test]
1357    fn test_decode_i64_i32_width_conversions() {
1358        let wat = r#"
1359            (module
1360                (func (export "conv") (param i32 i64) (result i32)
1361                    local.get 0
1362                    i64.extend_i32_u
1363                    local.get 0
1364                    i64.extend_i32_s
1365                    i64.add
1366                    local.get 1
1367                    i64.add
1368                    i32.wrap_i64
1369                )
1370            )
1371        "#;
1372        let wasm = wat::parse_str(wat).expect("parse");
1373        let functions = decode_wasm_functions(&wasm).expect("decode");
1374        let ops = &functions[0].ops;
1375        assert!(
1376            ops.contains(&WasmOp::I64ExtendI32U),
1377            "i64.extend_i32_u must decode (not be dropped): {ops:?}"
1378        );
1379        assert!(
1380            ops.contains(&WasmOp::I64ExtendI32S),
1381            "i64.extend_i32_s must decode (not be dropped): {ops:?}"
1382        );
1383        assert!(
1384            ops.contains(&WasmOp::I32WrapI64),
1385            "i32.wrap_i64 must decode (not be dropped): {ops:?}"
1386        );
1387    }
1388
1389    /// #204 WAKE-path regression: `br_table` must DECODE (it was unmapped in
1390    /// `convert_operator` → silently dropped, so the selector emitted no index
1391    /// dispatch and every `br_table` fell through to target 0 — gale's binary
1392    /// semaphore never took its WAKE branch). Targets + default are preserved.
1393    #[test]
1394    fn test_decode_br_table() {
1395        let wat = r#"
1396            (module
1397                (func (export "bt") (param i32) (result i32)
1398                    (block (block (block
1399                        local.get 0
1400                        br_table 2 0 1 2)
1401                      i32.const 30 return)
1402                      i32.const 20 return)
1403                    i32.const 10))
1404        "#;
1405        let wasm = wat::parse_str(wat).expect("parse");
1406        let functions = decode_wasm_functions(&wasm).expect("decode");
1407        let bt = functions[0]
1408            .ops
1409            .iter()
1410            .find_map(|o| match o {
1411                WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
1412                _ => None,
1413            })
1414            .expect("br_table must decode (not be dropped)");
1415        assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
1416        assert_eq!(bt.1, 2, "br_table default preserved");
1417    }
1418
1419    #[test]
1420    fn test_decode_arithmetic() {
1421        let wat = r#"
1422            (module
1423                (func (export "calc") (result i32)
1424                    i32.const 5
1425                    i32.const 3
1426                    i32.mul
1427                    i32.const 2
1428                    i32.add
1429                )
1430            )
1431        "#;
1432
1433        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1434        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1435
1436        assert_eq!(functions.len(), 1);
1437        assert_eq!(functions[0].export_name, Some("calc".to_string()));
1438        assert_eq!(
1439            functions[0].ops,
1440            vec![
1441                WasmOp::I32Const(5),
1442                WasmOp::I32Const(3),
1443                WasmOp::I32Mul,
1444                WasmOp::I32Const(2),
1445                WasmOp::I32Add,
1446                WasmOp::End,
1447            ]
1448        );
1449    }
1450
1451    #[test]
1452    fn test_decode_multi_function_module() {
1453        let wat = r#"
1454            (module
1455                (func $helper)
1456                (func (export "add") (param i32 i32) (result i32)
1457                    local.get 0
1458                    local.get 1
1459                    i32.add
1460                )
1461                (func (export "sub") (param i32 i32) (result i32)
1462                    local.get 0
1463                    local.get 1
1464                    i32.sub
1465                )
1466            )
1467        "#;
1468
1469        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1470        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1471
1472        assert_eq!(functions.len(), 3);
1473        assert_eq!(functions[0].index, 0);
1474        assert_eq!(functions[0].export_name, None);
1475        assert_eq!(functions[1].index, 1);
1476        assert_eq!(functions[1].export_name, Some("add".to_string()));
1477        assert_eq!(functions[2].index, 2);
1478        assert_eq!(functions[2].export_name, Some("sub".to_string()));
1479    }
1480
1481    #[test]
1482    fn test_decode_module_with_imports() {
1483        let wat = r#"
1484            (module
1485                (import "env" "log" (func $log (param i32)))
1486                (import "env" "memory" (memory 1))
1487                (func (export "run") (param i32)
1488                    local.get 0
1489                    call 0
1490                )
1491            )
1492        "#;
1493
1494        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1495        let module = decode_wasm_module(&wasm).expect("Failed to decode");
1496
1497        // Should have 2 imports (1 func, 1 memory)
1498        assert_eq!(module.imports.len(), 2);
1499        assert_eq!(module.num_imported_funcs, 1);
1500
1501        // First import is the function
1502        assert_eq!(module.imports[0].module, "env");
1503        assert_eq!(module.imports[0].name, "log");
1504        assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
1505
1506        // Second import is memory
1507        assert_eq!(module.imports[1].module, "env");
1508        assert_eq!(module.imports[1].name, "memory");
1509        assert_eq!(module.imports[1].kind, ImportKind::Memory);
1510
1511        // Should have 1 local function (index 1, because import is index 0)
1512        assert_eq!(module.functions.len(), 1);
1513        assert_eq!(module.functions[0].index, 1);
1514        assert_eq!(module.functions[0].export_name, Some("run".to_string()));
1515    }
1516
1517    #[test]
1518    fn test_find_function_by_export_name() {
1519        let wat = r#"
1520            (module
1521                (func $helper)
1522                (func (export "add") (param i32 i32) (result i32)
1523                    local.get 0
1524                    local.get 1
1525                    i32.add
1526                )
1527            )
1528        "#;
1529
1530        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1531        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1532
1533        let add_func = functions
1534            .iter()
1535            .find(|f| f.export_name.as_deref() == Some("add"))
1536            .expect("Should find 'add' function");
1537
1538        assert_eq!(add_func.index, 1);
1539        assert!(add_func.ops.contains(&WasmOp::I32Add));
1540    }
1541
1542    #[test]
1543    fn test_decode_subword_loads() {
1544        let wat = r#"
1545            (module
1546                (memory 1)
1547                (func (export "test") (param i32) (result i32)
1548                    local.get 0
1549                    i32.load8_u
1550                )
1551            )
1552        "#;
1553
1554        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1555        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1556
1557        assert_eq!(functions.len(), 1);
1558        assert!(functions[0].ops.contains(&WasmOp::I32Load8U {
1559            offset: 0,
1560            align: 0,
1561        }));
1562    }
1563
1564    #[test]
1565    fn test_decode_subword_stores() {
1566        let wat = r#"
1567            (module
1568                (memory 1)
1569                (func (export "test") (param i32 i32)
1570                    local.get 0
1571                    local.get 1
1572                    i32.store8
1573                )
1574            )
1575        "#;
1576
1577        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1578        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1579
1580        assert_eq!(functions.len(), 1);
1581        assert!(functions[0].ops.contains(&WasmOp::I32Store8 {
1582            offset: 0,
1583            align: 0,
1584        }));
1585    }
1586
1587    #[test]
1588    fn test_decode_memory_size_grow() {
1589        let wat = r#"
1590            (module
1591                (memory 1)
1592                (func (export "test") (result i32)
1593                    memory.size
1594                )
1595            )
1596        "#;
1597
1598        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1599        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1600
1601        assert_eq!(functions.len(), 1);
1602        assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
1603    }
1604
1605    #[test]
1606    fn test_decode_memory_grow() {
1607        let wat = r#"
1608            (module
1609                (memory 1)
1610                (func (export "test") (param i32) (result i32)
1611                    local.get 0
1612                    memory.grow
1613                )
1614            )
1615        "#;
1616
1617        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1618        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1619
1620        assert_eq!(functions.len(), 1);
1621        assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
1622    }
1623
1624    #[test]
1625    fn test_decode_bulk_memory_374() {
1626        // #374: memory.copy / memory.fill on the single linear memory decode to
1627        // the new WasmOp variants (was `_ => None` -> loud-skip).
1628        let wat = r#"
1629            (module
1630                (memory 1)
1631                (func (export "cpy") (param i32 i32 i32)
1632                    local.get 0 local.get 1 local.get 2 memory.copy)
1633                (func (export "fil") (param i32 i32 i32)
1634                    local.get 0 local.get 1 local.get 2 memory.fill)
1635            )
1636        "#;
1637        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1638        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1639        assert_eq!(functions.len(), 2);
1640        assert!(functions[0].ops.contains(&WasmOp::MemoryCopy));
1641        assert!(functions[1].ops.contains(&WasmOp::MemoryFill));
1642        // Neither function is flagged unsupported (they now lower).
1643        assert!(functions[0].unsupported.is_none());
1644        assert!(functions[1].unsupported.is_none());
1645    }
1646
1647    #[test]
1648    fn test_decode_i64_subword_loads() {
1649        let wat = r#"
1650            (module
1651                (memory 1)
1652                (func (export "test") (param i32) (result i64)
1653                    local.get 0
1654                    i64.load8_s
1655                )
1656            )
1657        "#;
1658
1659        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1660        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1661
1662        assert_eq!(functions.len(), 1);
1663        assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
1664            offset: 0,
1665            align: 0,
1666        }));
1667    }
1668
1669    #[test]
1670    fn test_decode_all_subword_memory_ops() {
1671        // Test that all sub-word operations are decoded from WAT
1672        let wat = r#"
1673            (module
1674                (memory 1)
1675                (func (export "test") (param i32)
1676                    ;; i32 sub-word loads
1677                    local.get 0
1678                    i32.load8_s
1679                    drop
1680                    local.get 0
1681                    i32.load8_u
1682                    drop
1683                    local.get 0
1684                    i32.load16_s
1685                    drop
1686                    local.get 0
1687                    i32.load16_u
1688                    drop
1689
1690                    ;; i32 sub-word stores
1691                    local.get 0
1692                    i32.const 42
1693                    i32.store8
1694                    local.get 0
1695                    i32.const 42
1696                    i32.store16
1697
1698                    ;; i64 sub-word loads
1699                    local.get 0
1700                    i64.load8_s
1701                    drop
1702                    local.get 0
1703                    i64.load8_u
1704                    drop
1705                    local.get 0
1706                    i64.load16_s
1707                    drop
1708                    local.get 0
1709                    i64.load16_u
1710                    drop
1711                    local.get 0
1712                    i64.load32_s
1713                    drop
1714                    local.get 0
1715                    i64.load32_u
1716                    drop
1717
1718                    ;; i64 sub-word stores
1719                    local.get 0
1720                    i64.const 42
1721                    i64.store8
1722                    local.get 0
1723                    i64.const 42
1724                    i64.store16
1725                    local.get 0
1726                    i64.const 42
1727                    i64.store32
1728                )
1729            )
1730        "#;
1731
1732        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1733        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1734
1735        assert_eq!(functions.len(), 1);
1736        let ops = &functions[0].ops;
1737
1738        // Verify i32 sub-word ops are present
1739        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
1740        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
1741        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
1742        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
1743        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
1744        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
1745
1746        // Verify i64 sub-word ops are present
1747        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
1748        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
1749        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
1750        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
1751        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
1752        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
1753        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
1754        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
1755        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
1756    }
1757
1758    #[test]
1759    fn test_decode_simd_i32x4_add() {
1760        let wat = r#"
1761            (module
1762                (func (export "add_v128") (param v128 v128) (result v128)
1763                    local.get 0
1764                    local.get 1
1765                    i32x4.add
1766                )
1767            )
1768        "#;
1769
1770        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1771        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1772
1773        assert_eq!(functions.len(), 1);
1774        assert!(
1775            functions[0].ops.contains(&WasmOp::I32x4Add),
1776            "Should decode i32x4.add: {:?}",
1777            functions[0].ops
1778        );
1779    }
1780
1781    #[test]
1782    fn test_decode_simd_v128_const() {
1783        let wat = r#"
1784            (module
1785                (func (export "const_v128") (result v128)
1786                    v128.const i32x4 1 2 3 4
1787                )
1788            )
1789        "#;
1790
1791        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1792        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1793
1794        assert_eq!(functions.len(), 1);
1795        assert!(
1796            functions[0]
1797                .ops
1798                .iter()
1799                .any(|o| matches!(o, WasmOp::V128Const(_))),
1800            "Should decode v128.const: {:?}",
1801            functions[0].ops
1802        );
1803    }
1804
1805    #[test]
1806    fn test_decode_simd_v128_load_store() {
1807        let wat = r#"
1808            (module
1809                (memory 1)
1810                (func (export "load_store") (param i32)
1811                    local.get 0
1812                    v128.load
1813                    local.get 0
1814                    v128.store
1815                )
1816            )
1817        "#;
1818
1819        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1820        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1821
1822        assert_eq!(functions.len(), 1);
1823        let ops = &functions[0].ops;
1824        assert!(
1825            ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
1826            "Should decode v128.load"
1827        );
1828        assert!(
1829            ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
1830            "Should decode v128.store"
1831        );
1832    }
1833
1834    #[test]
1835    fn test_decode_simd_bitwise_ops() {
1836        let wat = r#"
1837            (module
1838                (func (export "bitwise") (param v128 v128) (result v128)
1839                    local.get 0
1840                    local.get 1
1841                    v128.and
1842                )
1843            )
1844        "#;
1845
1846        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1847        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1848
1849        assert_eq!(functions.len(), 1);
1850        assert!(functions[0].ops.contains(&WasmOp::V128And));
1851    }
1852
1853    #[test]
1854    fn test_decode_simd_splat() {
1855        let wat = r#"
1856            (module
1857                (func (export "splat") (param i32) (result v128)
1858                    local.get 0
1859                    i32x4.splat
1860                )
1861            )
1862        "#;
1863
1864        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1865        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1866
1867        assert_eq!(functions.len(), 1);
1868        assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
1869    }
1870
1871    #[test]
1872    fn test_decode_simd_extract_lane() {
1873        let wat = r#"
1874            (module
1875                (func (export "extract") (param v128) (result i32)
1876                    local.get 0
1877                    i32x4.extract_lane 2
1878                )
1879            )
1880        "#;
1881
1882        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1883        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1884
1885        assert_eq!(functions.len(), 1);
1886        assert!(
1887            functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
1888            "Should decode i32x4.extract_lane 2"
1889        );
1890    }
1891
1892    #[test]
1893    fn test_decode_simd_f32x4_arithmetic() {
1894        let wat = r#"
1895            (module
1896                (func (export "f32x4_add") (param v128 v128) (result v128)
1897                    local.get 0
1898                    local.get 1
1899                    f32x4.add
1900                )
1901            )
1902        "#;
1903
1904        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1905        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1906
1907        assert_eq!(functions.len(), 1);
1908        assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
1909    }
1910
1911    #[test]
1912    fn test_369_scalar_float_op_flags_function_unsupported_not_dropped() {
1913        // #369: a scalar f32/f64 op the decoder can't lower must FLAG the
1914        // function (-> loud skip), never be silently dropped (which left a
1915        // `mov r0,r1` wrong-value stub). A pure-integer function stays clean.
1916        let wat = r#"
1917            (module
1918                (func (export "fadd") (param f32 f32) (result f32)
1919                    local.get 0 local.get 1 f32.add)
1920                (func (export "iadd") (param i32 i32) (result i32)
1921                    local.get 0 local.get 1 i32.add))
1922        "#;
1923        let wasm = wat::parse_str(wat).expect("parse");
1924        let functions = decode_wasm_functions(&wasm).expect("decode");
1925        let fadd = functions
1926            .iter()
1927            .find(|f| f.export_name.as_deref() == Some("fadd"))
1928            .unwrap();
1929        let iadd = functions
1930            .iter()
1931            .find(|f| f.export_name.as_deref() == Some("iadd"))
1932            .unwrap();
1933        assert!(
1934            fadd.unsupported.is_some(),
1935            "f32.add must flag the function unsupported (loud-skip), got {:?}",
1936            fadd.unsupported
1937        );
1938        assert!(
1939            fadd.unsupported.as_deref().unwrap().contains("F32Add"),
1940            "diagnostic should name the op: {:?}",
1941            fadd.unsupported
1942        );
1943        assert!(
1944            iadd.unsupported.is_none(),
1945            "a pure-integer function must NOT be flagged: {:?}",
1946            iadd.unsupported
1947        );
1948    }
1949
1950    #[test]
1951    fn test_decode_simd_multiple_ops() {
1952        let wat = r#"
1953            (module
1954                (func (export "simd_ops") (param v128 v128 v128) (result v128)
1955                    ;; (a + b) * c
1956                    local.get 0
1957                    local.get 1
1958                    i32x4.add
1959                    local.get 2
1960                    i32x4.mul
1961                )
1962            )
1963        "#;
1964
1965        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
1966        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1967
1968        assert_eq!(functions.len(), 1);
1969        let ops = &functions[0].ops;
1970        assert!(ops.contains(&WasmOp::I32x4Add));
1971        assert!(ops.contains(&WasmOp::I32x4Mul));
1972    }
1973
1974    /// VCR-DBG-001 step 1 (#394): the decoder records a module-relative wasm byte
1975    /// offset per emitted op — the DWARF-for-wasm address space that bridges
1976    /// synth's op-index `source_line` to the input wasm's `.debug_line`. Purely
1977    /// additive metadata (no codegen consumer ⇒ frozen fixtures byte-identical,
1978    /// verified separately); this test pins the structural invariants.
1979    #[test]
1980    fn test_decode_records_aligned_increasing_op_offsets_dbg001() {
1981        let wat = r#"
1982            (module
1983                (func (export "f") (param i32 i32) (result i32)
1984                    local.get 0
1985                    local.get 1
1986                    i32.add
1987                    i32.const 7
1988                    i32.mul))
1989        "#;
1990        let wasm = wat::parse_str(wat).expect("parse WAT");
1991        let functions = decode_wasm_functions(&wasm).expect("decode");
1992        let f = &functions[0];
1993
1994        // One offset per emitted op, index-aligned with `ops`.
1995        assert_eq!(
1996            f.op_offsets.len(),
1997            f.ops.len(),
1998            "op_offsets must be parallel to ops"
1999        );
2000        assert!(!f.op_offsets.is_empty());
2001
2002        // Byte offsets are strictly increasing through the body (each op consumes
2003        // at least one byte) and module-relative (well past the header).
2004        assert!(
2005            f.op_offsets.windows(2).all(|w| w[1] > w[0]),
2006            "wasm byte offsets must strictly increase: {:?}",
2007            f.op_offsets
2008        );
2009        assert!(
2010            f.op_offsets[0] >= 8,
2011            "module-relative offset is past the 8-byte wasm header"
2012        );
2013    }
2014
2015    /// #237: the decoder captures a global's `i32.const` initializer + mutability,
2016    /// so the native-pointer ABI can recognize the stack-pointer global.
2017    #[test]
2018    fn test_decode_captures_global_initializer() {
2019        let wat = r#"
2020            (module
2021                (memory 2)
2022                (global $__stack_pointer (mut i32) (i32.const 65536))
2023                (global $immutable_const i32 (i32.const 7))
2024                (func (export "f") (result i32) global.get 0)
2025            )
2026        "#;
2027        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2028        let module = decode_wasm_module(&wasm).expect("Failed to decode");
2029
2030        assert_eq!(module.globals.len(), 2, "both globals captured");
2031        let sp = &module.globals[0];
2032        assert_eq!(sp.index, 0);
2033        assert_eq!(sp.init_i32, Some(65536), "stack-pointer init captured");
2034        assert!(sp.mutable, "stack pointer is mutable");
2035        let c = &module.globals[1];
2036        assert_eq!(c.init_i32, Some(7));
2037        assert!(!c.mutable, "second global is immutable");
2038        assert_eq!(sp.slot_bytes, 4, "i32 global occupies one 4-byte slot");
2039        assert_eq!(c.slot_bytes, 4);
2040    }
2041
2042    /// #643: the decoder records the DECLARED slot width per global — an i64
2043    /// (or f64) global occupies 8 bytes, so the globals-table layout can give
2044    /// it room for both words and shift every later global's offset.
2045    #[test]
2046    fn test_decode_records_global_slot_widths_643() {
2047        let wat = r#"
2048            (module
2049                (global $c (mut i64) (i64.const 0))
2050                (global $k (mut i32) (i32.const 0))
2051                (global $f (mut f64) (f64.const 0))
2052                (func (export "f") (result i32) global.get 1)
2053            )
2054        "#;
2055        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2056        let module = decode_wasm_module(&wasm).expect("Failed to decode");
2057
2058        assert_eq!(module.globals.len(), 3);
2059        assert_eq!(module.globals[0].slot_bytes, 8, "i64 global is 8 bytes");
2060        assert_eq!(module.globals[1].slot_bytes, 4, "i32 global is 4 bytes");
2061        assert_eq!(module.globals[2].slot_bytes, 8, "f64 global is 8 bytes");
2062    }
2063
2064    /// #509: the decoder records `(param_count, result_count)` for every
2065    /// `Block`/`Loop`/`If`, ordinal-keyed in op order, covering all three
2066    /// blocktype encodings: `Empty → (0,0)`, `ValType → (0,1)`, and
2067    /// `FuncType(i) →` counts from the type section (here a multi-result
2068    /// block, which wat encodes as a functype blocktype).
2069    #[test]
2070    fn test_decode_records_block_arity_side_table_509() {
2071        let wat = r#"
2072            (module
2073                (func (export "f") (param i32) (result i32)
2074                    (block (result i32)
2075                        (block (nop))
2076                        (local.get 0)
2077                        (if (result i32)
2078                            (then (i32.const 1))
2079                            (else (i32.const 2)))))
2080                (func (export "g") (result i32)
2081                    (block (result i32 i32)
2082                        (i32.const 1) (i32.const 2))
2083                    i32.add)
2084                (func (export "h") (param i32) (result i32)
2085                    (local.get 0)
2086                    (loop (param i32) (result i32))))
2087        "#;
2088        let wasm = wat::parse_str(wat).expect("parse WAT");
2089
2090        // Both decode entry points must produce the same side-table.
2091        for functions in [
2092            decode_wasm_functions(&wasm).expect("decode"),
2093            decode_wasm_module(&wasm).expect("decode").functions,
2094        ] {
2095            // f: Block(result i32), Block(void), If(result i32) — in op order.
2096            assert_eq!(
2097                functions[0].block_arity,
2098                vec![(0, 1), (0, 0), (0, 1)],
2099                "f: ValType/Empty/ValType blocktypes"
2100            );
2101            // g: one multi-result block via a FuncType blocktype.
2102            assert_eq!(
2103                functions[1].block_arity,
2104                vec![(0, 2)],
2105                "g: functype blocktype result count from the type section"
2106            );
2107            // h: a parameterized loop — the input arity is what a br to the
2108            // header would carry (the #509 loud-decline discriminator).
2109            assert_eq!(
2110                functions[2].block_arity,
2111                vec![(1, 1)],
2112                "h: loop params captured"
2113            );
2114        }
2115    }
2116
2117    /// #642: the decoder captures table 0's compile-time size, per-segment
2118    /// element shapes and per-function type indices, and the closed-world
2119    /// verdict VERIFIES a fully-covered homogeneous table.
2120    #[test]
2121    fn test_call_indirect_guards_closed_world_verified_642() {
2122        // The #642 repro shape: 3-entry table, fully covered, one signature.
2123        let wat = r#"
2124            (module
2125                (type $bin (func (param i32 i32) (result i32)))
2126                (table 3 funcref)
2127                (elem (i32.const 0) $add $sub $mul)
2128                (func $add (param i32 i32) (result i32)
2129                    (i32.add (local.get 0) (local.get 1)))
2130                (func $sub (param i32 i32) (result i32)
2131                    (i32.sub (local.get 0) (local.get 1)))
2132                (func $mul (param i32 i32) (result i32)
2133                    (i32.mul (local.get 0) (local.get 1)))
2134                (func (export "f") (param i32 i32) (result i32)
2135                    (call_indirect (type $bin)
2136                        (local.get 0) (i32.const 10) (local.get 1)))
2137            )
2138        "#;
2139        let wasm = wat::parse_str(wat).expect("parse");
2140        let module = decode_wasm_module(&wasm).expect("decode");
2141
2142        assert_eq!(module.table_size, Some(3), "table section min size");
2143        assert_eq!(
2144            module.elem_segments,
2145            vec![ElemSegmentInfo {
2146                offset: Some(0),
2147                funcs: Some(vec![0, 1, 2]),
2148            }]
2149        );
2150        // 2 type-section entries ($bin + the export's (i32 i32)->i32 dedups
2151        // to one in practice, but don't assume — just check func 0..2 share
2152        // a signature with type 0).
2153        assert_eq!(module.func_type_indices.len(), 4);
2154
2155        let guards = module.call_indirect_guards();
2156        assert_eq!(guards.table_size, Some(3));
2157        // Type index 0 ($bin) must be VERIFIED: every table entry has its
2158        // exact signature.
2159        assert_eq!(
2160            guards.type_reject.first(),
2161            Some(&None),
2162            "closed-world type check must verify the homogeneous table: {:?}",
2163            guards.type_reject
2164        );
2165    }
2166
2167    /// #642: a heterogeneous table (an entry whose signature differs from the
2168    /// expected type) must REJECT that expected type — the raw code-pointer
2169    /// table cannot be runtime-type-checked, so the lowering has to decline.
2170    #[test]
2171    fn test_call_indirect_guards_heterogeneous_table_rejects_642() {
2172        let wat = r#"
2173            (module
2174                (type $bin (func (param i32 i32) (result i32)))
2175                (type $un (func (param i32) (result i32)))
2176                (table 2 funcref)
2177                (elem (i32.const 0) $add $neg)
2178                (func $add (type $bin)
2179                    (i32.add (local.get 0) (local.get 1)))
2180                (func $neg (type $un)
2181                    (i32.sub (i32.const 0) (local.get 0)))
2182                (func (export "f") (param i32 i32) (result i32)
2183                    (call_indirect (type $bin)
2184                        (local.get 0) (i32.const 10) (local.get 1)))
2185            )
2186        "#;
2187        let wasm = wat::parse_str(wat).expect("parse");
2188        let module = decode_wasm_module(&wasm).expect("decode");
2189        let guards = module.call_indirect_guards();
2190        assert_eq!(guards.table_size, Some(2));
2191        // BOTH expected types must be rejected: the table holds one function
2192        // of each signature, so neither type's closed world holds.
2193        assert!(
2194            guards.type_reject[0].is_some() && guards.type_reject[1].is_some(),
2195            "heterogeneous table must reject every expected type: {:?}",
2196            guards.type_reject
2197        );
2198    }
2199
2200    /// #642: an uninitialized table slot (elem covers less than the declared
2201    /// size) is a null funcref — calling it must trap, which the raw
2202    /// code-pointer table cannot express at runtime → reject all types.
2203    #[test]
2204    fn test_call_indirect_guards_null_slot_rejects_642() {
2205        let wat = r#"
2206            (module
2207                (type $s (func (result i32)))
2208                (table 3 funcref)
2209                (elem (i32.const 0) $f0 $f1)
2210                (func $f0 (result i32) (i32.const 10))
2211                (func $f1 (result i32) (i32.const 11))
2212                (func (export "run") (param i32) (result i32)
2213                    (call_indirect (type $s) (local.get 0)))
2214            )
2215        "#;
2216        let wasm = wat::parse_str(wat).expect("parse");
2217        let module = decode_wasm_module(&wasm).expect("decode");
2218        let guards = module.call_indirect_guards();
2219        assert_eq!(guards.table_size, Some(3));
2220        assert!(
2221            guards.type_reject.iter().all(|r| r.is_some()),
2222            "slot 2 is a null funcref — every type must be rejected: {:?}",
2223            guards.type_reject
2224        );
2225        assert!(
2226            guards.type_reject[0].as_deref().unwrap().contains("slot 2"),
2227            "reason names the uninitialized slot: {:?}",
2228            guards.type_reject[0]
2229        );
2230    }
2231
2232    /// #642: no table at all → no compile-time bound → table_size None and
2233    /// every type rejected (the lowering declines).
2234    #[test]
2235    fn test_call_indirect_guards_no_table_642() {
2236        let wat = r#"
2237            (module
2238                (func (export "f") (param i32) (result i32) (local.get 0))
2239            )
2240        "#;
2241        let wasm = wat::parse_str(wat).expect("parse");
2242        let module = decode_wasm_module(&wasm).expect("decode");
2243        assert_eq!(module.table_size, None);
2244        let guards = module.call_indirect_guards();
2245        assert_eq!(guards.table_size, None);
2246        assert!(guards.type_reject.iter().all(|r| r.is_some()));
2247    }
2248
2249    /// #642: duplicate-but-structurally-identical types stay interchangeable —
2250    /// the closed-world check compares SIGNATURES, not type indices.
2251    #[test]
2252    fn test_call_indirect_guards_duplicate_types_verified_642() {
2253        let wat = r#"
2254            (module
2255                (type $a (func (result i32)))
2256                (type $b (func (result i32)))
2257                (table 1 funcref)
2258                (elem (i32.const 0) $f)
2259                (func $f (type $a) (i32.const 7))
2260                (func (export "run") (param i32) (result i32)
2261                    (call_indirect (type $b) (local.get 0)))
2262            )
2263        "#;
2264        let wasm = wat::parse_str(wat).expect("parse");
2265        let module = decode_wasm_module(&wasm).expect("decode");
2266        let guards = module.call_indirect_guards();
2267        // $f has type $a; the call expects $b — structurally identical, so
2268        // BOTH type indices must verify. (A third type — the export's
2269        // (i32)->i32 — is correctly rejected: different signature.)
2270        assert_eq!(
2271            &guards.type_reject[0..2],
2272            &[None, None],
2273            "structural signature comparison must accept duplicate types: {:?}",
2274            guards.type_reject
2275        );
2276        assert!(
2277            guards.type_reject[2].is_some(),
2278            "the structurally-different third type must still be rejected"
2279        );
2280    }
2281}