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