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