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