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