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