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 captured constant global initializer (#649). Only INTEGER `t.const` init
51/// exprs are captured: `f32.const`/`f64.const` inits deliberately decode to
52/// `None` — float-typed global ACCESS is the GI-FPU-001 (#369) loud-skip lane,
53/// and fabricating a bit-pattern here must not quietly unskip it. Non-const
54/// init exprs (e.g. `global.get` of an import) are not statically known and
55/// also decode to `None`.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum GlobalInit {
58    /// A leading `i32.const` initializer.
59    I32(i32),
60    /// A leading `i64.const` initializer — BOTH words must reach the emitted
61    /// global slot (#649: `init_i32`-shaped capture silently zeroed these).
62    I64(i64),
63}
64
65/// A WASM global's declaration — its initial value and mutability (#237).
66/// Needed so the native-pointer ABI can recognize a global whose initializer is
67/// a linear-memory address (e.g. `$__stack_pointer = 65536`) and make it
68/// `__synth_wasm_data`-relative, rather than reading it from an R9 globals table
69/// the self-contained drop-in object can't rely on.
70#[derive(Debug, Clone)]
71pub struct WasmGlobal {
72    /// Global index (defined globals; imported globals are not counted here).
73    pub index: u32,
74    /// The captured constant initializer (#237/#649): `i32.const` or
75    /// `i64.const`. Float/non-const init exprs decode to `None` — see
76    /// [`GlobalInit`].
77    pub init: Option<GlobalInit>,
78    /// Whether the global is mutable.
79    pub mutable: bool,
80    /// #643: byte width of the global's storage slot, from its declared value
81    /// type — 4 for i32/f32, 8 for i64/f64, 16 for v128. The globals table is
82    /// laid out by SUMMING these widths (not `index * 4`): an i64 global needs
83    /// room for both words, and every later global's offset shifts with it.
84    pub slot_bytes: u32,
85}
86
87impl WasmMemory {
88    /// Get initial size in bytes
89    pub fn initial_bytes(&self) -> u32 {
90        self.initial_pages * 65536
91    }
92
93    /// Get maximum size in bytes (or initial if not specified)
94    pub fn max_bytes(&self) -> u32 {
95        self.max_pages.unwrap_or(self.initial_pages) * 65536
96    }
97}
98
99/// #642: one element segment's statically-decoded shape — see
100/// [`DecodedModule::elem_segments`].
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ElemSegmentInfo {
103    /// #650: the table this ACTIVE segment initializes (0 for the pre-#650
104    /// single-table form). Meaningless when `offset` is `None`.
105    pub table_index: u32,
106    /// Const i32 offset of an ACTIVE segment into its table; `None` =
107    /// placement not statically verifiable (passive/declared segment or a
108    /// non-const offset expression).
109    pub offset: Option<u32>,
110    /// The segment's function indices in slot order; `None` = contents not
111    /// statically verifiable (an entry was not a plain `ref.func`).
112    pub funcs: Option<Vec<u32>>,
113}
114
115/// #642/#650: one table's `call_indirect` guard inputs — see
116/// [`CallIndirectGuards`] for the layout contract and soundness argument.
117#[derive(Debug, Clone, Default, PartialEq, Eq)]
118pub struct TableGuards {
119    /// Compile-time size of this table (entries); `None` = no sound bound
120    /// known (an imported table with growable limits).
121    pub table_size: Option<u32>,
122    /// #650: byte offset of this table's base within the contiguous R11
123    /// region — `sum(size(0..N)) * 4`, a compile-time constant. `None` when
124    /// any PRECEDING table's size is unknown (the base is then not a
125    /// compile-time constant and the lowering declines).
126    pub base_byte_offset: Option<u32>,
127    /// Per expected-type index: `None` = closed-world type property VERIFIED
128    /// against THIS table; `Some(reason)` = not verifiable (the lowering
129    /// declines).
130    pub type_reject: Vec<Option<String>>,
131    /// #664: whether this table's image contains at least one uninitialized
132    /// (null funcref) slot. WASM Core §4.4.8 requires a `call_indirect`
133    /// reaching a null slot to TRAP — the closed-world type check verifies
134    /// the INITIALIZED slots only, and the lowering must emit a runtime
135    /// null check (pointer == 0 → trap) before the indirect branch when
136    /// this is set. `false` for a fully-initialized table keeps today's
137    /// exact dispatch bytes (no null check) BY CONSTRUCTION. Only
138    /// meaningful when the type verdict is `None` (verified); reject paths
139    /// decline before it is consulted.
140    pub has_null_slots: bool,
141    /// #676: this table's image is statically known but HETEROGENEOUS — its
142    /// initialized slots span at least two distinct STRUCTURAL signature
143    /// classes, so no expected type's closed world can hold
144    /// (`type_reject[t]` is `Some` for every `t`) — yet the mismatch trap
145    /// (WASM Core §4.4.8) IS dischargeable at runtime: the type-id sidecar
146    /// (see [`CallIndirectGuards`]) carries each slot's structural class id,
147    /// and the dispatch compares the indexed slot's id against the expected
148    /// type's class id (a compile-time immediate), trapping on inequality.
149    /// When set (and [`CallIndirectGuards::type_ids_byte_offset`] is known),
150    /// the lowering emits that runtime check INSTEAD of declining. `false`
151    /// keeps the pre-#676 behavior: verified tables dispatch unchecked
152    /// (byte-identical), unverifiable tables decline.
153    pub runtime_type_check: bool,
154}
155
156/// #642/#650: everything the `call_indirect` lowering needs to emit its
157/// guards — computed once per module by
158/// [`DecodedModule::call_indirect_guards`] and threaded to the instruction
159/// selector via `CompileConfig`.
160///
161/// ## The R11 multi-table layout contract (#650)
162///
163/// The runtime/harness links every funcref table as ONE contiguous region of
164/// raw 4-byte code pointers based at R11, in declaration order (imported
165/// tables first): table 0 at `R11 + 0`, table N at
166/// `R11 + sum(size(0..N)) * 4`. The offsets are compile-time constants
167/// because tables are provably fixed-size (`table.grow`/`table.set` are
168/// unsupported ops whose functions loud-skip at decode — #642). A
169/// single-table module degenerates to the pre-#650 contract (table 0 at
170/// R11, offset 0) BY CONSTRUCTION, keeping its emitted bytes identical.
171///
172/// WASM Core §4.4.8 requires `call_indirect` to trap when `index >=
173/// table.size` and when the callee's type does not match the instruction's
174/// expected type. The region stores no size fields and no type ids, so, per
175/// table:
176///  - the BOUNDS check is emitted at runtime against THAT table's
177///    compile-time `table_size` immediate (sound: fixed-size, see above), and
178///  - the TYPE check is discharged at COMPILE time: for expected type `t`,
179///    `tables[n].type_reject[t]` is `None` only when every INITIALIZED slot
180///    of table `n` verifiably holds a function whose signature structurally
181///    equals type `t` (the closed-world property — no runtime mismatch is
182///    then possible). Otherwise it holds the reason, and the lowering
183///    declines LOUDLY rather than emit an unchecked indirect branch, and
184///  - a NULL (uninitialized) slot traps at RUNTIME (#664): the layout
185///    contract requires the runtime/harness to link every uninitialized
186///    slot as a ZERO word (null funcref has no code address; 0 is never a
187///    valid function pointer in the region), and when `has_null_slots` is
188///    set the dispatch emits a null check on the loaded pointer
189///    (`CMP #0` → trap) between the bounds guard and the indirect branch.
190///    A fully-initialized table (`has_null_slots == false`) keeps the
191///    pre-#664 dispatch bytes identical BY CONSTRUCTION, and
192///  - a HETEROGENEOUS table (mixed signatures — the closed-world property
193///    cannot hold for ANY expected type) is dispatched through a runtime
194///    type check against the **type-id sidecar** (#676): a parallel `u32`
195///    array the layout contract places at `R11 + type_ids_byte_offset`
196///    (immediately after the LAST table's pointer words, i.e. at
197///    `sum(size(0..num_tables)) * 4`), mirroring the pointer region slot
198///    for slot — table N's type-ids start at
199///    `R11 + type_ids_byte_offset + base_byte_offset(N)`. Each word is the
200///    slot's STRUCTURAL signature class id: structurally-equal function
201///    types share one dense id (1-based, first-occurrence order over the
202///    type section); id **0 is reserved for null slots**, so the type
203///    compare (expected ids are always >= 1) subsumes the #664 null trap
204///    in the same `CMP`. The dispatch loads `type_id[idx]`, compares it
205///    against the expected type's class id (compile-time immediate) and
206///    traps (`UDF`) on mismatch — WASM Core §4.4.8's runtime type check —
207///    before the pointer load and `BLX`. The sidecar words are emitted
208///    into the relocatable object as the `.synth.table_type_ids` section
209///    (non-ALLOC metadata, like `.meld_import_table`): the runtime/harness
210///    that links the pointer region copies them to
211///    `R11 + type_ids_byte_offset` verbatim — it never re-derives ids. A
212///    module with NO heterogeneous table emits no sidecar and no runtime
213///    type check anywhere: homogeneous dispatch bytes stay identical BY
214///    CONSTRUCTION (the #650 offset-0 / #664 `null_check: false` trick).
215///
216///    pre-#664 dispatch bytes identical BY CONSTRUCTION.
217///
218/// ## Companion: the self-contained SRAM layout contract (#687)
219///
220/// The R11 register above is ALSO the linear-memory base register, whose
221/// placement inside SRAM is governed by the self-contained image's stack
222/// layout: `--stack-layout=high` (default) keeps linmem at the SRAM start
223/// with the stack growing down from the top; `--stack-layout=low` reserves
224/// the stack at the SRAM BOTTOM and shifts linmem/globals (and the optimized
225/// path's absolute `0x2000_0100` base) up by the stack size, so an overflow
226/// BusFaults below SRAM instead of silently corrupting them. The full layout
227/// tables live on `build_multi_func_cortex_m_elf` in `synth-cli` (the builder
228/// that owns the addresses). Relocatable/host-linked objects are NOT covered
229/// — their linker script owns the layout, and the flag is refused there.
230#[derive(Debug, Clone, Default, PartialEq, Eq)]
231pub struct CallIndirectGuards {
232    /// Per-table guard inputs, indexed by table index (imports first). The
233    /// default (empty — no module context) DECLINES every `call_indirect`.
234    pub tables: Vec<TableGuards>,
235    /// #676: byte offset of the type-id sidecar within the R11 region — the
236    /// total pointer-region size, `sum(size(0..num_tables)) * 4`. `Some`
237    /// only when a sidecar exists: at least one table is heterogeneous
238    /// (see [`TableGuards::runtime_type_check`]) AND every table's size is
239    /// compile-time known (otherwise the sidecar base is not a constant and
240    /// heterogeneous dispatches keep declining). `None` = no sidecar.
241    pub type_ids_byte_offset: Option<u32>,
242    /// #676: the sidecar image — one `u32` structural class id per slot
243    /// across ALL tables in region order (0 = null slot). Emitted into the
244    /// object as `.synth.table_type_ids`; empty exactly when
245    /// `type_ids_byte_offset` is `None`. A table whose image is not
246    /// statically known contributes ZERO words (it declines at the
247    /// lowering, and 0 never equals an expected class id, so even a rogue
248    /// dispatch would trap, not branch).
249    pub type_ids_image: Vec<u32>,
250    /// #676: per module type index, that type's structural class id
251    /// (1-based, dense; structurally-equal duplicate types share an id).
252    /// The expected-type immediate the dispatch compares against. Empty
253    /// when `type_ids_byte_offset` is `None` (no sidecar — never consulted).
254    pub type_class_ids: Vec<u32>,
255}
256
257impl CallIndirectGuards {
258    /// Single-table (table 0 at R11 offset 0) guards — the pre-#650 shape,
259    /// used by tests and single-table call sites.
260    pub fn single_table(table_size: Option<u32>, type_reject: Vec<Option<String>>) -> Self {
261        Self {
262            tables: vec![TableGuards {
263                table_size,
264                base_byte_offset: Some(0),
265                type_reject,
266                has_null_slots: false,
267                runtime_type_check: false,
268            }],
269            ..Self::default()
270        }
271    }
272}
273
274/// Decoded WASM module with functions and memory
275#[derive(Debug, Clone)]
276pub struct DecodedModule {
277    /// Decoded functions
278    pub functions: Vec<FunctionOps>,
279    /// Linear memories
280    pub memories: Vec<WasmMemory>,
281    /// Data segments (offset, data) for memory initialization
282    pub data_segments: Vec<(u32, Vec<u8>)>,
283    /// Import entries (module name, field name, kind)
284    pub imports: Vec<ImportEntry>,
285    /// Number of imported functions (for distinguishing import calls from local calls)
286    pub num_imported_funcs: u32,
287    /// AAPCS integer-argument count per function, indexed by the *full* WASM
288    /// function index (imported functions first, then locally-defined ones).
289    /// Used by the backend to marshal call arguments into R0–R3 (issue #195).
290    /// Counts every parameter as one slot (i64/f64 over-counted — see the
291    /// backend's `set_func_arg_counts` scope note).
292    pub func_arg_counts: Vec<u32>,
293    /// AAPCS integer-argument count per *function type*, indexed by type index.
294    /// Used by `call_indirect`, whose callee arg count comes from the static
295    /// type index (issue #195).
296    pub type_arg_counts: Vec<u32>,
297    /// #311: whether each *function* (full index, imports first) returns i64 —
298    /// the call lowering must tag the result as a register PAIR (r0:r1) or the
299    /// hi half is invisible to liveness and the next constant clobbers it.
300    pub func_ret_i64: Vec<bool>,
301    /// #311: whether each *function type* returns i64 (for `call_indirect`).
302    pub type_ret_i64: Vec<bool>,
303    /// #359: declared parameter widths per *function* (full index, imports
304    /// first): `func_params_i64[f][k]` is true when param `k` is i64/f64. The
305    /// AAPCS stack-argument path needs the declared widths — op-stream inference
306    /// can't see an unused i64 param that still shifts the incoming-stack layout.
307    pub func_params_i64: Vec<Vec<bool>>,
308    /// GI-FPU-002 (#619/#369): declared f32-param mask per *function* (full
309    /// index, imports first): `func_params_f32[f][k]` is true when param `k` is
310    /// f32. The direct selector homes hard-float f32 args in S0..S15 (AAPCS-VFP),
311    /// which op-stream inference cannot recover for a pure-passthrough f32 param.
312    pub func_params_f32: Vec<Vec<bool>>,
313    /// GI-FPU-002 phase 2 (#369): declared f64-param mask per *function*
314    /// (full index, imports first): `func_params_f64[f][k]` is true when param
315    /// `k` is f64. Hard-float targets decline such functions loudly (the
316    /// legacy width inference treats the param as an i64 CORE pair — wrong
317    /// registers under AAPCS-VFP). Distinct from `func_params_i64`, which
318    /// deliberately lumps i64 and f64 for frame-layout purposes.
319    pub func_params_f64: Vec<Vec<bool>>,
320    /// GI-FPU-002 phase 2 (#719/#369): whether each *function* (full index,
321    /// imports first) returns f32. The direct selector's epilogue homes an f32
322    /// result in S0 (AAPCS-VFP); when the result value transited a core register
323    /// (e.g. it came from a call that returned f32 as an integer-tagged R0), the
324    /// epilogue must loudly decline rather than emit the integer R0 return (a
325    /// silent miscompile — the caller reads S0). Op-stream inference cannot see a
326    /// pure-passthrough f32 return, so it is carried from the declared signature.
327    pub func_ret_f32: Vec<bool>,
328    /// GI-FPU-002 phase 2 (#719/#369): whether each *function* returns f64 (D0
329    /// under AAPCS-VFP). Same epilogue-soundness role as `func_ret_f32`.
330    pub func_ret_f64: Vec<bool>,
331    /// GI-FPU-002 phase 2 (#719/#369): whether each *function type* returns
332    /// f32 / f64 — the `call_indirect` analogue of `func_ret_f32`/`func_ret_f64`
333    /// (the selector loudly declines an indirect call whose static type returns
334    /// a float this increment does not marshal, rather than tag S0/D0 as R0).
335    pub type_ret_f32: Vec<bool>,
336    /// See [`Self::type_ret_f32`].
337    pub type_ret_f64: Vec<bool>,
338    /// Defined globals with their initializers (#237). Empty if the module has
339    /// no global section. Used by the native-pointer ABI to make a global whose
340    /// initializer is a linear-memory address (e.g. `$__stack_pointer`)
341    /// self-contained rather than table-relative.
342    pub globals: Vec<WasmGlobal>,
343    /// Function indices that populate any table via an element segment (#275).
344    /// These are the possible `call_indirect` targets — a function reached only
345    /// through the table is invisible to direct-`call` reachability, so the
346    /// whole-graph closure must treat every table entry as reachable once any
347    /// reachable function performs a `call_indirect`. Empty for modules with no
348    /// element section (every leaf/direct-call module), keeping output identical.
349    pub elem_func_indices: Vec<u32>,
350    /// #642: compile-time size (in entries) of table 0 — `table_sizes[0]`,
351    /// kept as a convenience accessor. See [`Self::table_sizes`].
352    pub table_size: Option<u32>,
353    /// #650: compile-time size (in entries) per table, indexed by table index
354    /// (imported tables first, then the table section, in declaration order).
355    /// A DEFINED table's size is exact: `table.grow`/`table.set` are
356    /// unsupported ops (their functions loud-skip at decode), so nothing
357    /// synth compiles can resize or retype a table. An imported table only
358    /// yields a sound bound when its limits pin the size (`max == initial`);
359    /// otherwise its entry is `None` and the `call_indirect` lowering
360    /// declines (for that table AND for any later table, whose base offset
361    /// within the contiguous R11 region is then unknown).
362    pub table_sizes: Vec<Option<u32>>,
363    /// #642: per element segment, everything the closed-world `call_indirect`
364    /// type check needs. `offset` is the const i32 placement of an ACTIVE
365    /// segment into table `table_index` (`None` = passive/declared/non-const
366    /// offset — statically unverifiable placement); `funcs` are the segment's
367    /// function indices in slot order (`None` = an entry was not a plain
368    /// `ref.func`, e.g. `ref.null` — statically unverifiable contents).
369    pub elem_segments: Vec<ElemSegmentInfo>,
370    /// #642: type index per function, indexed by the FULL function index
371    /// (imports first, then locally-defined ones).
372    pub func_type_indices: Vec<u32>,
373    /// #642: canonical structural signature per type index (params/results
374    /// rendered as a string) — used for the closed-world `call_indirect` type
375    /// check, which must compare SIGNATURES, not raw type indices (a module
376    /// may carry structurally-identical duplicate types).
377    pub type_signatures: Vec<String>,
378    /// VCR-PERF-002 Phase 1 (#494): proven invariants from loom's `wsc.facts`
379    /// custom section, keyed by `(function index, value id)` — see
380    /// `docs/design/wsc-facts-encoding.md` (schema v1) and
381    /// [`crate::wsc_facts::parse_wsc_facts`]. FAIL-SAFE by contract (loom#231
382    /// Q4): a missing/unparseable section or unknown version yields the empty
383    /// vec, unknown fact kinds are skipped — never a decode error. Phase 1 is
384    /// ingestion only: NO codegen path consumes these yet, so emitted bytes
385    /// are unchanged whether or not a module carries the section.
386    pub wsc_facts: Vec<crate::wsc_facts::WscFact>,
387}
388
389impl DecodedModule {
390    /// #642/#650: compute the `call_indirect` guard inputs — per table, the
391    /// compile-time size for the runtime bounds check, the base byte offset
392    /// within the contiguous R11 region, and the per-expected-type
393    /// closed-world verdict that discharges the type check at compile time.
394    /// See [`CallIndirectGuards`] for the layout contract and soundness
395    /// argument.
396    pub fn call_indirect_guards(&self) -> CallIndirectGuards {
397        let n_types = self.type_signatures.len();
398
399        // A segment whose PLACEMENT is not statically attributable
400        // (passive/declared segment, non-const offset, or a table index the
401        // module does not declare) poisons EVERY table: `table.init` (itself
402        // an unsupported op) or a computed offset could land its entries
403        // anywhere, so no table's image is verifiable.
404        let global_poison: Option<&'static str> = self
405            .elem_segments
406            .iter()
407            .any(|seg| seg.offset.is_none() || seg.table_index as usize >= self.table_sizes.len())
408            .then_some(
409                "element segment is not statically verifiable (passive/declared \
410                 segment, non-const offset, out-of-range table, or non-`ref.func` \
411                 entry)",
412            );
413
414        // #676: structural signature classes — structurally-equal types share
415        // one dense 1-based id (first-occurrence order over the type section);
416        // id 0 is reserved for null slots. These feed the type-id sidecar and
417        // the expected-type compare immediate of the runtime type check.
418        let mut class_of_sig: std::collections::HashMap<&str, u32> =
419            std::collections::HashMap::new();
420        let mut type_class_ids: Vec<u32> = Vec::with_capacity(n_types);
421        for sig in &self.type_signatures {
422            let next = class_of_sig.len() as u32 + 1;
423            type_class_ids.push(*class_of_sig.entry(sig.as_str()).or_insert(next));
424        }
425
426        let mut tables = Vec::with_capacity(self.table_sizes.len());
427        // #676: per table, the slot class ids (None = image not statically
428        // known) — concatenated into the sidecar image below.
429        let mut per_table_slot_ids: Vec<Option<Vec<u32>>> =
430            Vec::with_capacity(self.table_sizes.len());
431        // Running word offset of the next table's base within the R11 region;
432        // `None` once a table of unknown size is passed (every later base is
433        // then not a compile-time constant).
434        let mut base_words: Option<u32> = Some(0);
435        for (n, &size) in self.table_sizes.iter().enumerate() {
436            let base_byte_offset = base_words.and_then(|w| w.checked_mul(4));
437            let (type_reject, has_null_slots, slot_class_ids) =
438                self.table_type_reject(n as u32, size, global_poison, n_types, &type_class_ids);
439            // #676: heterogeneous = the image is statically known and its
440            // INITIALIZED slots span >= 2 distinct structural classes (null
441            // slots — id 0 — don't count; a sparse homogeneous table stays
442            // on the #664 verified-plus-null-check path, bytes identical).
443            let runtime_type_check = slot_class_ids.as_ref().is_some_and(|ids| {
444                let mut distinct: Vec<u32> = ids.iter().copied().filter(|&c| c != 0).collect();
445                distinct.sort_unstable();
446                distinct.dedup();
447                distinct.len() >= 2
448            });
449            per_table_slot_ids.push(slot_class_ids);
450            tables.push(TableGuards {
451                table_size: size,
452                base_byte_offset,
453                type_reject,
454                has_null_slots,
455                runtime_type_check,
456            });
457            base_words = match (base_words, size) {
458                (Some(w), Some(s)) => w.checked_add(s),
459                _ => None,
460            };
461        }
462
463        // #676: the sidecar exists only when some table actually needs the
464        // runtime check AND the whole pointer region's size is compile-time
465        // known (`base_words` survived every table) — otherwise the sidecar
466        // base is not a constant and heterogeneous dispatches keep declining
467        // (their `runtime_type_check` flag is cleared so the lowering sees a
468        // plain reject).
469        let any_hetero = tables.iter().any(|t| t.runtime_type_check);
470        let type_ids_byte_offset = base_words
471            .filter(|_| any_hetero)
472            .and_then(|w| w.checked_mul(4));
473        let type_ids_image = if type_ids_byte_offset.is_some() {
474            self.table_sizes
475                .iter()
476                .zip(&per_table_slot_ids)
477                .flat_map(|(&size, ids)| match ids {
478                    Some(ids) => ids.clone(),
479                    // Image not statically known: zero words (id 0 never
480                    // matches an expected class id >= 1 — trap, not branch).
481                    None => vec![0u32; size.unwrap_or(0) as usize],
482                })
483                .collect()
484        } else {
485            for t in &mut tables {
486                t.runtime_type_check = false;
487            }
488            Vec::new()
489        };
490        CallIndirectGuards {
491            tables,
492            type_ids_byte_offset,
493            type_ids_image,
494            type_class_ids: if type_ids_byte_offset.is_some() {
495                type_class_ids
496            } else {
497                Vec::new()
498            },
499        }
500    }
501
502    /// #642/#650: the closed-world type verdicts for ONE table — `None` per
503    /// expected type when every INITIALIZED slot of table `n` verifiably
504    /// holds a function of that exact structural signature; `Some(reason)`
505    /// otherwise. The second component is `has_null_slots` (#664): whether
506    /// the table image left any slot uninitialized — a `call_indirect`
507    /// reaching one must TRAP at runtime (null check on the loaded pointer),
508    /// which the lowering emits only when this is set. Reject paths return
509    /// `false` (the verdict declines before the flag is consulted). The
510    /// third component (#676) is the table's slot class ids — per slot, the
511    /// structural signature class of the initializing function (0 for a
512    /// null slot) — `Some` exactly when the table image is statically
513    /// known; it feeds the type-id sidecar and the heterogeneity verdict.
514    fn table_type_reject(
515        &self,
516        n: u32,
517        size: Option<u32>,
518        global_poison: Option<&str>,
519        n_types: usize,
520        type_class_ids: &[u32],
521    ) -> (Vec<Option<String>>, bool, Option<Vec<u32>>) {
522        let reject_all = |reason: String| (vec![Some(reason); n_types], false, None);
523
524        if let Some(reason) = global_poison {
525            return reject_all(reason.to_string());
526        }
527        let Some(size) = size else {
528            return reject_all(format!(
529                "table {n} has no compile-time-fixed size (imported table with \
530                 growable limits)"
531            ));
532        };
533
534        // Reconstruct the table image: slot -> initializing function index.
535        let mut slots: Vec<Option<u32>> = vec![None; size as usize];
536        for seg in self.elem_segments.iter().filter(|s| s.table_index == n) {
537            let (Some(off), Some(funcs)) = (seg.offset, seg.funcs.as_ref()) else {
538                // Placement is known (global_poison ruled `offset: None` out),
539                // so this is an unverifiable CONTENTS case — it poisons only
540                // the table it targets.
541                return reject_all(format!(
542                    "element segment targeting table {n} is not statically \
543                     verifiable (non-`ref.func` entry)"
544                ));
545            };
546            for (k, &f) in funcs.iter().enumerate() {
547                let Some(slot) = slots.get_mut(off as usize + k) else {
548                    return reject_all(format!(
549                        "element segment (offset {off}, {} entries) writes past \
550                         table {n}'s declared size {size}",
551                        funcs.len()
552                    ));
553                };
554                *slot = Some(f);
555            }
556        }
557        // #664: an uninitialized slot is a null funcref — calling it must
558        // trap (WASM Core §4.4.8). It no longer poisons the closed world
559        // (pre-#664 it rejected EVERY type): the layout contract requires
560        // null slots to be linked as ZERO words, so the lowering discharges
561        // the trap at RUNTIME with a null check on the loaded pointer. The
562        // type check below therefore covers the INITIALIZED slots only —
563        // a null slot can never produce a live callee of the wrong type,
564        // because the null check traps before the branch.
565        let has_null_slots = slots.iter().any(|s| s.is_none());
566
567        let rejects = (0..n_types)
568            .map(|t| {
569                for f in slots.iter().flatten() {
570                    let Some(&fty) = self.func_type_indices.get(*f as usize) else {
571                        return Some(format!(
572                            "table {n} entry references function {f} with no known type"
573                        ));
574                    };
575                    if self.type_signatures.get(fty as usize) != self.type_signatures.get(t) {
576                        return Some(format!(
577                            "table {n} entry (function {f}, type {fty}) has a different \
578                             signature than expected type {t}"
579                        ));
580                    }
581                }
582                None
583            })
584            .collect();
585        // #676: per-slot structural class ids (0 = null). `None` as soon as
586        // any initializing function's type is unknown — the image is then
587        // not statically classifiable and the table can neither verify nor
588        // carry the runtime check (the rejects above already name it).
589        let slot_class_ids: Option<Vec<u32>> = slots
590            .iter()
591            .map(|s| match s {
592                None => Some(0u32),
593                Some(f) => self
594                    .func_type_indices
595                    .get(*f as usize)
596                    .and_then(|&fty| type_class_ids.get(fty as usize).copied()),
597            })
598            .collect();
599        (rejects, has_null_slots, slot_class_ids)
600    }
601}
602
603/// Decode a WASM binary and extract functions, memory, and data segments
604pub fn decode_wasm_module(wasm_bytes: &[u8]) -> Result<DecodedModule> {
605    let mut functions = Vec::new();
606    let mut memories = Vec::new();
607    let mut data_segments = Vec::new();
608    let mut globals: Vec<WasmGlobal> = Vec::new();
609    let mut imports = Vec::new();
610    let mut func_index = 0u32;
611    let mut num_imported_funcs = 0u32;
612    let mut export_names: HashMap<u32, String> = HashMap::new();
613    // #195: per-type AAPCS arg count (indexed by type index) and per-function
614    // arg count (indexed by full function index: imports first, then locals).
615    let mut type_arg_counts: Vec<u32> = Vec::new();
616    let mut func_arg_counts: Vec<u32> = Vec::new();
617    let mut type_ret_i64: Vec<bool> = Vec::new();
618    let mut func_ret_i64: Vec<bool> = Vec::new();
619    // GI-FPU-002 phase 2 (#719/#369): per-type / per-function f32/f64 return
620    // flags, so the direct selector's epilogue can loudly decline an f32/f64
621    // result that reaches it in a core register (never a silent R0 return).
622    let mut type_ret_f32: Vec<bool> = Vec::new();
623    let mut func_ret_f32: Vec<bool> = Vec::new();
624    let mut type_ret_f64: Vec<bool> = Vec::new();
625    let mut func_ret_f64: Vec<bool> = Vec::new();
626    // #359: declared param widths per type / per function (full index).
627    let mut type_params_i64: Vec<Vec<bool>> = Vec::new();
628    let mut func_params_i64: Vec<Vec<bool>> = Vec::new();
629    // GI-FPU-002 (#619/#369): per-type / per-function declared f32-param mask,
630    // so the direct selector can home hard-float (AAPCS-VFP) f32 args in S0..S15
631    // instead of the core-register (R0..R3) integer path. Independent of
632    // `params_i64` (which lumps f64 with i64): an f32 param is neither.
633    let mut type_params_f32: Vec<Vec<bool>> = Vec::new();
634    let mut func_params_f32: Vec<Vec<bool>> = Vec::new();
635    // GI-FPU-002 phase 2 (#369): per-type / per-function f64-param mask —
636    // hard-float targets decline f64 params loudly (D-register homing is a
637    // later increment; the legacy i64-pair treatment reads wrong registers).
638    let mut type_params_f64: Vec<Vec<bool>> = Vec::new();
639    let mut func_params_f64: Vec<Vec<bool>> = Vec::new();
640    // #509: (param_count, result_count) per type index, for FuncType blocktypes.
641    let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
642    let mut elem_func_indices: Vec<u32> = Vec::new();
643    // #642/#650: call_indirect guard inputs — per-table fixed sizes (imports
644    // first, then the table section, in declaration order), per-segment
645    // static shapes, per-function type index, per-type canonical signature.
646    let mut table_sizes: Vec<Option<u32>> = Vec::new();
647    let mut elem_segments: Vec<ElemSegmentInfo> = Vec::new();
648    let mut func_type_indices: Vec<u32> = Vec::new();
649    let mut type_signatures: Vec<String> = Vec::new();
650    // #394 Tier-1.x: function index → developer-facing name from the wasm
651    // `name` custom section (function-names subsection). Applied to
652    // `FunctionOps.debug_name` after the parse loop — the custom section
653    // conventionally trails the code section, so the entries are not yet
654    // available when each `CodeSectionEntry` is decoded.
655    let mut name_section_names: HashMap<u32, String> = HashMap::new();
656    // VCR-PERF-002 Phase 1 (#494): facts from loom's `wsc.facts` custom
657    // section. `None` until (and unless) the first such section is seen —
658    // duplicates are ignored (one prover, one section; encoding doc rule).
659    let mut wsc_facts: Option<Vec<crate::wsc_facts::WscFact>> = None;
660    // GI-FPU-001 (#369): f32/f64-typed globals in the FULL global index space
661    // (imports first, then defined). `global.get`/`global.set` decode fine
662    // (they are type-agnostic ops), but there is no float lowering: the
663    // f32.const/f64.const initializer is silently dropped (`init_i32: None`
664    // → slot zeroed), so a read returns 0.0 instead of the init — a silent
665    // wrong value. Functions touching a float global loud-skip instead.
666    let mut num_imported_globals = 0u32;
667    let mut float_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
668    // #680: v128-typed globals (same index space) — a SIMD access has no
669    // lowering on any target, so touching one must loud-skip the function.
670    let mut v128_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
671    // #680: per-type "params/results contain v128" and its per-defined-function
672    // projection — a v128 param/result is expressible with ZERO SIMD-proposal
673    // operators in the body (`local.get 0` passthrough), so the operator-level
674    // catch alone would miss it.
675    let mut type_has_v128: Vec<bool> = Vec::new();
676    let mut func_sig_has_v128: Vec<bool> = Vec::new();
677
678    for payload in Parser::new(0).parse_all(wasm_bytes) {
679        let payload = payload.context("Failed to parse WASM payload")?;
680
681        match payload {
682            Payload::TypeSection(reader) => {
683                // Record the parameter count of each function type so calls can
684                // marshal the right number of arguments (issue #195).
685                for rec_group in reader {
686                    let rec_group = rec_group.context("Failed to parse type")?;
687                    for sub_ty in rec_group.types() {
688                        // #509: blocktype arity per type index (saturated u8 —
689                        // >255 params/results is far beyond anything the
690                        // selector supports anyway, and the selector declines
691                        // rather than trusting a saturated count).
692                        type_block_arity.push(match &sub_ty.composite_type.inner {
693                            wasmparser::CompositeInnerType::Func(f) => (
694                                u8::try_from(f.params().len()).unwrap_or(u8::MAX),
695                                u8::try_from(f.results().len()).unwrap_or(u8::MAX),
696                            ),
697                            _ => (u8::MAX, u8::MAX),
698                        });
699                        let (count, ret_i64, params_i64) = match &sub_ty.composite_type.inner {
700                            wasmparser::CompositeInnerType::Func(func_ty) => (
701                                func_ty.params().len() as u32,
702                                func_ty
703                                    .results()
704                                    .first()
705                                    .is_some_and(|t| *t == wasmparser::ValType::I64),
706                                // #359: i64/f64 params occupy 8 bytes / a register
707                                // pair under AAPCS. f32/f64 are not in scope for the
708                                // stack-arg path (refused), but mark both 64-bit
709                                // float and i64 so the guard catches them.
710                                func_ty
711                                    .params()
712                                    .iter()
713                                    .map(|t| {
714                                        matches!(
715                                            t,
716                                            wasmparser::ValType::I64 | wasmparser::ValType::F64
717                                        )
718                                    })
719                                    .collect::<Vec<bool>>(),
720                            ),
721                            _ => (0, false, Vec::new()),
722                        };
723                        // GI-FPU-002: declared f32-param mask for this type.
724                        let params_f32 = match &sub_ty.composite_type.inner {
725                            wasmparser::CompositeInnerType::Func(func_ty) => func_ty
726                                .params()
727                                .iter()
728                                .map(|t| matches!(t, wasmparser::ValType::F32))
729                                .collect::<Vec<bool>>(),
730                            _ => Vec::new(),
731                        };
732                        // GI-FPU-002 phase 2: declared f64-param mask.
733                        let params_f64 = match &sub_ty.composite_type.inner {
734                            wasmparser::CompositeInnerType::Func(func_ty) => func_ty
735                                .params()
736                                .iter()
737                                .map(|t| matches!(t, wasmparser::ValType::F64))
738                                .collect::<Vec<bool>>(),
739                            _ => Vec::new(),
740                        };
741                        // GI-FPU-002 phase 2: f32/f64 return flags for this type.
742                        let (ret_f32, ret_f64) = match &sub_ty.composite_type.inner {
743                            wasmparser::CompositeInnerType::Func(func_ty) => (
744                                func_ty
745                                    .results()
746                                    .first()
747                                    .is_some_and(|t| *t == wasmparser::ValType::F32),
748                                func_ty
749                                    .results()
750                                    .first()
751                                    .is_some_and(|t| *t == wasmparser::ValType::F64),
752                            ),
753                            _ => (false, false),
754                        };
755                        type_arg_counts.push(count);
756                        type_ret_i64.push(ret_i64);
757                        type_ret_f32.push(ret_f32);
758                        type_ret_f64.push(ret_f64);
759                        type_params_i64.push(params_i64);
760                        type_params_f32.push(params_f32);
761                        type_params_f64.push(params_f64);
762                        // #680: v128 anywhere in the signature.
763                        type_has_v128.push(match &sub_ty.composite_type.inner {
764                            wasmparser::CompositeInnerType::Func(f) => f
765                                .params()
766                                .iter()
767                                .chain(f.results())
768                                .any(|t| *t == wasmparser::ValType::V128),
769                            _ => false,
770                        });
771                        // #642: canonical structural signature for the
772                        // closed-world call_indirect type check (compares
773                        // SIGNATURES so duplicate types stay interchangeable).
774                        type_signatures.push(match &sub_ty.composite_type.inner {
775                            wasmparser::CompositeInnerType::Func(f) => {
776                                format!("{:?}->{:?}", f.params(), f.results())
777                            }
778                            other => format!("non-func:{other:?}"),
779                        });
780                    }
781                }
782            }
783            Payload::ImportSection(reader) => {
784                // wasmparser 0.221+ groups imports (the "compact imports"
785                // proposal): the section reader yields `Imports` groups, each of
786                // which may expand to several `Import`s. `into_imports()`
787                // flattens groups back to individual `Import`s (preserving the
788                // module/name/ty fields), keeping the per-import loop intact.
789                for import in reader.into_imports() {
790                    let import = import.context("Failed to parse import")?;
791                    let (kind, idx) = match import.ty {
792                        wasmparser::TypeRef::Func(type_idx) => {
793                            let idx = num_imported_funcs;
794                            num_imported_funcs += 1;
795                            // Record the imported function's arg count at its
796                            // full function index (imports come first).
797                            func_type_indices.push(type_idx); // #642
798                            func_arg_counts
799                                .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
800                            func_ret_i64.push(
801                                type_ret_i64
802                                    .get(type_idx as usize)
803                                    .copied()
804                                    .unwrap_or(false),
805                            );
806                            func_ret_f32.push(
807                                type_ret_f32
808                                    .get(type_idx as usize)
809                                    .copied()
810                                    .unwrap_or(false),
811                            );
812                            func_ret_f64.push(
813                                type_ret_f64
814                                    .get(type_idx as usize)
815                                    .copied()
816                                    .unwrap_or(false),
817                            );
818                            func_params_i64.push(
819                                type_params_i64
820                                    .get(type_idx as usize)
821                                    .cloned()
822                                    .unwrap_or_default(),
823                            );
824                            func_params_f32.push(
825                                type_params_f32
826                                    .get(type_idx as usize)
827                                    .cloned()
828                                    .unwrap_or_default(),
829                            );
830                            func_params_f64.push(
831                                type_params_f64
832                                    .get(type_idx as usize)
833                                    .cloned()
834                                    .unwrap_or_default(),
835                            );
836                            (ImportKind::Function(type_idx), idx)
837                        }
838                        wasmparser::TypeRef::Memory(_) => (ImportKind::Memory, 0),
839                        wasmparser::TypeRef::Table(t) => {
840                            // #642: an imported table only yields a SOUND
841                            // compile-time bound when its limits pin the size
842                            // exactly (max == initial) — a growable import
843                            // could be larger at runtime, and a bounds guard
844                            // against `initial` would trap spec-valid calls.
845                            // #650: imported tables take the leading table
846                            // indices, in declaration order.
847                            table_sizes.push(match (u32::try_from(t.initial), t.maximum) {
848                                (Ok(init), Some(max)) if u64::from(init) == max => Some(init),
849                                _ => None,
850                            });
851                            (ImportKind::Table, 0)
852                        }
853                        wasmparser::TypeRef::Global(g) => {
854                            // GI-FPU-001 (#369): imported globals come first in
855                            // the global index space — record float-typed ones
856                            // so accesses loud-skip their function.
857                            if matches!(
858                                g.content_type,
859                                wasmparser::ValType::F32 | wasmparser::ValType::F64
860                            ) {
861                                float_globals.insert(num_imported_globals);
862                            }
863                            // #680: v128-typed imported globals — same lane.
864                            if g.content_type == wasmparser::ValType::V128 {
865                                v128_globals.insert(num_imported_globals);
866                            }
867                            num_imported_globals += 1;
868                            (ImportKind::Global, 0)
869                        }
870                        _ => continue,
871                    };
872                    imports.push(ImportEntry {
873                        module: import.module.to_string(),
874                        name: import.name.to_string(),
875                        kind,
876                        index: idx,
877                    });
878                }
879            }
880            Payload::FunctionSection(reader) => {
881                // Each entry gives the type index of a locally-defined function,
882                // in order. Their full function indices follow the imports, so
883                // appending to `func_arg_counts` keeps it indexed by full index
884                // (issue #195).
885                for ty in reader {
886                    let type_idx = ty.context("Failed to parse function type index")?;
887                    func_type_indices.push(type_idx); // #642
888                    func_arg_counts
889                        .push(type_arg_counts.get(type_idx as usize).copied().unwrap_or(0));
890                    func_ret_i64.push(
891                        type_ret_i64
892                            .get(type_idx as usize)
893                            .copied()
894                            .unwrap_or(false),
895                    );
896                    func_ret_f32.push(
897                        type_ret_f32
898                            .get(type_idx as usize)
899                            .copied()
900                            .unwrap_or(false),
901                    );
902                    func_ret_f64.push(
903                        type_ret_f64
904                            .get(type_idx as usize)
905                            .copied()
906                            .unwrap_or(false),
907                    );
908                    func_params_i64.push(
909                        type_params_i64
910                            .get(type_idx as usize)
911                            .cloned()
912                            .unwrap_or_default(),
913                    );
914                    func_params_f32.push(
915                        type_params_f32
916                            .get(type_idx as usize)
917                            .cloned()
918                            .unwrap_or_default(),
919                    );
920                    func_params_f64.push(
921                        type_params_f64
922                            .get(type_idx as usize)
923                            .cloned()
924                            .unwrap_or_default(),
925                    );
926                    // #680: defined-function order matches code-entry order.
927                    func_sig_has_v128.push(
928                        type_has_v128
929                            .get(type_idx as usize)
930                            .copied()
931                            .unwrap_or(false),
932                    );
933                }
934            }
935            Payload::TableSection(reader) => {
936                // #642: a DEFINED table's compile-time size is exact — its
937                // initial size is its permanent size, because nothing synth
938                // compiles can resize it (`table.grow` is an unsupported op
939                // whose function loud-skips at decode). #650: EVERY table is
940                // recorded — the contiguous R11 region places table N at
941                // byte offset `sum(size(0..N)) * 4`.
942                for table in reader {
943                    let table = table.context("Failed to parse table")?;
944                    table_sizes.push(u32::try_from(table.ty.initial).ok());
945                }
946            }
947            Payload::MemorySection(reader) => {
948                for (idx, memory) in reader.into_iter().enumerate() {
949                    let mem = memory.context("Failed to parse memory")?;
950                    memories.push(WasmMemory {
951                        index: idx as u32,
952                        initial_pages: mem.initial as u32,
953                        max_pages: mem.maximum.map(|m| m as u32),
954                        shared: mem.shared,
955                    });
956                }
957            }
958            Payload::GlobalSection(reader) => {
959                // #237/#649: capture each defined global's constant initializer
960                // + mutability. The init is a const expr; we decode a leading
961                // `i32.const` (the `$__stack_pointer`/data-layout shape) or
962                // `i64.const` (#649: capturing only i32 silently ZEROED every
963                // nonzero i64 init). f32/f64 inits stay `None` on purpose —
964                // float global access is the GI-FPU-001 (#369) loud-skip lane —
965                // as do non-const init exprs (`global.get` of an import).
966                for (idx, global) in reader.into_iter().enumerate() {
967                    let global = global.context("Failed to parse global")?;
968                    let mut ops = global.init_expr.get_operators_reader();
969                    let init = match ops.read() {
970                        Ok(wasmparser::Operator::I32Const { value }) => {
971                            Some(GlobalInit::I32(value))
972                        }
973                        Ok(wasmparser::Operator::I64Const { value }) => {
974                            Some(GlobalInit::I64(value))
975                        }
976                        _ => None,
977                    };
978                    // #643: record the slot width from the DECLARED value type.
979                    // i64/f64 globals occupy 8 bytes (a register pair on the
980                    // 32-bit targets), v128 sixteen; laying every global out at
981                    // `index * 4` silently dropped the high word of every i64.
982                    let slot_bytes = match global.ty.content_type {
983                        wasmparser::ValType::I64 | wasmparser::ValType::F64 => 8,
984                        wasmparser::ValType::V128 => 16,
985                        _ => 4,
986                    };
987                    // GI-FPU-001 (#369): a float-typed global's initializer is
988                    // NOT captured (`init_i32` only decodes `i32.const`), so
989                    // its slot would be silently zeroed — record it so any
990                    // function accessing it loud-skips instead of reading a
991                    // silently-wrong 0.0.
992                    if matches!(
993                        global.ty.content_type,
994                        wasmparser::ValType::F32 | wasmparser::ValType::F64
995                    ) {
996                        float_globals.insert(num_imported_globals + idx as u32);
997                    }
998                    // #680: a v128-typed global's `v128.const` initializer is
999                    // not captured either (slot zeroed) and an access moves 4
1000                    // of the 16 bytes — record it so accesses loud-skip.
1001                    if global.ty.content_type == wasmparser::ValType::V128 {
1002                        v128_globals.insert(num_imported_globals + idx as u32);
1003                    }
1004                    globals.push(WasmGlobal {
1005                        index: idx as u32,
1006                        init,
1007                        mutable: global.ty.mutable,
1008                        slot_bytes,
1009                    });
1010                }
1011            }
1012            Payload::DataSection(reader) => {
1013                for data in reader {
1014                    let data = data.context("Failed to parse data segment")?;
1015                    if let wasmparser::DataKind::Active {
1016                        memory_index: 0,
1017                        offset_expr,
1018                    } = data.kind
1019                    {
1020                        let mut ops = offset_expr.get_operators_reader();
1021                        if let Ok(wasmparser::Operator::I32Const { value }) = ops.read() {
1022                            data_segments.push((value as u32, data.data.to_vec()));
1023                        }
1024                    }
1025                }
1026            }
1027            Payload::ElementSection(reader) => {
1028                // #275: collect every function index that initializes a table.
1029                // These are the `call_indirect` targets the direct-call closure
1030                // cannot see; `reachable_from_exports` unions them in when a
1031                // reachable function does a `call_indirect`. Both element forms
1032                // are handled: a flat function-index list, and the const-expr
1033                // form whose `ref.func` entries name the functions.
1034                for elem in reader {
1035                    let elem = elem.context("Failed to parse element segment")?;
1036                    // #642/#650: the segment's static placement — a const i32
1037                    // offset of an ACTIVE segment into its target table (any
1038                    // table index: the R11 region is contiguous, #650);
1039                    // anything else is unverifiable and poisons the
1040                    // closed-world type check.
1041                    let (seg_table, seg_offset): (u32, Option<u32>) = match &elem.kind {
1042                        wasmparser::ElementKind::Active {
1043                            table_index,
1044                            offset_expr,
1045                        } => {
1046                            let mut ops = offset_expr.get_operators_reader();
1047                            let off = match ops.read() {
1048                                Ok(wasmparser::Operator::I32Const { value }) => {
1049                                    u32::try_from(value).ok()
1050                                }
1051                                _ => None,
1052                            };
1053                            (table_index.unwrap_or(0), off)
1054                        }
1055                        _ => (0, None),
1056                    };
1057                    let mut seg_funcs: Option<Vec<u32>> = Some(Vec::new());
1058                    match elem.items {
1059                        wasmparser::ElementItems::Functions(funcs) => {
1060                            for f in funcs {
1061                                let f = f.context("Failed to parse element func index")?;
1062                                elem_func_indices.push(f);
1063                                if let Some(v) = seg_funcs.as_mut() {
1064                                    v.push(f);
1065                                }
1066                            }
1067                        }
1068                        wasmparser::ElementItems::Expressions(_, exprs) => {
1069                            for expr in exprs {
1070                                let expr = expr.context("Failed to parse element expr")?;
1071                                // #642: an entry is verifiable only when it is
1072                                // a single plain `ref.func` (reader yields the
1073                                // op + the implicit `end`). `ref.null` or any
1074                                // computed entry poisons the segment.
1075                                let mut entry_func: Option<u32> = None;
1076                                let mut plain = true;
1077                                for (k, op) in expr.get_operators_reader().into_iter().enumerate() {
1078                                    match (k, op.context("Failed to parse element op")?) {
1079                                        (0, wasmparser::Operator::RefFunc { function_index }) => {
1080                                            elem_func_indices.push(function_index);
1081                                            entry_func = Some(function_index);
1082                                        }
1083                                        (_, wasmparser::Operator::End) => {}
1084                                        (_, wasmparser::Operator::RefFunc { function_index }) => {
1085                                            // Keep the pre-#642 reachability
1086                                            // behaviour: every ref.func seen
1087                                            // anywhere is a possible target.
1088                                            elem_func_indices.push(function_index);
1089                                            plain = false;
1090                                        }
1091                                        _ => plain = false,
1092                                    }
1093                                }
1094                                match (plain, entry_func, seg_funcs.as_mut()) {
1095                                    (true, Some(f), Some(v)) => v.push(f),
1096                                    _ => seg_funcs = None,
1097                                }
1098                            }
1099                        }
1100                    }
1101                    elem_segments.push(ElemSegmentInfo {
1102                        table_index: seg_table,
1103                        offset: seg_offset,
1104                        funcs: seg_funcs,
1105                    });
1106                }
1107            }
1108            Payload::ExportSection(exports) => {
1109                for export in exports {
1110                    let export = export.context("Failed to parse export")?;
1111                    if export.kind == ExternalKind::Func {
1112                        export_names.insert(export.index, export.name.to_string());
1113                    }
1114                }
1115            }
1116            Payload::CodeSectionEntry(body) => {
1117                let (ops, op_offsets, block_arity, mut unsupported) =
1118                    decode_function_body(&body, &type_block_arity, &float_globals, &v128_globals)?;
1119                // #680: a v128 param/result reaches the body only through
1120                // type-agnostic ops (a `local.get 0` passthrough compiles to
1121                // a 4-byte `mov`), so flag the SIGNATURE even when the body
1122                // contains no SIMD-proposal operator.
1123                if unsupported.is_none()
1124                    && func_sig_has_v128
1125                        .get(func_index as usize)
1126                        .copied()
1127                        .unwrap_or(false)
1128                {
1129                    unsupported = Some(
1130                        "signature has a v128 param/result — no SIMD lowering \
1131                         for this target (#680)"
1132                            .to_string(),
1133                    );
1134                }
1135                let actual_index = num_imported_funcs + func_index;
1136                let export_name = export_names.get(&actual_index).cloned();
1137
1138                functions.push(FunctionOps {
1139                    index: actual_index,
1140                    export_name,
1141                    debug_name: None, // filled from the `name` section after the loop
1142                    ops,
1143                    op_offsets,
1144                    unsupported,
1145                    block_arity,
1146                });
1147                func_index += 1;
1148            }
1149            Payload::CustomSection(c) => {
1150                // #394 Tier-1.x: the wasm `name` custom section.
1151                if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
1152                    parse_name_section_func_names(reader, &mut name_section_names);
1153                }
1154                // VCR-PERF-002 Phase 1 (#494): loom's `wsc.facts` section.
1155                // `parse_wsc_facts` is TOTAL (fail-safe skew, loom#231 Q4):
1156                // any malformed payload decodes to the empty fact list WITH a
1157                // stderr diagnostic, never an error — facts are optional
1158                // accelerators and must not be able to change a compilation
1159                // outcome. First section wins.
1160                if c.name() == crate::wsc_facts::WSC_FACTS_SECTION_NAME && wsc_facts.is_none() {
1161                    let parsed = crate::wsc_facts::parse_wsc_facts(c.data());
1162                    if let Some(reason) = &parsed.section_ignored {
1163                        eprintln!(
1164                            "warning: ignoring unparseable `wsc.facts` custom section \
1165                             ({reason}) — facts are optional accelerators, compilation \
1166                             is unaffected (#494 fail-safe skew rule)"
1167                        );
1168                    } else if parsed.records_skipped > 0 {
1169                        eprintln!(
1170                            "warning: skipped {} unknown/undecodable `wsc.facts` \
1171                             record(s) (likely a newer loom emitter); {} known fact(s) \
1172                             kept, compilation is unaffected (#494 fail-safe skew rule)",
1173                            parsed.records_skipped,
1174                            parsed.facts.len()
1175                        );
1176                    }
1177                    wsc_facts = Some(parsed.facts);
1178                }
1179            }
1180            _ => {}
1181        }
1182    }
1183
1184    apply_name_section(&mut functions, &name_section_names);
1185
1186    Ok(DecodedModule {
1187        functions,
1188        memories,
1189        data_segments,
1190        imports,
1191        num_imported_funcs,
1192        func_arg_counts,
1193        type_arg_counts,
1194        func_ret_i64,
1195        type_ret_i64,
1196        func_params_i64,
1197        func_params_f32,
1198        func_params_f64,
1199        func_ret_f32,
1200        func_ret_f64,
1201        type_ret_f32,
1202        type_ret_f64,
1203        globals,
1204        elem_func_indices,
1205        table_size: table_sizes.first().copied().flatten(),
1206        table_sizes,
1207        elem_segments,
1208        func_type_indices,
1209        type_signatures,
1210        wsc_facts: wsc_facts.unwrap_or_default(),
1211    })
1212}
1213
1214/// Parse the function-names subsection of a wasm `name` custom section into
1215/// `out` (function index → developer-facing name, e.g.
1216/// `core::panicking::panic_fmt::h...`). Best-effort by design: the section is
1217/// DEBUG METADATA only, so a malformed entry is skipped rather than failing the
1218/// compile — no codegen path depends on it (#394 Tier-1.x).
1219fn parse_name_section_func_names(
1220    reader: wasmparser::NameSectionReader<'_>,
1221    out: &mut HashMap<u32, String>,
1222) {
1223    for subsection in reader.into_iter().flatten() {
1224        if let wasmparser::Name::Function(map) = subsection {
1225            for naming in map.into_iter().flatten() {
1226                out.insert(naming.index, naming.name.to_string());
1227            }
1228        }
1229    }
1230}
1231
1232/// Fill each function's `debug_name` from the `name`-section map (keyed by the
1233/// FULL function index, imports first — the same index space `FunctionOps.index`
1234/// uses). A function without an entry keeps `None` (⇒ `func_N` downstream).
1235fn apply_name_section(functions: &mut [FunctionOps], names: &HashMap<u32, String>) {
1236    if names.is_empty() {
1237        return;
1238    }
1239    for f in functions {
1240        f.debug_name = names.get(&f.index).cloned();
1241    }
1242}
1243
1244/// Decode a WASM binary and extract all function bodies as WasmOp sequences
1245pub fn decode_wasm_functions(wasm_bytes: &[u8]) -> Result<Vec<FunctionOps>> {
1246    let mut functions = Vec::new();
1247    let mut func_index = 0u32;
1248    let mut num_imported_funcs = 0u32;
1249    let mut export_names: HashMap<u32, String> = HashMap::new();
1250    let mut name_section_names: HashMap<u32, String> = HashMap::new();
1251    // #509: (param_count, result_count) per type index, for FuncType blocktypes.
1252    let mut type_block_arity: Vec<(u8, u8)> = Vec::new();
1253    // GI-FPU-001 (#369): float-typed globals (full index space, imports first)
1254    // whose accesses must loud-skip — see `decode_wasm_module`.
1255    let mut num_imported_globals = 0u32;
1256    let mut float_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
1257    // #680: v128-typed globals + v128 params/results — see `decode_wasm_module`.
1258    let mut v128_globals: std::collections::HashSet<u32> = std::collections::HashSet::new();
1259    let mut type_has_v128: Vec<bool> = Vec::new();
1260    let mut func_sig_has_v128: Vec<bool> = Vec::new();
1261
1262    for payload in Parser::new(0).parse_all(wasm_bytes) {
1263        let payload = payload.context("Failed to parse WASM payload")?;
1264
1265        match payload {
1266            Payload::TypeSection(reader) => {
1267                // #509: the blocktype-arity side-table needs the type section
1268                // for `BlockType::FuncType(i)` lookups (the wasm binary format
1269                // places types before code, so the table is complete before any
1270                // `CodeSectionEntry` is decoded).
1271                for rec_group in reader {
1272                    let rec_group = rec_group.context("Failed to parse type")?;
1273                    for sub_ty in rec_group.types() {
1274                        type_block_arity.push(match &sub_ty.composite_type.inner {
1275                            wasmparser::CompositeInnerType::Func(f) => (
1276                                u8::try_from(f.params().len()).unwrap_or(u8::MAX),
1277                                u8::try_from(f.results().len()).unwrap_or(u8::MAX),
1278                            ),
1279                            _ => (u8::MAX, u8::MAX),
1280                        });
1281                        // #680: v128 anywhere in the signature.
1282                        type_has_v128.push(match &sub_ty.composite_type.inner {
1283                            wasmparser::CompositeInnerType::Func(f) => f
1284                                .params()
1285                                .iter()
1286                                .chain(f.results())
1287                                .any(|t| *t == wasmparser::ValType::V128),
1288                            _ => false,
1289                        });
1290                    }
1291                }
1292            }
1293            Payload::ImportSection(imports) => {
1294                // wasmparser 0.221+ compact-imports grouping — flatten groups
1295                // to individual imports (see the ImportSection handler above).
1296                for import in imports.into_imports() {
1297                    let import = import.context("Failed to parse import")?;
1298                    match import.ty {
1299                        wasmparser::TypeRef::Func(_) => num_imported_funcs += 1,
1300                        wasmparser::TypeRef::Global(g) => {
1301                            // GI-FPU-001 (#369): see `decode_wasm_module` —
1302                            // float-typed global accesses must loud-skip.
1303                            if matches!(
1304                                g.content_type,
1305                                wasmparser::ValType::F32 | wasmparser::ValType::F64
1306                            ) {
1307                                float_globals.insert(num_imported_globals);
1308                            }
1309                            // #680: v128-typed imported globals — same lane.
1310                            if g.content_type == wasmparser::ValType::V128 {
1311                                v128_globals.insert(num_imported_globals);
1312                            }
1313                            num_imported_globals += 1;
1314                        }
1315                        _ => {}
1316                    }
1317                }
1318            }
1319            Payload::FunctionSection(reader) => {
1320                // #680: defined-function type indices, in order — the per-
1321                // function v128-signature flag (`decode_wasm_module` gets this
1322                // from its existing FunctionSection handling).
1323                for ty in reader {
1324                    let type_idx = ty.context("Failed to parse function type index")?;
1325                    func_sig_has_v128.push(
1326                        type_has_v128
1327                            .get(type_idx as usize)
1328                            .copied()
1329                            .unwrap_or(false),
1330                    );
1331                }
1332            }
1333            Payload::GlobalSection(reader) => {
1334                // GI-FPU-001 (#369): record f32/f64-typed defined globals so
1335                // `decode_function_body` flags accesses (their initializer is
1336                // dropped on this path too — same silent-zero hazard).
1337                for (idx, global) in reader.into_iter().enumerate() {
1338                    let global = global.context("Failed to parse global")?;
1339                    if matches!(
1340                        global.ty.content_type,
1341                        wasmparser::ValType::F32 | wasmparser::ValType::F64
1342                    ) {
1343                        float_globals.insert(num_imported_globals + idx as u32);
1344                    }
1345                    // #680: v128-typed defined globals — same lane.
1346                    if global.ty.content_type == wasmparser::ValType::V128 {
1347                        v128_globals.insert(num_imported_globals + idx as u32);
1348                    }
1349                }
1350            }
1351            Payload::ExportSection(exports) => {
1352                for export in exports {
1353                    let export = export.context("Failed to parse export")?;
1354                    if export.kind == ExternalKind::Func {
1355                        export_names.insert(export.index, export.name.to_string());
1356                    }
1357                }
1358            }
1359            Payload::CodeSectionEntry(body) => {
1360                let (ops, op_offsets, block_arity, mut unsupported) =
1361                    decode_function_body(&body, &type_block_arity, &float_globals, &v128_globals)?;
1362                // #680: v128 param/result — see `decode_wasm_module`.
1363                if unsupported.is_none()
1364                    && func_sig_has_v128
1365                        .get(func_index as usize)
1366                        .copied()
1367                        .unwrap_or(false)
1368                {
1369                    unsupported = Some(
1370                        "signature has a v128 param/result — no SIMD lowering \
1371                         for this target (#680)"
1372                            .to_string(),
1373                    );
1374                }
1375                let actual_index = num_imported_funcs + func_index;
1376                let export_name = export_names.get(&actual_index).cloned();
1377
1378                functions.push(FunctionOps {
1379                    index: actual_index,
1380                    export_name,
1381                    debug_name: None, // filled from the `name` section after the loop
1382                    ops,
1383                    op_offsets,
1384                    unsupported,
1385                    block_arity,
1386                });
1387                func_index += 1;
1388            }
1389            Payload::CustomSection(c) => {
1390                // #394 Tier-1.x: the wasm `name` custom section.
1391                if let wasmparser::KnownCustom::Name(reader) = c.as_known() {
1392                    parse_name_section_func_names(reader, &mut name_section_names);
1393                }
1394            }
1395            _ => {}
1396        }
1397    }
1398
1399    apply_name_section(&mut functions, &name_section_names);
1400
1401    Ok(functions)
1402}
1403
1404/// Decoded function with its WasmOp sequence
1405#[derive(Debug, Clone)]
1406pub struct FunctionOps {
1407    /// Function index in the module (includes imported functions)
1408    pub index: u32,
1409    /// Export name if this function is exported
1410    pub export_name: Option<String>,
1411    /// #394 Tier-1.x: the function's developer-facing name from the wasm `name`
1412    /// custom section (function-names subsection), e.g.
1413    /// `core::panicking::panic_fmt::h6651313c3e2c6c2f` — present for INTERNAL
1414    /// (non-exported) functions too, unlike `export_name`. DEBUG METADATA only:
1415    /// consumed by the `--debug-line` `DW_TAG_subprogram` emit (name priority:
1416    /// name-section > export name > `func_N`); no codegen or symbol-table path
1417    /// reads it, so emitted `.text`/`.symtab` are unchanged (frozen-safe).
1418    /// `None` when the module has no `name` section or no entry for this index.
1419    pub debug_name: Option<String>,
1420    /// The WASM operations in this function body
1421    pub ops: Vec<WasmOp>,
1422    /// VCR-DBG-001 step 1 (#394): module-relative wasm byte offset of each op in
1423    /// `ops` (same index → same op). This is the address space DWARF-for-wasm
1424    /// `.debug_line` keys on, so it is the bridge from synth's op-index
1425    /// `source_line` to the input wasm's DWARF (wasm-offset → source). PURELY
1426    /// ADDITIVE metadata: no codegen path reads it, so emitted `.text` is
1427    /// unchanged and the frozen fixtures stay bit-identical. Empty until consumed
1428    /// by the DWARF emitter (Tier 1).
1429    pub op_offsets: Vec<u32>,
1430    /// `Some(reason)` when the body contained a value-affecting operator the
1431    /// decoder cannot lower (e.g. scalar f32/f64 — #369, bulk-memory
1432    /// memory.copy/fill). Such an op would otherwise be silently *dropped*
1433    /// (`convert_operator` → `None`), leaving the operand stack wrong and the
1434    /// function a silent miscompile. The compile path LOUD-SKIPS a flagged
1435    /// function (diagnostic + symbol absent → link error names it) instead —
1436    /// the #180/#185 "unsupported op must Err, never silently continue"
1437    /// contract. `None` once every op decoded or was intentionally ignorable
1438    /// (Nop).
1439    pub unsupported: Option<String>,
1440    /// #509: blocktype arity side-table — `(param_count, result_count)` of the
1441    /// k-th `Block`/`Loop`/`If` op in `ops`, in order of appearance.
1442    /// ORDINAL-keyed, not op-index-keyed, on purpose: the backend may rewrite
1443    /// the op stream before selection (e.g. the #539 `i32.const 0; memory.grow`
1444    /// → `memory.size` fold), which shifts op indices but never adds/removes
1445    /// control ops, so the ordinal stays aligned. `BlockType::Empty → (0,0)`,
1446    /// `ValType → (0,1)`, `FuncType(i) →` counts from the type section
1447    /// (saturated to u8; an unresolvable type index records `(u8::MAX,
1448    /// u8::MAX)` so the selector declines loudly instead of miscompiling).
1449    /// This is what lets the direct selector land a value carried by
1450    /// `br`/`br_if`/`br_table` in the target block's designated result
1451    /// register instead of dropping it — `WasmOp::Block/Loop/If` stay bare
1452    /// unit variants (zero ripple through the backends' match sites), and an
1453    /// empty table (hand-built op streams in unit tests) keeps the legacy
1454    /// void-block lowering.
1455    pub block_arity: Vec<(u8, u8)>,
1456}
1457
1458/// #509: `(param_count, result_count)` of a wasm blocktype, for the
1459/// [`FunctionOps::block_arity`] side-table. `type_block_arity` is the type
1460/// section's per-type-index counts (needed for the `FuncType` form); a missing
1461/// entry saturates to `(u8::MAX, u8::MAX)` so downstream declines loudly.
1462fn blocktype_arity(bt: &wasmparser::BlockType, type_block_arity: &[(u8, u8)]) -> (u8, u8) {
1463    match bt {
1464        wasmparser::BlockType::Empty => (0, 0),
1465        wasmparser::BlockType::Type(_) => (0, 1),
1466        wasmparser::BlockType::FuncType(i) => type_block_arity
1467            .get(*i as usize)
1468            .copied()
1469            .unwrap_or((u8::MAX, u8::MAX)),
1470    }
1471}
1472
1473/// The per-function payload [`decode_function_body`] extracts: `(ops,
1474/// op_offsets, block_arity, unsupported)` — see the matching
1475/// [`FunctionOps`] fields for each component's contract.
1476type DecodedBody = (Vec<WasmOp>, Vec<u32>, Vec<(u8, u8)>, Option<String>);
1477
1478/// Decode a single function body to WasmOp sequence.
1479///
1480/// Returns the ops plus `Some(reason)` if any operator was a value-affecting
1481/// op the decoder cannot lower (so the function must be loud-skipped, #369 —
1482/// not silently miscompiled by dropping the op).
1483fn decode_function_body(
1484    body: &wasmparser::FunctionBody,
1485    type_block_arity: &[(u8, u8)],
1486    float_globals: &std::collections::HashSet<u32>,
1487    v128_globals: &std::collections::HashSet<u32>,
1488) -> Result<DecodedBody> {
1489    let mut ops = Vec::new();
1490    // VCR-DBG-001 step 1: parallel to `ops` — the module-relative wasm byte
1491    // offset of each emitted op (the DWARF-for-wasm address space). Captured via
1492    // the offset-aware reader; pushed only when an op is pushed, so indices stay
1493    // aligned with `ops`. Additive metadata, no codegen consumer ⇒ frozen-safe.
1494    let mut op_offsets = Vec::new();
1495    // #509: ordinal blocktype-arity side-table — one entry per Block/Loop/If in
1496    // `ops` order (see `FunctionOps::block_arity`).
1497    let mut block_arity: Vec<(u8, u8)> = Vec::new();
1498    let mut unsupported: Option<String> = None;
1499
1500    // #680: a v128-typed LOCAL is expressible with zero SIMD-proposal
1501    // operators (`local.get`/`local.set`/`local.tee` are type-agnostic), but
1502    // every selector lowers those as 4-byte (or 8-byte i64) register moves —
1503    // silently truncating the 16-byte value. Flag the declaration up front.
1504    for local in body.get_locals_reader()? {
1505        let (count, ty) = local.context("Failed to read local declaration")?;
1506        if unsupported.is_none() && count > 0 && ty == wasmparser::ValType::V128 {
1507            unsupported = Some(
1508                "declares a v128-typed local — no SIMD lowering for this \
1509                 target, accesses would silently truncate the 16-byte value \
1510                 (#680)"
1511                    .to_string(),
1512            );
1513        }
1514    }
1515
1516    let ops_reader = body.get_operators_reader()?;
1517    for item in ops_reader.into_iter_with_offsets() {
1518        let (op, offset) = item.context("Failed to read operator")?;
1519
1520        // #680: SIMD (v128) category-level honesty guard. Some SIMD ops decode
1521        // into `WasmOp` v128 variants that only a dead, never-wired Helium/MVE
1522        // prototype can select (`has_helium` is set by tests alone), so on
1523        // every real target they were silently dropped at selection —
1524        // `i32x4.add` compiled to an operand passthrough and `v128.store`
1525        // left memory unwritten. Catch the ENTIRE SIMD + relaxed-SIMD
1526        // proposal space here (macro-generated from wasmparser's operator
1527        // table — no hand-kept list to fall out of date) and route the
1528        // function through the same loud-skip/honest-bail lane as scalar
1529        // floats (GI-FPU-001). Targets with real SIMD hardware (Helium/MVE on
1530        // cortex-m55) can lift this once a lowering is actually wired.
1531        if unsupported.is_none() && is_simd_operator(&op) {
1532            unsupported = Some(format!(
1533                "{op:?}: no SIMD lowering for this target — the op would be \
1534                 silently dropped to a no-op (WASM SIMD proposal, #680)"
1535            ));
1536        }
1537
1538        if let Some(wasm_op) = convert_operator(&op) {
1539            // #509: capture the blocktype arity BEFORE the enum flattens it away
1540            // (`WasmOp::Block/Loop/If` are unit variants by design).
1541            if let wasmparser::Operator::Block { blockty }
1542            | wasmparser::Operator::Loop { blockty }
1543            | wasmparser::Operator::If { blockty } = &op
1544            {
1545                block_arity.push(blocktype_arity(blockty, type_block_arity));
1546            }
1547            // GI-FPU-001 (#369): `global.get`/`global.set` decode fine (the
1548            // ops are type-agnostic), but an f32/f64-typed global has no
1549            // float lowering — its const initializer is dropped (slot zeroed),
1550            // so a read returns a silently-wrong 0.0. Flag the function for
1551            // the same loud-skip the scalar float ops get.
1552            if unsupported.is_none()
1553                && let WasmOp::GlobalGet(i) | WasmOp::GlobalSet(i) = &wasm_op
1554                && float_globals.contains(i)
1555            {
1556                unsupported = Some(format!(
1557                    "{wasm_op:?} on an f32/f64-typed global — float globals \
1558                     have no lowering, the initializer would be silently \
1559                     zeroed (GI-FPU-001)"
1560                ));
1561            }
1562            // #680: same hazard for v128-typed globals — `global.get`/
1563            // `global.set` decode fine, but there is no SIMD lowering: the
1564            // access would move 4 of the 16 bytes and the `v128.const`
1565            // initializer is never captured (slot zeroed).
1566            if unsupported.is_none()
1567                && let WasmOp::GlobalGet(i) | WasmOp::GlobalSet(i) = &wasm_op
1568                && v128_globals.contains(i)
1569            {
1570                unsupported = Some(format!(
1571                    "{wasm_op:?} on a v128-typed global — no SIMD lowering \
1572                     for this target (#680)"
1573                ));
1574            }
1575            ops.push(wasm_op);
1576            op_offsets.push(offset as u32);
1577        } else if unsupported.is_none() && !is_intentionally_ignored(&op) {
1578            // The op was DROPPED by `convert_operator` (`_ => None`) and is not
1579            // an intentional no-op (Nop) — record it so the
1580            // function is loud-skipped rather than silently miscompiled (#369).
1581            unsupported = Some(format!("{op:?}"));
1582        }
1583    }
1584
1585    Ok((ops, op_offsets, block_arity, unsupported))
1586}
1587
1588/// Operators that `convert_operator` returns `None` for *on purpose* — they
1589/// carry no value-affecting semantics for our backend, so dropping them is
1590/// correct (NOT a silent miscompile). Everything else that decodes to `None`
1591/// is an unsupported op that must loud-skip its function (#369).
1592///
1593/// #665: `Unreachable` is NOT on this list — it traps (WASM §4.4.5), so it
1594/// decodes to `WasmOp::Unreachable` and every backend lowers it to a trap
1595/// instruction (or loud-declines). Only `Nop` is genuinely ignorable.
1596fn is_intentionally_ignored(op: &wasmparser::Operator) -> bool {
1597    use wasmparser::Operator::*;
1598    matches!(op, Nop)
1599}
1600
1601/// #680: is `op` from the WASM SIMD or relaxed-SIMD proposal?
1602///
1603/// CATEGORY-LEVEL by construction: the match is macro-generated from
1604/// `wasmparser::for_each_operator!`'s own proposal markers (`@simd` /
1605/// `@relaxed_simd`), so it covers the entire SIMD operator space of the
1606/// pinned wasmparser — there is no hand-kept op list that a new lane op,
1607/// load/store variant, or relaxed-SIMD instruction can silently fall out of.
1608/// Used to loud-skip functions with SIMD ops: no target has a SIMD lowering
1609/// wired today (the Helium/MVE selector arms are gated on a `has_helium`
1610/// flag only tests set), so a decoded v128 `WasmOp` was silently dropped at
1611/// selection — the #554-class miscompile this predicate closes.
1612fn is_simd_operator(op: &wasmparser::Operator) -> bool {
1613    macro_rules! define_match_operator {
1614        ($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => {
1615            match op {
1616                $(
1617                    wasmparser::Operator::$op { .. } => {
1618                        define_match_operator!(impl_one @$proposal)
1619                    }
1620                )*
1621                // `Operator` is non-exhaustive; an operator outside the
1622                // pinned wasmparser's own table cannot be produced by it.
1623                _ => false,
1624            }
1625        };
1626        (impl_one @simd) => { true };
1627        (impl_one @relaxed_simd) => { true };
1628        (impl_one @$proposal:ident) => { false };
1629    }
1630    wasmparser::for_each_operator!(define_match_operator)
1631}
1632
1633/// Convert a wasmparser Operator to our WasmOp enum
1634fn convert_operator(op: &wasmparser::Operator) -> Option<WasmOp> {
1635    use wasmparser::Operator::*;
1636
1637    match op {
1638        // Constants
1639        I32Const { value } => Some(WasmOp::I32Const(*value)),
1640
1641        // i32 Arithmetic
1642        I32Add => Some(WasmOp::I32Add),
1643        I32Sub => Some(WasmOp::I32Sub),
1644        I32Mul => Some(WasmOp::I32Mul),
1645        I32DivS => Some(WasmOp::I32DivS),
1646        I32DivU => Some(WasmOp::I32DivU),
1647        I32RemS => Some(WasmOp::I32RemS),
1648        I32RemU => Some(WasmOp::I32RemU),
1649
1650        // i64 Constants
1651        I64Const { value } => Some(WasmOp::I64Const(*value)),
1652
1653        // i64 Arithmetic
1654        I64Add => Some(WasmOp::I64Add),
1655        I64Sub => Some(WasmOp::I64Sub),
1656        I64Mul => Some(WasmOp::I64Mul),
1657        I64DivS => Some(WasmOp::I64DivS),
1658        I64DivU => Some(WasmOp::I64DivU),
1659        I64RemS => Some(WasmOp::I64RemS),
1660        I64RemU => Some(WasmOp::I64RemU),
1661
1662        // i64 Bitwise
1663        I64And => Some(WasmOp::I64And),
1664        I64Or => Some(WasmOp::I64Or),
1665        I64Xor => Some(WasmOp::I64Xor),
1666        I64Shl => Some(WasmOp::I64Shl),
1667        I64ShrS => Some(WasmOp::I64ShrS),
1668        I64ShrU => Some(WasmOp::I64ShrU),
1669        I64Rotl => Some(WasmOp::I64Rotl),
1670        I64Rotr => Some(WasmOp::I64Rotr),
1671        I64Clz => Some(WasmOp::I64Clz),
1672        I64Ctz => Some(WasmOp::I64Ctz),
1673        I64Popcnt => Some(WasmOp::I64Popcnt),
1674        I64Extend8S => Some(WasmOp::I64Extend8S),
1675        I64Extend16S => Some(WasmOp::I64Extend16S),
1676        I64Extend32S => Some(WasmOp::I64Extend32S),
1677        // i32<->i64 width conversions. Previously UNMAPPED → silently dropped,
1678        // which left an i32 value as a 64-bit operand with a garbage high half
1679        // (harmless when a following `i64.shl 32` discards it, but a latent
1680        // miscompile for extend-then-arithmetic, and it breaks width-correct
1681        // register allocation). (#204)
1682        I64ExtendI32U => Some(WasmOp::I64ExtendI32U),
1683        I64ExtendI32S => Some(WasmOp::I64ExtendI32S),
1684        I32WrapI64 => Some(WasmOp::I32WrapI64),
1685
1686        // i64 Comparison
1687        I64Eqz => Some(WasmOp::I64Eqz),
1688        I64Eq => Some(WasmOp::I64Eq),
1689        I64Ne => Some(WasmOp::I64Ne),
1690        I64LtS => Some(WasmOp::I64LtS),
1691        I64LtU => Some(WasmOp::I64LtU),
1692        I64LeS => Some(WasmOp::I64LeS),
1693        I64LeU => Some(WasmOp::I64LeU),
1694        I64GtS => Some(WasmOp::I64GtS),
1695        I64GtU => Some(WasmOp::I64GtU),
1696        I64GeS => Some(WasmOp::I64GeS),
1697        I64GeU => Some(WasmOp::I64GeU),
1698
1699        // Bitwise
1700        I32And => Some(WasmOp::I32And),
1701        I32Or => Some(WasmOp::I32Or),
1702        I32Xor => Some(WasmOp::I32Xor),
1703        I32Shl => Some(WasmOp::I32Shl),
1704        I32ShrS => Some(WasmOp::I32ShrS),
1705        I32ShrU => Some(WasmOp::I32ShrU),
1706        I32Rotl => Some(WasmOp::I32Rotl),
1707        I32Rotr => Some(WasmOp::I32Rotr),
1708        I32Clz => Some(WasmOp::I32Clz),
1709        I32Ctz => Some(WasmOp::I32Ctz),
1710        I32Popcnt => Some(WasmOp::I32Popcnt),
1711        I32Extend8S => Some(WasmOp::I32Extend8S),
1712        I32Extend16S => Some(WasmOp::I32Extend16S),
1713
1714        // Comparison
1715        I32Eqz => Some(WasmOp::I32Eqz),
1716        I32Eq => Some(WasmOp::I32Eq),
1717        I32Ne => Some(WasmOp::I32Ne),
1718        I32LtS => Some(WasmOp::I32LtS),
1719        I32LtU => Some(WasmOp::I32LtU),
1720        I32LeS => Some(WasmOp::I32LeS),
1721        I32LeU => Some(WasmOp::I32LeU),
1722        I32GtS => Some(WasmOp::I32GtS),
1723        I32GtU => Some(WasmOp::I32GtU),
1724        I32GeS => Some(WasmOp::I32GeS),
1725        I32GeU => Some(WasmOp::I32GeU),
1726
1727        // Memory
1728        I32Load { memarg } => Some(WasmOp::I32Load {
1729            offset: memarg.offset as u32,
1730            align: memarg.align as u32,
1731        }),
1732        I32Store { memarg } => Some(WasmOp::I32Store {
1733            offset: memarg.offset as u32,
1734            align: memarg.align as u32,
1735        }),
1736        // #372: full-width i64 load/store. The selector already lowers these to
1737        // a lo/hi i32 register-pair access (`generate_i64_load/store_with_bounds_check`,
1738        // reusing the #171 pair regalloc) — only the decoder arm was missing, so
1739        // `i64.load`/`i64.store` fell through `_ => None` and (since v0.11.46)
1740        // loud-skipped their function. The narrow forms (I64Load8.. / I64Store32)
1741        // were already decoded below.
1742        I64Load { memarg } => Some(WasmOp::I64Load {
1743            offset: memarg.offset as u32,
1744            align: memarg.align as u32,
1745        }),
1746        I64Store { memarg } => Some(WasmOp::I64Store {
1747            offset: memarg.offset as u32,
1748            align: memarg.align as u32,
1749        }),
1750
1751        // Sub-word loads (i32)
1752        I32Load8S { memarg } => Some(WasmOp::I32Load8S {
1753            offset: memarg.offset as u32,
1754            align: memarg.align as u32,
1755        }),
1756        I32Load8U { memarg } => Some(WasmOp::I32Load8U {
1757            offset: memarg.offset as u32,
1758            align: memarg.align as u32,
1759        }),
1760        I32Load16S { memarg } => Some(WasmOp::I32Load16S {
1761            offset: memarg.offset as u32,
1762            align: memarg.align as u32,
1763        }),
1764        I32Load16U { memarg } => Some(WasmOp::I32Load16U {
1765            offset: memarg.offset as u32,
1766            align: memarg.align as u32,
1767        }),
1768
1769        // Sub-word stores (i32)
1770        I32Store8 { memarg } => Some(WasmOp::I32Store8 {
1771            offset: memarg.offset as u32,
1772            align: memarg.align as u32,
1773        }),
1774        I32Store16 { memarg } => Some(WasmOp::I32Store16 {
1775            offset: memarg.offset as u32,
1776            align: memarg.align as u32,
1777        }),
1778
1779        // Local/Global
1780        LocalGet { local_index } => Some(WasmOp::LocalGet(*local_index)),
1781        LocalSet { local_index } => Some(WasmOp::LocalSet(*local_index)),
1782        LocalTee { local_index } => Some(WasmOp::LocalTee(*local_index)),
1783        GlobalGet { global_index } => Some(WasmOp::GlobalGet(*global_index)),
1784        GlobalSet { global_index } => Some(WasmOp::GlobalSet(*global_index)),
1785
1786        // Control flow
1787        Block { .. } => Some(WasmOp::Block),
1788        Loop { .. } => Some(WasmOp::Loop),
1789        Br { relative_depth } => Some(WasmOp::Br(*relative_depth)),
1790        BrIf { relative_depth } => Some(WasmOp::BrIf(*relative_depth)),
1791        // br_table: indexed multi-way branch. Previously UNMAPPED → silently
1792        // dropped, so the selector never emitted the index dispatch and control
1793        // fell straight into the first table arm — every br_table behaved as if
1794        // it always took target 0 (gale's binary-sem WAKE path never fired). The
1795        // jump-table relative depths + default depth are preserved in order.
1796        BrTable { targets } => {
1797            let default = targets.default();
1798            let tgts: Vec<u32> = targets.targets().filter_map(Result::ok).collect();
1799            Some(WasmOp::BrTable {
1800                targets: tgts,
1801                default,
1802            })
1803        }
1804        Return => Some(WasmOp::Return),
1805        Call { function_index } => Some(WasmOp::Call(*function_index)),
1806        CallIndirect {
1807            type_index,
1808            table_index,
1809            ..
1810        } => Some(WasmOp::CallIndirect {
1811            type_index: *type_index,
1812            table_index: *table_index,
1813        }),
1814
1815        // End is needed for control flow pattern matching
1816        End => Some(WasmOp::End),
1817
1818        // #665: `unreachable` MUST reach the backends — WASM Core §4.4.5
1819        // requires it to trap unconditionally. It was previously dropped here
1820        // (treated like Nop), so every backend compiled it to a no-op and
1821        // control FELL THROUGH panic!/abort/unreachable-default guards with
1822        // undefined register state. The selector arms (ARM: UDF #0, RV32:
1823        // ebreak) already existed; they just never received the op.
1824        Unreachable => Some(WasmOp::Unreachable),
1825
1826        // Nop - skip (genuinely no semantics)
1827        Nop => None,
1828
1829        // Drop is needed for br_if pattern matching
1830        Drop => Some(WasmOp::Drop),
1831
1832        // Select
1833        Select => Some(WasmOp::Select),
1834
1835        // If/Else - simplified handling
1836        If { .. } => Some(WasmOp::If),
1837        Else => Some(WasmOp::Else),
1838
1839        // i64 sub-word loads
1840        I64Load8S { memarg } => Some(WasmOp::I64Load8S {
1841            offset: memarg.offset as u32,
1842            align: memarg.align as u32,
1843        }),
1844        I64Load8U { memarg } => Some(WasmOp::I64Load8U {
1845            offset: memarg.offset as u32,
1846            align: memarg.align as u32,
1847        }),
1848        I64Load16S { memarg } => Some(WasmOp::I64Load16S {
1849            offset: memarg.offset as u32,
1850            align: memarg.align as u32,
1851        }),
1852        I64Load16U { memarg } => Some(WasmOp::I64Load16U {
1853            offset: memarg.offset as u32,
1854            align: memarg.align as u32,
1855        }),
1856        I64Load32S { memarg } => Some(WasmOp::I64Load32S {
1857            offset: memarg.offset as u32,
1858            align: memarg.align as u32,
1859        }),
1860        I64Load32U { memarg } => Some(WasmOp::I64Load32U {
1861            offset: memarg.offset as u32,
1862            align: memarg.align as u32,
1863        }),
1864
1865        // i64 sub-word stores
1866        I64Store8 { memarg } => Some(WasmOp::I64Store8 {
1867            offset: memarg.offset as u32,
1868            align: memarg.align as u32,
1869        }),
1870        I64Store16 { memarg } => Some(WasmOp::I64Store16 {
1871            offset: memarg.offset as u32,
1872            align: memarg.align as u32,
1873        }),
1874        I64Store32 { memarg } => Some(WasmOp::I64Store32 {
1875            offset: memarg.offset as u32,
1876            align: memarg.align as u32,
1877        }),
1878
1879        // Memory management
1880        MemorySize { mem, .. } => Some(WasmOp::MemorySize(*mem)),
1881        MemoryGrow { mem, .. } => Some(WasmOp::MemoryGrow(*mem)),
1882
1883        // Bulk memory (#374). The backend supports a single linear memory
1884        // (memory 0); any non-zero memory index falls through to `_ => None` and
1885        // loud-skips the function (GI-FPU-001 honesty contract) rather than
1886        // miscompiling a multi-memory copy. memory.copy reads dst/src memories;
1887        // memory.fill one. The selector lowers these to a bounds-checked byte
1888        // loop (see select_with_stack).
1889        MemoryCopy {
1890            dst_mem: 0,
1891            src_mem: 0,
1892        } => Some(WasmOp::MemoryCopy),
1893        MemoryFill { mem: 0 } => Some(WasmOp::MemoryFill),
1894
1895        // ========================================================================
1896        // v128 SIMD operations (WASM SIMD proposal, 0xFD prefix)
1897        // ========================================================================
1898        V128Const { value } => {
1899            let mut bytes = [0u8; 16];
1900            bytes.copy_from_slice(value.bytes());
1901            Some(WasmOp::V128Const(bytes))
1902        }
1903        V128Load { memarg } => Some(WasmOp::V128Load {
1904            offset: memarg.offset as u32,
1905            align: memarg.align as u32,
1906        }),
1907        V128Store { memarg } => Some(WasmOp::V128Store {
1908            offset: memarg.offset as u32,
1909            align: memarg.align as u32,
1910        }),
1911
1912        // v128 bitwise
1913        V128And => Some(WasmOp::V128And),
1914        V128Or => Some(WasmOp::V128Or),
1915        V128Xor => Some(WasmOp::V128Xor),
1916        V128Not => Some(WasmOp::V128Not),
1917        V128AndNot => Some(WasmOp::V128AndNot),
1918
1919        // i8x16
1920        I8x16Add => Some(WasmOp::I8x16Add),
1921        I8x16Sub => Some(WasmOp::I8x16Sub),
1922        I8x16Neg => Some(WasmOp::I8x16Neg),
1923        I8x16Eq => Some(WasmOp::I8x16Eq),
1924        I8x16Ne => Some(WasmOp::I8x16Ne),
1925        I8x16LtS => Some(WasmOp::I8x16LtS),
1926        I8x16LtU => Some(WasmOp::I8x16LtU),
1927        I8x16GtS => Some(WasmOp::I8x16GtS),
1928        I8x16GtU => Some(WasmOp::I8x16GtU),
1929        I8x16LeS => Some(WasmOp::I8x16LeS),
1930        I8x16LeU => Some(WasmOp::I8x16LeU),
1931        I8x16GeS => Some(WasmOp::I8x16GeS),
1932        I8x16GeU => Some(WasmOp::I8x16GeU),
1933        I8x16Splat => Some(WasmOp::I8x16Splat),
1934        I8x16ExtractLaneS { lane } => Some(WasmOp::I8x16ExtractLaneS(*lane)),
1935        I8x16ExtractLaneU { lane } => Some(WasmOp::I8x16ExtractLaneU(*lane)),
1936        I8x16ReplaceLane { lane } => Some(WasmOp::I8x16ReplaceLane(*lane)),
1937        I8x16Shuffle { lanes } => Some(WasmOp::I8x16Shuffle(*lanes)),
1938        I8x16Swizzle => Some(WasmOp::I8x16Swizzle),
1939
1940        // i16x8
1941        I16x8Add => Some(WasmOp::I16x8Add),
1942        I16x8Sub => Some(WasmOp::I16x8Sub),
1943        I16x8Mul => Some(WasmOp::I16x8Mul),
1944        I16x8Neg => Some(WasmOp::I16x8Neg),
1945        I16x8Eq => Some(WasmOp::I16x8Eq),
1946        I16x8Ne => Some(WasmOp::I16x8Ne),
1947        I16x8LtS => Some(WasmOp::I16x8LtS),
1948        I16x8LtU => Some(WasmOp::I16x8LtU),
1949        I16x8GtS => Some(WasmOp::I16x8GtS),
1950        I16x8GtU => Some(WasmOp::I16x8GtU),
1951        I16x8LeS => Some(WasmOp::I16x8LeS),
1952        I16x8LeU => Some(WasmOp::I16x8LeU),
1953        I16x8GeS => Some(WasmOp::I16x8GeS),
1954        I16x8GeU => Some(WasmOp::I16x8GeU),
1955        I16x8Splat => Some(WasmOp::I16x8Splat),
1956        I16x8ExtractLaneS { lane } => Some(WasmOp::I16x8ExtractLaneS(*lane)),
1957        I16x8ExtractLaneU { lane } => Some(WasmOp::I16x8ExtractLaneU(*lane)),
1958        I16x8ReplaceLane { lane } => Some(WasmOp::I16x8ReplaceLane(*lane)),
1959
1960        // i32x4
1961        I32x4Add => Some(WasmOp::I32x4Add),
1962        I32x4Sub => Some(WasmOp::I32x4Sub),
1963        I32x4Mul => Some(WasmOp::I32x4Mul),
1964        I32x4Neg => Some(WasmOp::I32x4Neg),
1965        I32x4Eq => Some(WasmOp::I32x4Eq),
1966        I32x4Ne => Some(WasmOp::I32x4Ne),
1967        I32x4LtS => Some(WasmOp::I32x4LtS),
1968        I32x4LtU => Some(WasmOp::I32x4LtU),
1969        I32x4GtS => Some(WasmOp::I32x4GtS),
1970        I32x4GtU => Some(WasmOp::I32x4GtU),
1971        I32x4LeS => Some(WasmOp::I32x4LeS),
1972        I32x4LeU => Some(WasmOp::I32x4LeU),
1973        I32x4GeS => Some(WasmOp::I32x4GeS),
1974        I32x4GeU => Some(WasmOp::I32x4GeU),
1975        I32x4Splat => Some(WasmOp::I32x4Splat),
1976        I32x4ExtractLane { lane } => Some(WasmOp::I32x4ExtractLane(*lane)),
1977        I32x4ReplaceLane { lane } => Some(WasmOp::I32x4ReplaceLane(*lane)),
1978
1979        // i64x2
1980        I64x2Add => Some(WasmOp::I64x2Add),
1981        I64x2Sub => Some(WasmOp::I64x2Sub),
1982        I64x2Mul => Some(WasmOp::I64x2Mul),
1983        I64x2Neg => Some(WasmOp::I64x2Neg),
1984        I64x2Eq => Some(WasmOp::I64x2Eq),
1985        I64x2Ne => Some(WasmOp::I64x2Ne),
1986        I64x2LtS => Some(WasmOp::I64x2LtS),
1987        I64x2GtS => Some(WasmOp::I64x2GtS),
1988        I64x2LeS => Some(WasmOp::I64x2LeS),
1989        I64x2GeS => Some(WasmOp::I64x2GeS),
1990        I64x2Splat => Some(WasmOp::I64x2Splat),
1991        I64x2ExtractLane { lane } => Some(WasmOp::I64x2ExtractLane(*lane)),
1992        I64x2ReplaceLane { lane } => Some(WasmOp::I64x2ReplaceLane(*lane)),
1993
1994        // === Scalar f32 (GI-FPU-002 phase 1, #619/#369) ===
1995        // Un-dropped so the FPU targets (cortex-m4f/m7/m7dp) can route these to
1996        // the VFP selector arms in `select_with_stack`. The FPU gate lives at
1997        // the selector/validate layer (`requires_fpu()` + `set_target`): on a
1998        // non-FPU target (m0/m3/r5) these still honest-reject. f64 stays dropped
1999        // (phase 2 — M7DP D-registers). Only the wired scope is un-dropped; the
2000        // rest of the scalar f32 surface (abs/neg/sqrt/min/max/…) still falls to
2001        // `_ => None` and loud-skips its function until phase 1b wires it.
2002        F32Add => Some(WasmOp::F32Add),
2003        F32Sub => Some(WasmOp::F32Sub),
2004        F32Mul => Some(WasmOp::F32Mul),
2005        F32Div => Some(WasmOp::F32Div),
2006        F32Eq => Some(WasmOp::F32Eq),
2007        F32Ne => Some(WasmOp::F32Ne),
2008        F32Lt => Some(WasmOp::F32Lt),
2009        F32Le => Some(WasmOp::F32Le),
2010        F32Gt => Some(WasmOp::F32Gt),
2011        F32Ge => Some(WasmOp::F32Ge),
2012        F32Const { value } => Some(WasmOp::F32Const(f32::from_bits(value.bits()))),
2013        // #708 (phase 1b): `f32.load` un-dropped. The selector lowers it as the
2014        // proven `i32.load` address sequence (`[R11,idx]`→absolute-base rewrite +
2015        // bounds guard) into a core register, then a bit-exact `VMOV Sd,Rd`
2016        // (reinterpret) — a VLDR loads the same 4 bytes, so the bit pattern is
2017        // identical.
2018        F32Load { memarg } => Some(WasmOp::F32Load {
2019            offset: memarg.offset as u32,
2020            align: memarg.align as u32,
2021        }),
2022        // #719 (phase 1b): `f32.store` — the VFP-store twin of `f32.load`. The
2023        // selector moves the S-register value into a core register (`VMOV Rn,Sn`,
2024        // a reinterpret) and reuses the PROVEN `i32.store` address path; a VSTR
2025        // would write the same 4 bytes, so the stored word is bit-exact. (falcon
2026        // has 10 f32.store functions, #719.)
2027        F32Store { memarg } => Some(WasmOp::F32Store {
2028            offset: memarg.offset as u32,
2029            align: memarg.align as u32,
2030        }),
2031        // #719 (phase 1b): scalar f32 sign-family math — `VABS.F32` / `VNEG.F32`
2032        // and the `copysign` sign-bit splice. Pure single-precision VFP, no
2033        // numeric approximation; bit-exact across ±0.0 / NaN-sign / ±inf.
2034        F32Abs => Some(WasmOp::F32Abs),
2035        F32Neg => Some(WasmOp::F32Neg),
2036        F32Copysign => Some(WasmOp::F32Copysign),
2037        // #708 (phase 1b): the f32<->i32 bit-casts. Pure `VMOV` between a core
2038        // register and a single-precision S-register — no numeric conversion.
2039        F32ReinterpretI32 => Some(WasmOp::F32ReinterpretI32),
2040        I32ReinterpretF32 => Some(WasmOp::I32ReinterpretF32),
2041        F32ConvertI32S => Some(WasmOp::F32ConvertI32S),
2042        F32ConvertI32U => Some(WasmOp::F32ConvertI32U),
2043        I32TruncF32S => Some(WasmOp::I32TruncF32S),
2044        I32TruncF32U => Some(WasmOp::I32TruncF32U),
2045
2046        // === Scalar f64 (GI-FPU-002 phase 2, #369) ===
2047        // Un-dropped for the DOUBLE-precision FPU target (cortex-m7dp D0..D15);
2048        // the capability gate lives in `select_with_stack`'s preamble — any
2049        // f64 op on m4f/m7 (single-precision) or m0/m3/r5 (no FPU) still
2050        // honest-rejects its function. Exactly this set is lowered by
2051        // `try_lower_f64` + the `F64Load`/`F64Store` selector arms; the rest
2052        // of the f64 surface (min/max/copysign/rounding/i64<->f64/…) stays at
2053        // `_ => None` (loud-skip) until a later increment wires it.
2054        F64Const { value } => Some(WasmOp::F64Const(f64::from_bits(value.bits()))),
2055        F64PromoteF32 => Some(WasmOp::F64PromoteF32),
2056        F64Add => Some(WasmOp::F64Add),
2057        F64Sub => Some(WasmOp::F64Sub),
2058        F64Mul => Some(WasmOp::F64Mul),
2059        F64Div => Some(WasmOp::F64Div),
2060        F64Abs => Some(WasmOp::F64Abs),
2061        F64Neg => Some(WasmOp::F64Neg),
2062        F64Sqrt => Some(WasmOp::F64Sqrt),
2063        F64Eq => Some(WasmOp::F64Eq),
2064        F64Ne => Some(WasmOp::F64Ne),
2065        F64Lt => Some(WasmOp::F64Lt),
2066        F64Le => Some(WasmOp::F64Le),
2067        F64Gt => Some(WasmOp::F64Gt),
2068        F64Ge => Some(WasmOp::F64Ge),
2069        F64Load { memarg } => Some(WasmOp::F64Load {
2070            offset: memarg.offset as u32,
2071            align: memarg.align as u32,
2072        }),
2073        F64Store { memarg } => Some(WasmOp::F64Store {
2074            offset: memarg.offset as u32,
2075            align: memarg.align as u32,
2076        }),
2077
2078        // f32x4
2079        F32x4Add => Some(WasmOp::F32x4Add),
2080        F32x4Sub => Some(WasmOp::F32x4Sub),
2081        F32x4Mul => Some(WasmOp::F32x4Mul),
2082        F32x4Div => Some(WasmOp::F32x4Div),
2083        F32x4Abs => Some(WasmOp::F32x4Abs),
2084        F32x4Neg => Some(WasmOp::F32x4Neg),
2085        F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
2086        F32x4Eq => Some(WasmOp::F32x4Eq),
2087        F32x4Ne => Some(WasmOp::F32x4Ne),
2088        F32x4Lt => Some(WasmOp::F32x4Lt),
2089        F32x4Le => Some(WasmOp::F32x4Le),
2090        F32x4Gt => Some(WasmOp::F32x4Gt),
2091        F32x4Ge => Some(WasmOp::F32x4Ge),
2092        F32x4Splat => Some(WasmOp::F32x4Splat),
2093        F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
2094        F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
2095
2096        // Other operators not yet supported
2097        _ => None,
2098    }
2099}
2100
2101#[cfg(test)]
2102mod tests {
2103    use super::*;
2104
2105    #[test]
2106    fn test_decode_simple_add() {
2107        let wat = r#"
2108            (module
2109                (func (export "add") (param i32 i32) (result i32)
2110                    local.get 0
2111                    local.get 1
2112                    i32.add
2113                )
2114            )
2115        "#;
2116
2117        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2118        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2119
2120        assert_eq!(functions.len(), 1);
2121        assert_eq!(functions[0].index, 0);
2122        assert_eq!(functions[0].export_name, Some("add".to_string()));
2123        assert_eq!(
2124            functions[0].ops,
2125            vec![
2126                WasmOp::LocalGet(0),
2127                WasmOp::LocalGet(1),
2128                WasmOp::I32Add,
2129                WasmOp::End
2130            ]
2131        );
2132    }
2133
2134    /// #204 regression: `i64.extend_i32_u`, `i64.extend_i32_s` and
2135    /// `i32.wrap_i64` must DECODE (they were previously unmapped → silently
2136    /// dropped by `convert_operator`, leaving an i32 value as a 64-bit operand
2137    /// with a garbage high half — the root cause of gale's miscompiled
2138    /// `(new_count << 32)` pack). The decoder must surface all three.
2139    #[test]
2140    fn test_decode_i64_i32_width_conversions() {
2141        let wat = r#"
2142            (module
2143                (func (export "conv") (param i32 i64) (result i32)
2144                    local.get 0
2145                    i64.extend_i32_u
2146                    local.get 0
2147                    i64.extend_i32_s
2148                    i64.add
2149                    local.get 1
2150                    i64.add
2151                    i32.wrap_i64
2152                )
2153            )
2154        "#;
2155        let wasm = wat::parse_str(wat).expect("parse");
2156        let functions = decode_wasm_functions(&wasm).expect("decode");
2157        let ops = &functions[0].ops;
2158        assert!(
2159            ops.contains(&WasmOp::I64ExtendI32U),
2160            "i64.extend_i32_u must decode (not be dropped): {ops:?}"
2161        );
2162        assert!(
2163            ops.contains(&WasmOp::I64ExtendI32S),
2164            "i64.extend_i32_s must decode (not be dropped): {ops:?}"
2165        );
2166        assert!(
2167            ops.contains(&WasmOp::I32WrapI64),
2168            "i32.wrap_i64 must decode (not be dropped): {ops:?}"
2169        );
2170    }
2171
2172    /// #204 WAKE-path regression: `br_table` must DECODE (it was unmapped in
2173    /// `convert_operator` → silently dropped, so the selector emitted no index
2174    /// dispatch and every `br_table` fell through to target 0 — gale's binary
2175    /// semaphore never took its WAKE branch). Targets + default are preserved.
2176    #[test]
2177    fn test_decode_br_table() {
2178        let wat = r#"
2179            (module
2180                (func (export "bt") (param i32) (result i32)
2181                    (block (block (block
2182                        local.get 0
2183                        br_table 2 0 1 2)
2184                      i32.const 30 return)
2185                      i32.const 20 return)
2186                    i32.const 10))
2187        "#;
2188        let wasm = wat::parse_str(wat).expect("parse");
2189        let functions = decode_wasm_functions(&wasm).expect("decode");
2190        let bt = functions[0]
2191            .ops
2192            .iter()
2193            .find_map(|o| match o {
2194                WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
2195                _ => None,
2196            })
2197            .expect("br_table must decode (not be dropped)");
2198        assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
2199        assert_eq!(bt.1, 2, "br_table default preserved");
2200    }
2201
2202    #[test]
2203    fn test_decode_arithmetic() {
2204        let wat = r#"
2205            (module
2206                (func (export "calc") (result i32)
2207                    i32.const 5
2208                    i32.const 3
2209                    i32.mul
2210                    i32.const 2
2211                    i32.add
2212                )
2213            )
2214        "#;
2215
2216        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2217        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2218
2219        assert_eq!(functions.len(), 1);
2220        assert_eq!(functions[0].export_name, Some("calc".to_string()));
2221        assert_eq!(
2222            functions[0].ops,
2223            vec![
2224                WasmOp::I32Const(5),
2225                WasmOp::I32Const(3),
2226                WasmOp::I32Mul,
2227                WasmOp::I32Const(2),
2228                WasmOp::I32Add,
2229                WasmOp::End,
2230            ]
2231        );
2232    }
2233
2234    #[test]
2235    fn test_decode_multi_function_module() {
2236        let wat = r#"
2237            (module
2238                (func $helper)
2239                (func (export "add") (param i32 i32) (result i32)
2240                    local.get 0
2241                    local.get 1
2242                    i32.add
2243                )
2244                (func (export "sub") (param i32 i32) (result i32)
2245                    local.get 0
2246                    local.get 1
2247                    i32.sub
2248                )
2249            )
2250        "#;
2251
2252        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2253        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2254
2255        assert_eq!(functions.len(), 3);
2256        assert_eq!(functions[0].index, 0);
2257        assert_eq!(functions[0].export_name, None);
2258        assert_eq!(functions[1].index, 1);
2259        assert_eq!(functions[1].export_name, Some("add".to_string()));
2260        assert_eq!(functions[2].index, 2);
2261        assert_eq!(functions[2].export_name, Some("sub".to_string()));
2262    }
2263
2264    #[test]
2265    fn test_decode_module_with_imports() {
2266        let wat = r#"
2267            (module
2268                (import "env" "log" (func $log (param i32)))
2269                (import "env" "memory" (memory 1))
2270                (func (export "run") (param i32)
2271                    local.get 0
2272                    call 0
2273                )
2274            )
2275        "#;
2276
2277        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2278        let module = decode_wasm_module(&wasm).expect("Failed to decode");
2279
2280        // Should have 2 imports (1 func, 1 memory)
2281        assert_eq!(module.imports.len(), 2);
2282        assert_eq!(module.num_imported_funcs, 1);
2283
2284        // First import is the function
2285        assert_eq!(module.imports[0].module, "env");
2286        assert_eq!(module.imports[0].name, "log");
2287        assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
2288
2289        // Second import is memory
2290        assert_eq!(module.imports[1].module, "env");
2291        assert_eq!(module.imports[1].name, "memory");
2292        assert_eq!(module.imports[1].kind, ImportKind::Memory);
2293
2294        // Should have 1 local function (index 1, because import is index 0)
2295        assert_eq!(module.functions.len(), 1);
2296        assert_eq!(module.functions[0].index, 1);
2297        assert_eq!(module.functions[0].export_name, Some("run".to_string()));
2298    }
2299
2300    #[test]
2301    fn test_find_function_by_export_name() {
2302        let wat = r#"
2303            (module
2304                (func $helper)
2305                (func (export "add") (param i32 i32) (result i32)
2306                    local.get 0
2307                    local.get 1
2308                    i32.add
2309                )
2310            )
2311        "#;
2312
2313        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2314        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2315
2316        let add_func = functions
2317            .iter()
2318            .find(|f| f.export_name.as_deref() == Some("add"))
2319            .expect("Should find 'add' function");
2320
2321        assert_eq!(add_func.index, 1);
2322        assert!(add_func.ops.contains(&WasmOp::I32Add));
2323    }
2324
2325    #[test]
2326    fn test_decode_subword_loads() {
2327        let wat = r#"
2328            (module
2329                (memory 1)
2330                (func (export "test") (param i32) (result i32)
2331                    local.get 0
2332                    i32.load8_u
2333                )
2334            )
2335        "#;
2336
2337        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2338        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2339
2340        assert_eq!(functions.len(), 1);
2341        assert!(functions[0].ops.contains(&WasmOp::I32Load8U {
2342            offset: 0,
2343            align: 0,
2344        }));
2345    }
2346
2347    #[test]
2348    fn test_decode_subword_stores() {
2349        let wat = r#"
2350            (module
2351                (memory 1)
2352                (func (export "test") (param i32 i32)
2353                    local.get 0
2354                    local.get 1
2355                    i32.store8
2356                )
2357            )
2358        "#;
2359
2360        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2361        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2362
2363        assert_eq!(functions.len(), 1);
2364        assert!(functions[0].ops.contains(&WasmOp::I32Store8 {
2365            offset: 0,
2366            align: 0,
2367        }));
2368    }
2369
2370    #[test]
2371    fn test_decode_memory_size_grow() {
2372        let wat = r#"
2373            (module
2374                (memory 1)
2375                (func (export "test") (result i32)
2376                    memory.size
2377                )
2378            )
2379        "#;
2380
2381        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2382        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2383
2384        assert_eq!(functions.len(), 1);
2385        assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
2386    }
2387
2388    #[test]
2389    fn test_decode_memory_grow() {
2390        let wat = r#"
2391            (module
2392                (memory 1)
2393                (func (export "test") (param i32) (result i32)
2394                    local.get 0
2395                    memory.grow
2396                )
2397            )
2398        "#;
2399
2400        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2401        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2402
2403        assert_eq!(functions.len(), 1);
2404        assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
2405    }
2406
2407    #[test]
2408    fn test_decode_bulk_memory_374() {
2409        // #374: memory.copy / memory.fill on the single linear memory decode to
2410        // the new WasmOp variants (was `_ => None` -> loud-skip).
2411        let wat = r#"
2412            (module
2413                (memory 1)
2414                (func (export "cpy") (param i32 i32 i32)
2415                    local.get 0 local.get 1 local.get 2 memory.copy)
2416                (func (export "fil") (param i32 i32 i32)
2417                    local.get 0 local.get 1 local.get 2 memory.fill)
2418            )
2419        "#;
2420        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2421        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2422        assert_eq!(functions.len(), 2);
2423        assert!(functions[0].ops.contains(&WasmOp::MemoryCopy));
2424        assert!(functions[1].ops.contains(&WasmOp::MemoryFill));
2425        // Neither function is flagged unsupported (they now lower).
2426        assert!(functions[0].unsupported.is_none());
2427        assert!(functions[1].unsupported.is_none());
2428    }
2429
2430    #[test]
2431    fn test_decode_i64_subword_loads() {
2432        let wat = r#"
2433            (module
2434                (memory 1)
2435                (func (export "test") (param i32) (result i64)
2436                    local.get 0
2437                    i64.load8_s
2438                )
2439            )
2440        "#;
2441
2442        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2443        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2444
2445        assert_eq!(functions.len(), 1);
2446        assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
2447            offset: 0,
2448            align: 0,
2449        }));
2450    }
2451
2452    #[test]
2453    fn test_decode_all_subword_memory_ops() {
2454        // Test that all sub-word operations are decoded from WAT
2455        let wat = r#"
2456            (module
2457                (memory 1)
2458                (func (export "test") (param i32)
2459                    ;; i32 sub-word loads
2460                    local.get 0
2461                    i32.load8_s
2462                    drop
2463                    local.get 0
2464                    i32.load8_u
2465                    drop
2466                    local.get 0
2467                    i32.load16_s
2468                    drop
2469                    local.get 0
2470                    i32.load16_u
2471                    drop
2472
2473                    ;; i32 sub-word stores
2474                    local.get 0
2475                    i32.const 42
2476                    i32.store8
2477                    local.get 0
2478                    i32.const 42
2479                    i32.store16
2480
2481                    ;; i64 sub-word loads
2482                    local.get 0
2483                    i64.load8_s
2484                    drop
2485                    local.get 0
2486                    i64.load8_u
2487                    drop
2488                    local.get 0
2489                    i64.load16_s
2490                    drop
2491                    local.get 0
2492                    i64.load16_u
2493                    drop
2494                    local.get 0
2495                    i64.load32_s
2496                    drop
2497                    local.get 0
2498                    i64.load32_u
2499                    drop
2500
2501                    ;; i64 sub-word stores
2502                    local.get 0
2503                    i64.const 42
2504                    i64.store8
2505                    local.get 0
2506                    i64.const 42
2507                    i64.store16
2508                    local.get 0
2509                    i64.const 42
2510                    i64.store32
2511                )
2512            )
2513        "#;
2514
2515        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2516        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2517
2518        assert_eq!(functions.len(), 1);
2519        let ops = &functions[0].ops;
2520
2521        // Verify i32 sub-word ops are present
2522        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
2523        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
2524        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
2525        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
2526        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
2527        assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
2528
2529        // Verify i64 sub-word ops are present
2530        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
2531        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
2532        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
2533        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
2534        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
2535        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
2536        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
2537        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
2538        assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
2539    }
2540
2541    #[test]
2542    fn test_decode_simd_i32x4_add() {
2543        let wat = r#"
2544            (module
2545                (func (export "add_v128") (param v128 v128) (result v128)
2546                    local.get 0
2547                    local.get 1
2548                    i32x4.add
2549                )
2550            )
2551        "#;
2552
2553        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2554        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2555
2556        assert_eq!(functions.len(), 1);
2557        assert!(
2558            functions[0].ops.contains(&WasmOp::I32x4Add),
2559            "Should decode i32x4.add: {:?}",
2560            functions[0].ops
2561        );
2562    }
2563
2564    #[test]
2565    fn test_decode_simd_v128_const() {
2566        let wat = r#"
2567            (module
2568                (func (export "const_v128") (result v128)
2569                    v128.const i32x4 1 2 3 4
2570                )
2571            )
2572        "#;
2573
2574        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2575        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2576
2577        assert_eq!(functions.len(), 1);
2578        assert!(
2579            functions[0]
2580                .ops
2581                .iter()
2582                .any(|o| matches!(o, WasmOp::V128Const(_))),
2583            "Should decode v128.const: {:?}",
2584            functions[0].ops
2585        );
2586    }
2587
2588    #[test]
2589    fn test_decode_simd_v128_load_store() {
2590        let wat = r#"
2591            (module
2592                (memory 1)
2593                (func (export "load_store") (param i32)
2594                    local.get 0
2595                    v128.load
2596                    local.get 0
2597                    v128.store
2598                )
2599            )
2600        "#;
2601
2602        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2603        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2604
2605        assert_eq!(functions.len(), 1);
2606        let ops = &functions[0].ops;
2607        assert!(
2608            ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
2609            "Should decode v128.load"
2610        );
2611        assert!(
2612            ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
2613            "Should decode v128.store"
2614        );
2615    }
2616
2617    #[test]
2618    fn test_decode_simd_bitwise_ops() {
2619        let wat = r#"
2620            (module
2621                (func (export "bitwise") (param v128 v128) (result v128)
2622                    local.get 0
2623                    local.get 1
2624                    v128.and
2625                )
2626            )
2627        "#;
2628
2629        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2630        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2631
2632        assert_eq!(functions.len(), 1);
2633        assert!(functions[0].ops.contains(&WasmOp::V128And));
2634    }
2635
2636    #[test]
2637    fn test_decode_simd_splat() {
2638        let wat = r#"
2639            (module
2640                (func (export "splat") (param i32) (result v128)
2641                    local.get 0
2642                    i32x4.splat
2643                )
2644            )
2645        "#;
2646
2647        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2648        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2649
2650        assert_eq!(functions.len(), 1);
2651        assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
2652    }
2653
2654    #[test]
2655    fn test_decode_simd_extract_lane() {
2656        let wat = r#"
2657            (module
2658                (func (export "extract") (param v128) (result i32)
2659                    local.get 0
2660                    i32x4.extract_lane 2
2661                )
2662            )
2663        "#;
2664
2665        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2666        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2667
2668        assert_eq!(functions.len(), 1);
2669        assert!(
2670            functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
2671            "Should decode i32x4.extract_lane 2"
2672        );
2673    }
2674
2675    #[test]
2676    fn test_decode_simd_f32x4_arithmetic() {
2677        let wat = r#"
2678            (module
2679                (func (export "f32x4_add") (param v128 v128) (result v128)
2680                    local.get 0
2681                    local.get 1
2682                    f32x4.add
2683                )
2684            )
2685        "#;
2686
2687        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2688        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2689
2690        assert_eq!(functions.len(), 1);
2691        assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
2692    }
2693
2694    #[test]
2695    fn test_369_scalar_float_op_flags_function_unsupported_not_dropped() {
2696        // GI-FPU-002 (#619): the in-scope scalar f32 ops (add/sub/mul/div,
2697        // comparisons, i32.trunc_f32_s/u, f32.convert_i32_s/u, f32.const) are
2698        // now DECODED (routed to the VFP selector on FPU targets), so `f32.add`
2699        // is no longer flagged — and since phase 2 (#369) so is the lowered
2700        // f64 subset (`f64.add` here; the m7dp-only capability gate lives in
2701        // the selector). An out-of-scope scalar float op (`f64.min`) is STILL
2702        // flagged (loud-skip), never silently dropped — the #369 honesty
2703        // contract holds for the not-yet-lowered surface. A pure-integer
2704        // function stays clean.
2705        let wat = r#"
2706            (module
2707                (func (export "fadd") (param f32 f32) (result f32)
2708                    local.get 0 local.get 1 f32.add)
2709                (func (export "dadd") (param f64 f64) (result f64)
2710                    local.get 0 local.get 1 f64.add)
2711                (func (export "dmin") (param f64 f64) (result f64)
2712                    local.get 0 local.get 1 f64.min)
2713                (func (export "iadd") (param i32 i32) (result i32)
2714                    local.get 0 local.get 1 i32.add))
2715        "#;
2716        let wasm = wat::parse_str(wat).expect("parse");
2717        let functions = decode_wasm_functions(&wasm).expect("decode");
2718        let fadd = functions
2719            .iter()
2720            .find(|f| f.export_name.as_deref() == Some("fadd"))
2721            .unwrap();
2722        let dadd = functions
2723            .iter()
2724            .find(|f| f.export_name.as_deref() == Some("dadd"))
2725            .unwrap();
2726        let dmin = functions
2727            .iter()
2728            .find(|f| f.export_name.as_deref() == Some("dmin"))
2729            .unwrap();
2730        let iadd = functions
2731            .iter()
2732            .find(|f| f.export_name.as_deref() == Some("iadd"))
2733            .unwrap();
2734        // In-scope f32 op: now decoded (reachable), not flagged.
2735        assert!(
2736            fadd.unsupported.is_none(),
2737            "GI-FPU-002: f32.add must now decode (not be flagged), got {:?}",
2738            fadd.unsupported
2739        );
2740        assert!(
2741            fadd.ops.contains(&WasmOp::F32Add),
2742            "f32.add must decode to WasmOp::F32Add: {:?}",
2743            fadd.ops
2744        );
2745        // In-scope f64 op (phase 2, #369): now decoded, not flagged.
2746        assert!(
2747            dadd.unsupported.is_none(),
2748            "GI-FPU-002 phase 2: f64.add must now decode (not be flagged), got {:?}",
2749            dadd.unsupported
2750        );
2751        assert!(
2752            dadd.ops.contains(&WasmOp::F64Add),
2753            "f64.add must decode to WasmOp::F64Add: {:?}",
2754            dadd.ops
2755        );
2756        // Out-of-scope scalar double op: still flagged, never dropped.
2757        assert!(
2758            dmin.unsupported.is_some(),
2759            "f64.min must still flag the function unsupported (out of scope), got {:?}",
2760            dmin.unsupported
2761        );
2762        assert!(
2763            iadd.unsupported.is_none(),
2764            "a pure-integer function must NOT be flagged: {:?}",
2765            iadd.unsupported
2766        );
2767    }
2768
2769    #[test]
2770    fn test_369_float_global_access_flags_function_unsupported() {
2771        // GI-FPU-001 (#369): `global.get`/`global.set` on an f32/f64-typed
2772        // global decode fine (the ops are type-agnostic), but the float
2773        // initializer is dropped (`init_i32: None` -> slot zeroed), so a read
2774        // returned a silently-wrong 0.0 instead of the init (verified: the
2775        // 2.5f bit pattern 0x40200000 was absent from the output ELF). The
2776        // access must flag the function for the loud-skip path. Accesses to
2777        // integer globals stay clean.
2778        let wat = r#"
2779            (module
2780                (global $fg f32 (f32.const 2.5))
2781                (global $dg (mut f64) (f64.const 1.5))
2782                (global $ig (mut i32) (i32.const 7))
2783                (func (export "fget") (result f32) global.get $fg)
2784                (func (export "dset") (param f64) local.get 0 global.set $dg)
2785                (func (export "iget") (result i32) global.get $ig))
2786        "#;
2787        let wasm = wat::parse_str(wat).expect("parse");
2788
2789        // Both decode entry points must flag (the CLI compiles through both:
2790        // decode_wasm_module on the all-exports/module paths,
2791        // decode_wasm_functions on the single-function path).
2792        let module = decode_wasm_module(&wasm).expect("decode module");
2793        for functions in [
2794            &module.functions,
2795            &decode_wasm_functions(&wasm).expect("decode fns"),
2796        ] {
2797            let by_name = |n: &str| {
2798                functions
2799                    .iter()
2800                    .find(|f| f.export_name.as_deref() == Some(n))
2801                    .unwrap()
2802            };
2803            let fget = by_name("fget");
2804            assert!(
2805                fget.unsupported.is_some(),
2806                "global.get of an f32 global must flag the function (loud-skip), got {:?}",
2807                fget.unsupported
2808            );
2809            let reason = fget.unsupported.as_deref().unwrap();
2810            assert!(
2811                reason.contains("GlobalGet") && reason.contains("GI-FPU-001"),
2812                "diagnostic should name the op and GI-FPU-001: {reason:?}"
2813            );
2814            let dset = by_name("dset");
2815            assert!(
2816                dset.unsupported
2817                    .as_deref()
2818                    .is_some_and(|r| r.contains("GlobalSet")),
2819                "global.set of an f64 global must flag the function, got {:?}",
2820                dset.unsupported
2821            );
2822            assert!(
2823                by_name("iget").unsupported.is_none(),
2824                "an i32 global access must NOT be flagged: {:?}",
2825                by_name("iget").unsupported
2826            );
2827        }
2828    }
2829
2830    #[test]
2831    fn test_369_imported_float_global_shifts_index_space() {
2832        // GI-FPU-001 (#369): imported globals come FIRST in the global index
2833        // space. An imported f64 global at index 0 must be flagged, and the
2834        // defined i32 global at index 1 must NOT be mistaken for it.
2835        let wat = r#"
2836            (module
2837                (import "env" "fg" (global f64))
2838                (global $ig i32 (i32.const 3))
2839                (func (export "fget") (result f64) global.get 0)
2840                (func (export "iget") (result i32) global.get 1))
2841        "#;
2842        let wasm = wat::parse_str(wat).expect("parse");
2843        let functions = decode_wasm_functions(&wasm).expect("decode");
2844        let by_name = |n: &str| {
2845            functions
2846                .iter()
2847                .find(|f| f.export_name.as_deref() == Some(n))
2848                .unwrap()
2849        };
2850        assert!(
2851            by_name("fget")
2852                .unsupported
2853                .as_deref()
2854                .is_some_and(|r| r.contains("GI-FPU-001")),
2855            "imported f64 global access must flag: {:?}",
2856            by_name("fget").unsupported
2857        );
2858        assert!(
2859            by_name("iget").unsupported.is_none(),
2860            "defined i32 global at shifted index 1 must NOT flag: {:?}",
2861            by_name("iget").unsupported
2862        );
2863    }
2864
2865    #[test]
2866    fn test_680_simd_ops_flag_function_unsupported_not_dropped() {
2867        // #680: SIMD (v128) ops decode into WasmOp variants no production
2868        // target can select (`has_helium` is test-only), so they were silently
2869        // dropped at selection — `i32x4.add` compiled to an operand
2870        // passthrough (`mov r0,r1`) and shipped a wrong result. The issue's
2871        // exact reproducer must flag the function; the scalar sibling must
2872        // stay compilable (non-vacuity).
2873        let wat = r#"
2874            (module
2875                (memory 1)
2876                (func (export "vadd") (param i32 i32) (result i32)
2877                    (i32x4.extract_lane 2
2878                        (i32x4.add (i32x4.splat (local.get 0))
2879                                   (i32x4.splat (local.get 1)))))
2880                (func (export "vstore") (param i32 i32) (result i32)
2881                    (v128.store (i32.const 0) (i32x4.splat (local.get 0)))
2882                    (i32.load (i32.const 0)))
2883                (func (export "iadd") (param i32 i32) (result i32)
2884                    local.get 0 local.get 1 i32.add))
2885        "#;
2886        let wasm = wat::parse_str(wat).expect("parse");
2887
2888        // Both decode entry points must flag (the CLI compiles through both).
2889        let module = decode_wasm_module(&wasm).expect("decode module");
2890        for functions in [
2891            &module.functions,
2892            &decode_wasm_functions(&wasm).expect("decode fns"),
2893        ] {
2894            let by_name = |n: &str| {
2895                functions
2896                    .iter()
2897                    .find(|f| f.export_name.as_deref() == Some(n))
2898                    .unwrap()
2899            };
2900            for name in ["vadd", "vstore"] {
2901                let reason = by_name(name).unsupported.as_deref();
2902                assert!(
2903                    reason.is_some(),
2904                    "{name}: v128 ops must flag the function (loud-skip), got None"
2905                );
2906                let reason = reason.unwrap();
2907                assert!(
2908                    reason.contains("no SIMD lowering for this target") && reason.contains("#680"),
2909                    "{name}: diagnostic must name the target gap and #680: {reason:?}"
2910                );
2911            }
2912            // The reason names the FIRST SIMD op hit (splat in both bodies).
2913            assert!(
2914                by_name("vadd")
2915                    .unsupported
2916                    .as_deref()
2917                    .unwrap()
2918                    .contains("I32x4Splat"),
2919                "diagnostic should name the op: {:?}",
2920                by_name("vadd").unsupported
2921            );
2922            assert!(
2923                by_name("iadd").unsupported.is_none(),
2924                "a scalar function in the same module must NOT be flagged: {:?}",
2925                by_name("iadd").unsupported
2926            );
2927        }
2928    }
2929
2930    #[test]
2931    fn test_680_v128_local_and_signature_flag_function() {
2932        // #680: v128 VALUES are expressible with ZERO SIMD-proposal operators
2933        // in the body — a v128-typed local or a v128 param/result is reached
2934        // through type-agnostic `local.get`/`local.set`, which the selectors
2935        // lower as 4-byte moves (silent 16-byte truncation). Both must flag.
2936        let wat = r#"
2937            (module
2938                (func (export "vlocal") (result i32) (local v128)
2939                    i32.const 7)
2940                (func (export "vpass") (param v128) (result v128)
2941                    local.get 0)
2942                (func (export "scalar") (param i32) (result i32)
2943                    local.get 0))
2944        "#;
2945        let wasm = wat::parse_str(wat).expect("parse");
2946        let module = decode_wasm_module(&wasm).expect("decode module");
2947        for functions in [
2948            &module.functions,
2949            &decode_wasm_functions(&wasm).expect("decode fns"),
2950        ] {
2951            let by_name = |n: &str| {
2952                functions
2953                    .iter()
2954                    .find(|f| f.export_name.as_deref() == Some(n))
2955                    .unwrap()
2956            };
2957            assert!(
2958                by_name("vlocal")
2959                    .unsupported
2960                    .as_deref()
2961                    .is_some_and(|r| r.contains("v128-typed local") && r.contains("#680")),
2962                "a v128-typed local declaration must flag: {:?}",
2963                by_name("vlocal").unsupported
2964            );
2965            assert!(
2966                by_name("vpass")
2967                    .unsupported
2968                    .as_deref()
2969                    .is_some_and(|r| r.contains("v128 param/result") && r.contains("#680")),
2970                "a v128 param/result signature must flag (op-free body!): {:?}",
2971                by_name("vpass").unsupported
2972            );
2973            assert!(
2974                by_name("scalar").unsupported.is_none(),
2975                "a scalar function must NOT be flagged: {:?}",
2976                by_name("scalar").unsupported
2977            );
2978        }
2979    }
2980
2981    #[test]
2982    fn test_680_v128_global_access_flags_function() {
2983        // #680: `global.get`/`global.set` on a v128-typed global decode fine
2984        // (type-agnostic ops), but the access would move 4 of the 16 bytes and
2985        // the `v128.const` initializer is never captured. Same lane as the
2986        // float globals (#648/GI-FPU-001); imported globals shift the index
2987        // space (imports first). The i32-global sibling stays compilable.
2988        let wat = r#"
2989            (module
2990                (import "env" "vg" (global v128))
2991                (global $ig (mut i32) (i32.const 7))
2992                (global $dg (mut v128) (v128.const i32x4 1 2 3 4))
2993                (func (export "vget") global.get 0 drop)
2994                (func (export "iget") (result i32) global.get $ig))
2995        "#;
2996        let wasm = wat::parse_str(wat).expect("parse");
2997        let module = decode_wasm_module(&wasm).expect("decode module");
2998        for functions in [
2999            &module.functions,
3000            &decode_wasm_functions(&wasm).expect("decode fns"),
3001        ] {
3002            let by_name = |n: &str| {
3003                functions
3004                    .iter()
3005                    .find(|f| f.export_name.as_deref() == Some(n))
3006                    .unwrap()
3007            };
3008            let reason = by_name("vget").unsupported.as_deref();
3009            assert!(
3010                reason.is_some_and(|r| r.contains("GlobalGet")
3011                    && r.contains("v128-typed global")
3012                    && r.contains("#680")),
3013                "global.get of an imported v128 global must flag: {reason:?}"
3014            );
3015            assert!(
3016                by_name("iget").unsupported.is_none(),
3017                "an i32 global access must NOT be flagged: {:?}",
3018                by_name("iget").unsupported
3019            );
3020        }
3021    }
3022
3023    #[test]
3024    fn test_decode_simd_multiple_ops() {
3025        let wat = r#"
3026            (module
3027                (func (export "simd_ops") (param v128 v128 v128) (result v128)
3028                    ;; (a + b) * c
3029                    local.get 0
3030                    local.get 1
3031                    i32x4.add
3032                    local.get 2
3033                    i32x4.mul
3034                )
3035            )
3036        "#;
3037
3038        let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
3039        let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
3040
3041        assert_eq!(functions.len(), 1);
3042        let ops = &functions[0].ops;
3043        assert!(ops.contains(&WasmOp::I32x4Add));
3044        assert!(ops.contains(&WasmOp::I32x4Mul));
3045    }
3046
3047    /// VCR-DBG-001 step 1 (#394): the decoder records a module-relative wasm byte
3048    /// offset per emitted op — the DWARF-for-wasm address space that bridges
3049    /// synth's op-index `source_line` to the input wasm's `.debug_line`. Purely
3050    /// additive metadata (no codegen consumer ⇒ frozen fixtures byte-identical,
3051    /// verified separately); this test pins the structural invariants.
3052    #[test]
3053    fn test_decode_records_aligned_increasing_op_offsets_dbg001() {
3054        let wat = r#"
3055            (module
3056                (func (export "f") (param i32 i32) (result i32)
3057                    local.get 0
3058                    local.get 1
3059                    i32.add
3060                    i32.const 7
3061                    i32.mul))
3062        "#;
3063        let wasm = wat::parse_str(wat).expect("parse WAT");
3064        let functions = decode_wasm_functions(&wasm).expect("decode");
3065        let f = &functions[0];
3066
3067        // One offset per emitted op, index-aligned with `ops`.
3068        assert_eq!(
3069            f.op_offsets.len(),
3070            f.ops.len(),
3071            "op_offsets must be parallel to ops"
3072        );
3073        assert!(!f.op_offsets.is_empty());
3074
3075        // Byte offsets are strictly increasing through the body (each op consumes
3076        // at least one byte) and module-relative (well past the header).
3077        assert!(
3078            f.op_offsets.windows(2).all(|w| w[1] > w[0]),
3079            "wasm byte offsets must strictly increase: {:?}",
3080            f.op_offsets
3081        );
3082        assert!(
3083            f.op_offsets[0] >= 8,
3084            "module-relative offset is past the 8-byte wasm header"
3085        );
3086    }
3087
3088    /// #237: the decoder captures a global's `i32.const` initializer + mutability,
3089    /// so the native-pointer ABI can recognize the stack-pointer global.
3090    #[test]
3091    fn test_decode_captures_global_initializer() {
3092        let wat = r#"
3093            (module
3094                (memory 2)
3095                (global $__stack_pointer (mut i32) (i32.const 65536))
3096                (global $immutable_const i32 (i32.const 7))
3097                (func (export "f") (result i32) global.get 0)
3098            )
3099        "#;
3100        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3101        let module = decode_wasm_module(&wasm).expect("Failed to decode");
3102
3103        assert_eq!(module.globals.len(), 2, "both globals captured");
3104        let sp = &module.globals[0];
3105        assert_eq!(sp.index, 0);
3106        assert_eq!(
3107            sp.init,
3108            Some(GlobalInit::I32(65536)),
3109            "stack-pointer init captured"
3110        );
3111        assert!(sp.mutable, "stack pointer is mutable");
3112        let c = &module.globals[1];
3113        assert_eq!(c.init, Some(GlobalInit::I32(7)));
3114        assert!(!c.mutable, "second global is immutable");
3115        assert_eq!(sp.slot_bytes, 4, "i32 global occupies one 4-byte slot");
3116        assert_eq!(c.slot_bytes, 4);
3117    }
3118
3119    /// #643: the decoder records the DECLARED slot width per global — an i64
3120    /// (or f64) global occupies 8 bytes, so the globals-table layout can give
3121    /// it room for both words and shift every later global's offset.
3122    #[test]
3123    fn test_decode_records_global_slot_widths_643() {
3124        let wat = r#"
3125            (module
3126                (global $c (mut i64) (i64.const 0))
3127                (global $k (mut i32) (i32.const 0))
3128                (global $f (mut f64) (f64.const 0))
3129                (func (export "f") (result i32) global.get 1)
3130            )
3131        "#;
3132        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3133        let module = decode_wasm_module(&wasm).expect("Failed to decode");
3134
3135        assert_eq!(module.globals.len(), 3);
3136        assert_eq!(module.globals[0].slot_bytes, 8, "i64 global is 8 bytes");
3137        assert_eq!(module.globals[1].slot_bytes, 4, "i32 global is 4 bytes");
3138        assert_eq!(module.globals[2].slot_bytes, 8, "f64 global is 8 bytes");
3139    }
3140
3141    /// #649: a nonzero `i64.const` initializer is captured as BOTH words — the
3142    /// `init_i32`-shaped capture dropped it to `None` and every consumer's
3143    /// `unwrap_or(0)` silently ZEROED the global. f32/f64 inits stay `None`
3144    /// (GI-FPU-001/#369 loud-skip lane — never fabricate a float bit-pattern).
3145    #[test]
3146    fn test_decode_captures_i64_global_initializer_649() {
3147        let wat = r#"
3148            (module
3149                (global $g (mut i64) (i64.const 0x123456789ABCDEF0))
3150                (global $n (mut i64) (i64.const -1))
3151                (global $f (mut f64) (f64.const 1.5))
3152                (global $h (mut f32) (f32.const 2.5))
3153                (func (export "f") (result i32) i32.const 0)
3154            )
3155        "#;
3156        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3157        let module = decode_wasm_module(&wasm).expect("Failed to decode");
3158
3159        assert_eq!(module.globals.len(), 4);
3160        assert_eq!(
3161            module.globals[0].init,
3162            Some(GlobalInit::I64(0x123456789ABCDEF0u64 as i64)),
3163            "nonzero i64 init captured with both words"
3164        );
3165        assert_eq!(module.globals[1].init, Some(GlobalInit::I64(-1)));
3166        assert_eq!(
3167            module.globals[2].init, None,
3168            "f64 init is NOT captured (GI-FPU-001 loud-skip lane)"
3169        );
3170        assert_eq!(
3171            module.globals[3].init, None,
3172            "f32 init is NOT captured (GI-FPU-001 loud-skip lane)"
3173        );
3174    }
3175
3176    /// #509: the decoder records `(param_count, result_count)` for every
3177    /// `Block`/`Loop`/`If`, ordinal-keyed in op order, covering all three
3178    /// blocktype encodings: `Empty → (0,0)`, `ValType → (0,1)`, and
3179    /// `FuncType(i) →` counts from the type section (here a multi-result
3180    /// block, which wat encodes as a functype blocktype).
3181    #[test]
3182    fn test_decode_records_block_arity_side_table_509() {
3183        let wat = r#"
3184            (module
3185                (func (export "f") (param i32) (result i32)
3186                    (block (result i32)
3187                        (block (nop))
3188                        (local.get 0)
3189                        (if (result i32)
3190                            (then (i32.const 1))
3191                            (else (i32.const 2)))))
3192                (func (export "g") (result i32)
3193                    (block (result i32 i32)
3194                        (i32.const 1) (i32.const 2))
3195                    i32.add)
3196                (func (export "h") (param i32) (result i32)
3197                    (local.get 0)
3198                    (loop (param i32) (result i32))))
3199        "#;
3200        let wasm = wat::parse_str(wat).expect("parse WAT");
3201
3202        // Both decode entry points must produce the same side-table.
3203        for functions in [
3204            decode_wasm_functions(&wasm).expect("decode"),
3205            decode_wasm_module(&wasm).expect("decode").functions,
3206        ] {
3207            // f: Block(result i32), Block(void), If(result i32) — in op order.
3208            assert_eq!(
3209                functions[0].block_arity,
3210                vec![(0, 1), (0, 0), (0, 1)],
3211                "f: ValType/Empty/ValType blocktypes"
3212            );
3213            // g: one multi-result block via a FuncType blocktype.
3214            assert_eq!(
3215                functions[1].block_arity,
3216                vec![(0, 2)],
3217                "g: functype blocktype result count from the type section"
3218            );
3219            // h: a parameterized loop — the input arity is what a br to the
3220            // header would carry (the #509 loud-decline discriminator).
3221            assert_eq!(
3222                functions[2].block_arity,
3223                vec![(1, 1)],
3224                "h: loop params captured"
3225            );
3226        }
3227    }
3228
3229    /// #642: the decoder captures table 0's compile-time size, per-segment
3230    /// element shapes and per-function type indices, and the closed-world
3231    /// verdict VERIFIES a fully-covered homogeneous table.
3232    #[test]
3233    fn test_call_indirect_guards_closed_world_verified_642() {
3234        // The #642 repro shape: 3-entry table, fully covered, one signature.
3235        let wat = r#"
3236            (module
3237                (type $bin (func (param i32 i32) (result i32)))
3238                (table 3 funcref)
3239                (elem (i32.const 0) $add $sub $mul)
3240                (func $add (param i32 i32) (result i32)
3241                    (i32.add (local.get 0) (local.get 1)))
3242                (func $sub (param i32 i32) (result i32)
3243                    (i32.sub (local.get 0) (local.get 1)))
3244                (func $mul (param i32 i32) (result i32)
3245                    (i32.mul (local.get 0) (local.get 1)))
3246                (func (export "f") (param i32 i32) (result i32)
3247                    (call_indirect (type $bin)
3248                        (local.get 0) (i32.const 10) (local.get 1)))
3249            )
3250        "#;
3251        let wasm = wat::parse_str(wat).expect("parse");
3252        let module = decode_wasm_module(&wasm).expect("decode");
3253
3254        assert_eq!(module.table_size, Some(3), "table section min size");
3255        assert_eq!(module.table_sizes, vec![Some(3)], "#650 per-table sizes");
3256        assert_eq!(
3257            module.elem_segments,
3258            vec![ElemSegmentInfo {
3259                table_index: 0,
3260                offset: Some(0),
3261                funcs: Some(vec![0, 1, 2]),
3262            }]
3263        );
3264        // 2 type-section entries ($bin + the export's (i32 i32)->i32 dedups
3265        // to one in practice, but don't assume — just check func 0..2 share
3266        // a signature with type 0).
3267        assert_eq!(module.func_type_indices.len(), 4);
3268
3269        let guards = module.call_indirect_guards();
3270        assert_eq!(guards.tables.len(), 1);
3271        assert_eq!(guards.tables[0].table_size, Some(3));
3272        assert_eq!(
3273            guards.tables[0].base_byte_offset,
3274            Some(0),
3275            "#650: a single-table module keeps table 0 at R11 offset 0 by construction"
3276        );
3277        // Type index 0 ($bin) must be VERIFIED: every table entry has its
3278        // exact signature.
3279        assert_eq!(
3280            guards.tables[0].type_reject.first(),
3281            Some(&None),
3282            "closed-world type check must verify the homogeneous table: {:?}",
3283            guards.tables[0].type_reject
3284        );
3285        assert!(
3286            !guards.tables[0].has_null_slots,
3287            "#664: a fully-initialized table must NOT request the runtime \
3288             null check (dispatch bytes stay identical by construction)"
3289        );
3290    }
3291
3292    /// #642: a heterogeneous table (an entry whose signature differs from the
3293    /// expected type) must REJECT that expected type — the raw code-pointer
3294    /// table cannot be runtime-type-checked, so the lowering has to decline.
3295    #[test]
3296    fn test_call_indirect_guards_heterogeneous_table_rejects_642() {
3297        let wat = r#"
3298            (module
3299                (type $bin (func (param i32 i32) (result i32)))
3300                (type $un (func (param i32) (result i32)))
3301                (table 2 funcref)
3302                (elem (i32.const 0) $add $neg)
3303                (func $add (type $bin)
3304                    (i32.add (local.get 0) (local.get 1)))
3305                (func $neg (type $un)
3306                    (i32.sub (i32.const 0) (local.get 0)))
3307                (func (export "f") (param i32 i32) (result i32)
3308                    (call_indirect (type $bin)
3309                        (local.get 0) (i32.const 10) (local.get 1)))
3310            )
3311        "#;
3312        let wasm = wat::parse_str(wat).expect("parse");
3313        let module = decode_wasm_module(&wasm).expect("decode");
3314        let guards = module.call_indirect_guards();
3315        assert_eq!(guards.tables[0].table_size, Some(2));
3316        // BOTH expected types must be rejected: the table holds one function
3317        // of each signature, so neither type's closed world holds.
3318        assert!(
3319            guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3320            "heterogeneous table must reject every expected type: {:?}",
3321            guards.tables[0].type_reject
3322        );
3323        // #676: ... but the image is statically known, so the mismatch trap
3324        // is dischargeable at RUNTIME via the type-id sidecar.
3325        assert!(
3326            guards.tables[0].runtime_type_check,
3327            "heterogeneous-but-known table must offer the runtime check (#676)"
3328        );
3329        assert_eq!(
3330            guards.type_ids_byte_offset,
3331            Some(8),
3332            "sidecar sits after the 2-slot pointer region"
3333        );
3334        assert_eq!(
3335            guards.type_ids_image,
3336            vec![1, 2],
3337            "slot 0 = $bin (class 1), slot 1 = $un (class 2)"
3338        );
3339        assert_eq!(guards.type_class_ids, vec![1, 2]);
3340    }
3341
3342    /// #664 (relaxes the #642 all-reject): an uninitialized table slot (elem
3343    /// covers less than the declared size) is a null funcref — calling it
3344    /// must trap, which is now discharged at RUNTIME (null check on the
3345    /// zero-linked pointer), so the closed-world verdict verifies the
3346    /// INITIALIZED slots and sets `has_null_slots` for the lowering.
3347    #[test]
3348    fn test_call_indirect_guards_null_slot_verifies_with_flag_664() {
3349        let wat = r#"
3350            (module
3351                (type $s (func (result i32)))
3352                (table 3 funcref)
3353                (elem (i32.const 0) $f0 $f1)
3354                (func $f0 (result i32) (i32.const 10))
3355                (func $f1 (result i32) (i32.const 11))
3356                (func (export "run") (param i32) (result i32)
3357                    (call_indirect (type $s) (local.get 0)))
3358            )
3359        "#;
3360        let wasm = wat::parse_str(wat).expect("parse");
3361        let module = decode_wasm_module(&wasm).expect("decode");
3362        let guards = module.call_indirect_guards();
3363        assert_eq!(guards.tables[0].table_size, Some(3));
3364        assert_eq!(
3365            guards.tables[0].type_reject.first(),
3366            Some(&None),
3367            "initialized slots are homogeneous in $s — the verdict must \
3368             verify despite the null slot (#664): {:?}",
3369            guards.tables[0].type_reject
3370        );
3371        assert!(
3372            guards.tables[0].has_null_slots,
3373            "slot 2 is uninitialized — the lowering must emit the runtime \
3374             null check (#664)"
3375        );
3376    }
3377
3378    /// #664: the falcon shape — a SPARSE table (only slots 1 and 3 of 4
3379    /// initialized, by two separate segments) verifies with the null flag;
3380    /// a sparse table whose INITIALIZED slots are heterogeneous still
3381    /// rejects (the runtime null check cannot discharge a TYPE mismatch).
3382    #[test]
3383    fn test_call_indirect_guards_sparse_table_664() {
3384        let wat = r#"
3385            (module
3386                (type $t (func (param i32) (result i32)))
3387                (table 4 4 funcref)
3388                (func $f1 (type $t) (i32.add (local.get 0) (i32.const 100)))
3389                (func $f3 (type $t) (i32.sub (i32.const 1000) (local.get 0)))
3390                (elem (i32.const 1) $f1)
3391                (elem (i32.const 3) $f3)
3392                (func (export "via") (param i32 i32) (result i32)
3393                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3394            )
3395        "#;
3396        let wasm = wat::parse_str(wat).expect("parse");
3397        let module = decode_wasm_module(&wasm).expect("decode");
3398        let guards = module.call_indirect_guards();
3399        assert_eq!(guards.tables[0].table_size, Some(4));
3400        assert_eq!(
3401            guards.tables[0].type_reject.first(),
3402            Some(&None),
3403            "slots 1,3 are homogeneous in $t — verified: {:?}",
3404            guards.tables[0].type_reject
3405        );
3406        assert!(guards.tables[0].has_null_slots, "slots 0,2 are null");
3407
3408        // Heterogeneous INITIALIZED slots in a sparse table: still rejected.
3409        let wat = r#"
3410            (module
3411                (type $t (func (param i32) (result i32)))
3412                (type $u (func (param i32 i32) (result i32)))
3413                (table 4 4 funcref)
3414                (func $f1 (type $t) (local.get 0))
3415                (func $f3 (type $u) (i32.add (local.get 0) (local.get 1)))
3416                (elem (i32.const 1) $f1)
3417                (elem (i32.const 3) $f3)
3418                (func (export "via") (param i32 i32) (result i32)
3419                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3420            )
3421        "#;
3422        let wasm = wat::parse_str(wat).expect("parse");
3423        let module = decode_wasm_module(&wasm).expect("decode");
3424        let guards = module.call_indirect_guards();
3425        assert!(
3426            guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3427            "a heterogeneous sparse table must still reject every type: {:?}",
3428            guards.tables[0].type_reject
3429        );
3430        // #676: the sparse-heterogeneous case is now dischargeable at
3431        // runtime too — null slots take the reserved class id 0, so ONE
3432        // sidecar compare covers both the type mismatch and the null trap.
3433        assert!(guards.tables[0].runtime_type_check, "#676 runtime check");
3434        assert_eq!(guards.type_ids_byte_offset, Some(16), "4 pointer slots");
3435        assert_eq!(
3436            guards.type_ids_image,
3437            vec![0, 1, 0, 2],
3438            "nulls at 0/2 carry the reserved id 0; $t slot 1 = class 1, \
3439             $u slot 3 = class 2"
3440        );
3441    }
3442
3443    /// #676: the heterogeneous type-id sidecar — structural duplicate types
3444    /// share one class id (the meld 31-decls/25-distinct shape), null slots
3445    /// take the reserved id 0, and the sidecar base is the total pointer
3446    /// region size. A module with NO heterogeneous table gets NO sidecar
3447    /// (empty image, `None` offset) — homogeneous modules stay untouched.
3448    #[test]
3449    fn test_call_indirect_guards_heterogeneous_sidecar_676() {
3450        let wat = r#"
3451            (module
3452                (type $bin (func (param i32 i32) (result i32)))
3453                (type $un (func (param i32) (result i32)))
3454                (type $bin2 (func (param i32 i32) (result i32)))
3455                (table 5 5 funcref)
3456                (func $add (type $bin) (i32.add (local.get 0) (local.get 1)))
3457                (func $neg (type $un) (i32.sub (i32.const 0) (local.get 0)))
3458                (func $sub (type $bin2) (i32.sub (local.get 0) (local.get 1)))
3459                (elem (i32.const 0) func $add $neg $sub)
3460                (func (export "via2") (param i32 i32) (result i32)
3461                    (call_indirect (type $bin)
3462                        (local.get 0) (i32.const 3) (local.get 1)))
3463                (func (export "via1") (param i32 i32) (result i32)
3464                    (call_indirect (type $un) (local.get 0) (local.get 1)))
3465            )
3466        "#;
3467        let wasm = wat::parse_str(wat).expect("parse");
3468        let module = decode_wasm_module(&wasm).expect("decode");
3469        let guards = module.call_indirect_guards();
3470        assert!(guards.tables[0].runtime_type_check);
3471        assert_eq!(
3472            guards.type_class_ids,
3473            vec![1, 2, 1],
3474            "$bin2 is a structural duplicate of $bin — one class id (#676)"
3475        );
3476        assert_eq!(
3477            guards.type_ids_image,
3478            vec![1, 2, 1, 0, 0],
3479            "slots: $add(bin)=1, $neg(un)=2, $sub(bin2 ≡ bin)=1, null, null"
3480        );
3481        assert_eq!(
3482            guards.type_ids_byte_offset,
3483            Some(20),
3484            "sidecar starts after the 5 pointer words"
3485        );
3486
3487        // Homogeneous module → NO sidecar, no runtime check anywhere.
3488        let wat = r#"
3489            (module
3490                (type $t (func (param i32) (result i32)))
3491                (table 2 2 funcref)
3492                (func $f0 (type $t) (local.get 0))
3493                (func $f1 (type $t) (i32.const 7))
3494                (elem (i32.const 0) func $f0 $f1)
3495                (func (export "via") (param i32 i32) (result i32)
3496                    (call_indirect (type $t) (local.get 0) (local.get 1)))
3497            )
3498        "#;
3499        let wasm = wat::parse_str(wat).expect("parse");
3500        let module = decode_wasm_module(&wasm).expect("decode");
3501        let guards = module.call_indirect_guards();
3502        assert!(!guards.tables[0].runtime_type_check);
3503        assert_eq!(guards.type_ids_byte_offset, None, "no heterogeneous table");
3504        assert!(guards.type_ids_image.is_empty());
3505        assert!(guards.type_class_ids.is_empty());
3506    }
3507
3508    /// #642: no table at all → no compile-time bound → table_size None and
3509    /// every type rejected (the lowering declines).
3510    #[test]
3511    fn test_call_indirect_guards_no_table_642() {
3512        let wat = r#"
3513            (module
3514                (func (export "f") (param i32) (result i32) (local.get 0))
3515            )
3516        "#;
3517        let wasm = wat::parse_str(wat).expect("parse");
3518        let module = decode_wasm_module(&wasm).expect("decode");
3519        assert_eq!(module.table_size, None);
3520        assert!(module.table_sizes.is_empty(), "#650: no tables declared");
3521        let guards = module.call_indirect_guards();
3522        assert!(
3523            guards.tables.is_empty(),
3524            "no table → no guard entry → every call_indirect declines"
3525        );
3526    }
3527
3528    /// #642: duplicate-but-structurally-identical types stay interchangeable —
3529    /// the closed-world check compares SIGNATURES, not type indices.
3530    #[test]
3531    fn test_call_indirect_guards_duplicate_types_verified_642() {
3532        let wat = r#"
3533            (module
3534                (type $a (func (result i32)))
3535                (type $b (func (result i32)))
3536                (table 1 funcref)
3537                (elem (i32.const 0) $f)
3538                (func $f (type $a) (i32.const 7))
3539                (func (export "run") (param i32) (result i32)
3540                    (call_indirect (type $b) (local.get 0)))
3541            )
3542        "#;
3543        let wasm = wat::parse_str(wat).expect("parse");
3544        let module = decode_wasm_module(&wasm).expect("decode");
3545        let guards = module.call_indirect_guards();
3546        // $f has type $a; the call expects $b — structurally identical, so
3547        // BOTH type indices must verify. (A third type — the export's
3548        // (i32)->i32 — is correctly rejected: different signature.)
3549        assert_eq!(
3550            &guards.tables[0].type_reject[0..2],
3551            &[None, None],
3552            "structural signature comparison must accept duplicate types: {:?}",
3553            guards.tables[0].type_reject
3554        );
3555        assert!(
3556            guards.tables[0].type_reject[2].is_some(),
3557            "the structurally-different third type must still be rejected"
3558        );
3559    }
3560
3561    /// #650: TWO tables become a contiguous R11 region — table 0 at offset 0
3562    /// (byte-identical single-table degeneration), table 1 at
3563    /// `size(table 0) * 4`. Each table gets its OWN size, base offset, and
3564    /// per-type closed-world verdicts (segments only poison the table they
3565    /// target).
3566    #[test]
3567    fn test_call_indirect_guards_multi_table_650() {
3568        // The #650 repro shape: overlapping indices, distinct functions —
3569        // table0[1] != table1[1] (the aliasing canary).
3570        let wat = r#"
3571            (module
3572                (type $t (func (param i32) (result i32)))
3573                (type $u (func (param i32 i32) (result i32)))
3574                (table $t0 3 3 funcref)
3575                (table $t1 2 2 funcref)
3576                (func $a0 (type $t) (i32.add (local.get 0) (i32.const 100)))
3577                (func $a1 (type $t) (i32.add (local.get 0) (i32.const 200)))
3578                (func $a2 (type $t) (i32.add (local.get 0) (i32.const 300)))
3579                (func $b0 (type $u) (i32.add (local.get 0) (local.get 1)))
3580                (func $b1 (type $u) (i32.sub (local.get 0) (local.get 1)))
3581                (elem (table $t0) (i32.const 0) func $a0 $a1 $a2)
3582                (elem (table $t1) (i32.const 0) func $b0 $b1)
3583                (func (export "f") (param i32 i32) (result i32)
3584                    (call_indirect $t1 (type $u)
3585                        (local.get 0) (i32.const 10) (local.get 1)))
3586            )
3587        "#;
3588        let wasm = wat::parse_str(wat).expect("parse");
3589        let module = decode_wasm_module(&wasm).expect("decode");
3590        assert_eq!(module.table_sizes, vec![Some(3), Some(2)]);
3591        assert_eq!(module.table_size, Some(3), "compat accessor = table 0");
3592        assert_eq!(
3593            module.elem_segments[0].table_index, 0,
3594            "segment 0 targets table 0"
3595        );
3596        assert_eq!(
3597            module.elem_segments[1],
3598            ElemSegmentInfo {
3599                table_index: 1,
3600                offset: Some(0),
3601                funcs: Some(vec![3, 4]),
3602            },
3603            "segment 1 is statically attributed to table 1 (#650)"
3604        );
3605
3606        let guards = module.call_indirect_guards();
3607        assert_eq!(guards.tables.len(), 2);
3608        assert_eq!(guards.tables[0].table_size, Some(3));
3609        assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3610        assert_eq!(guards.tables[1].table_size, Some(2));
3611        assert_eq!(
3612            guards.tables[1].base_byte_offset,
3613            Some(12),
3614            "table 1 base = size(table 0) * 4 within the contiguous R11 region"
3615        );
3616        // Table 0 is homogeneous in $t (type 0); table 1 in $u (type 1) —
3617        // each verifies ITS type and rejects the other's.
3618        assert_eq!(guards.tables[0].type_reject[0], None, "table 0 vs $t");
3619        assert!(guards.tables[0].type_reject[1].is_some(), "table 0 vs $u");
3620        assert!(guards.tables[1].type_reject[0].is_some(), "table 1 vs $t");
3621        assert_eq!(guards.tables[1].type_reject[1], None, "table 1 vs $u");
3622    }
3623
3624    /// #650: an unknown-size table (growable import) declines ITSELF and
3625    /// makes every LATER table's base offset non-constant — but a table
3626    /// BEFORE it is unaffected.
3627    #[test]
3628    fn test_call_indirect_guards_unknown_size_poisons_later_bases_650() {
3629        let wat = r#"
3630            (module
3631                (type $t (func (result i32)))
3632                (import "env" "tbl" (table 4 funcref))
3633                (table $d 1 1 funcref)
3634                (func $f (type $t) (i32.const 7))
3635                (elem (table $d) (i32.const 0) func $f)
3636                (func (export "run") (param i32) (result i32)
3637                    (call_indirect $d (type $t) (local.get 0)))
3638            )
3639        "#;
3640        let wasm = wat::parse_str(wat).expect("parse");
3641        let module = decode_wasm_module(&wasm).expect("decode");
3642        assert_eq!(
3643            module.table_sizes,
3644            vec![None, Some(1)],
3645            "growable import (no max) has no sound compile-time size"
3646        );
3647        let guards = module.call_indirect_guards();
3648        assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3649        assert!(
3650            guards.tables[0].type_reject.iter().all(|r| r.is_some()),
3651            "unknown-size table rejects every type"
3652        );
3653        assert_eq!(
3654            guards.tables[1].base_byte_offset, None,
3655            "a later table's base is not a compile-time constant when a \
3656             preceding table's size is unknown (#650)"
3657        );
3658        assert_eq!(guards.tables[1].table_size, Some(1));
3659    }
3660}