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