Skip to main content

synth_core/
wasm_decoder.rs

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