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