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