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 // #869: the 64-bit integer<->float conversion family — previously all
2262 // dropped here (`_ => None`), which loud-skipped every function using
2263 // them (3 of 5 falcon cascade stages at their public entry points).
2264 // ARMv7E-M VFP has no 64-bit-integer<->float instruction (VCVT on
2265 // FPv4-SP/FPv5-D16 encodes only S32/U32 <-> F32/F64), so the ARM32
2266 // lowering is a self-contained multi-step expansion on cortex-m7dp
2267 // (see `try_lower_f32`/`try_lower_f64`); the TRAPPING trunc forms
2268 // carry the #709-class i64 domain guard (WASM §4.3.3 requires a trap
2269 // on NaN/out-of-range — the saturating decompose alone would be a
2270 // silent miscompile). Backends without the machinery (RV32, aarch64's
2271 // current subset, single-precision ARM) still loud-decline downstream.
2272 F32ConvertI64S => Some(WasmOp::F32ConvertI64S),
2273 F32ConvertI64U => Some(WasmOp::F32ConvertI64U),
2274 I64TruncF32S => Some(WasmOp::I64TruncF32S),
2275 I64TruncF32U => Some(WasmOp::I64TruncF32U),
2276 // #782a: the nontrapping trunc_sat family (0xFC-prefixed,
2277 // saturating-float-to-int proposal) — TOTAL ops (§4.3.2: NaN → 0,
2278 // out-of-range saturates to INT_MIN/INT_MAX, no traps). Un-dropped so
2279 // the selectors see them: ARM32 lowers the i32-target forms as a bare
2280 // VCVT (round-toward-zero VCVT already saturates and gives 0 for NaN —
2281 // exactly trunc_sat, the very behavior the #709 guard exists to keep
2282 // away from the TRAPPING forms); aarch64 lowers all eight via
2283 // FCVTZS/FCVTZU. The i64-target forms LOUD-decline on 32-bit ARM (no
2284 // i64 register-pair conversion path) and RV32 (no floats at all).
2285 I32TruncSatF32S => Some(WasmOp::I32TruncSatF32S),
2286 I32TruncSatF32U => Some(WasmOp::I32TruncSatF32U),
2287 I64TruncSatF32S => Some(WasmOp::I64TruncSatF32S),
2288 I64TruncSatF32U => Some(WasmOp::I64TruncSatF32U),
2289
2290 // === Scalar f64 (GI-FPU-002 phase 2, #369) ===
2291 // Un-dropped for the DOUBLE-precision FPU target (cortex-m7dp D0..D15);
2292 // the capability gate lives in `select_with_stack`'s preamble — any
2293 // f64 op on m4f/m7 (single-precision) or m0/m3/r5 (no FPU) still
2294 // honest-rejects its function. Exactly this set is lowered by
2295 // `try_lower_f64` + the `F64Load`/`F64Store` selector arms; the rest
2296 // of the f64 surface (min/max/copysign/rounding/i64<->f64/…) stays at
2297 // `_ => None` (loud-skip) until a later increment wires it.
2298 F64Const { value } => Some(WasmOp::F64Const(f64::from_bits(value.bits()))),
2299 F64PromoteF32 => Some(WasmOp::F64PromoteF32),
2300 F64Add => Some(WasmOp::F64Add),
2301 F64Sub => Some(WasmOp::F64Sub),
2302 F64Mul => Some(WasmOp::F64Mul),
2303 F64Div => Some(WasmOp::F64Div),
2304 F64Abs => Some(WasmOp::F64Abs),
2305 F64Neg => Some(WasmOp::F64Neg),
2306 F64Sqrt => Some(WasmOp::F64Sqrt),
2307 F64Eq => Some(WasmOp::F64Eq),
2308 F64Ne => Some(WasmOp::F64Ne),
2309 F64Lt => Some(WasmOp::F64Lt),
2310 F64Le => Some(WasmOp::F64Le),
2311 F64Gt => Some(WasmOp::F64Gt),
2312 F64Ge => Some(WasmOp::F64Ge),
2313 F64Load { memarg } => Some(WasmOp::F64Load {
2314 offset: memarg.offset as u32,
2315 align: memarg.align as u32,
2316 }),
2317 F64Store { memarg } => Some(WasmOp::F64Store {
2318 offset: memarg.offset as u32,
2319 align: memarg.align as u32,
2320 }),
2321 // GI-FPU-002 phase 3 (#369): the f64 op tail — rounding via single
2322 // VRINT{P,M,Z,N}.F64 (FPv5), min/max via VMINNM/VMAXNM + the
2323 // NaN-propagating fix-up, copysign via the VABS/conditional-VNEG
2324 // splice, f32.demote_f64 / i32<->f64 conversions via VCVT
2325 // (i32.trunc_f64_* carries the #709 trap-on-out-of-range domain
2326 // guard). Still m7dp-only (the selector preamble honest-rejects any
2327 // f64 op elsewhere). The i64<->f64 reinterprets were un-dropped by
2328 // #851 and the i64<->f64 conversions by #869 (below).
2329 F64Ceil => Some(WasmOp::F64Ceil),
2330 F64Floor => Some(WasmOp::F64Floor),
2331 F64Trunc => Some(WasmOp::F64Trunc),
2332 F64Nearest => Some(WasmOp::F64Nearest),
2333 F64Min => Some(WasmOp::F64Min),
2334 F64Max => Some(WasmOp::F64Max),
2335 F64Copysign => Some(WasmOp::F64Copysign),
2336 F32DemoteF64 => Some(WasmOp::F32DemoteF64),
2337 F64ConvertI32S => Some(WasmOp::F64ConvertI32S),
2338 F64ConvertI32U => Some(WasmOp::F64ConvertI32U),
2339 I32TruncF64S => Some(WasmOp::I32TruncF64S),
2340 I32TruncF64U => Some(WasmOp::I32TruncF64U),
2341 // #869 (+#756): the f64 half of the 64-bit integer<->float family —
2342 // i64->f64 exact two-word VCVT+scale+add, i64.trunc_f64_* via the
2343 // #709-class i64 domain guard + the #782 word-decompose. See the f32
2344 // group above for the family story.
2345 F64ConvertI64S => Some(WasmOp::F64ConvertI64S),
2346 F64ConvertI64U => Some(WasmOp::F64ConvertI64U),
2347 I64TruncF64S => Some(WasmOp::I64TruncF64S),
2348 I64TruncF64U => Some(WasmOp::I64TruncF64U),
2349 // #782a: f64-source trunc_sat twins (see the f32 group above). falcon
2350 // v1.123 carries 7× i32.trunc_sat_f64_s — the m7dp double-precision
2351 // VCVT twins lower the i32-target forms; i64 targets loud-decline.
2352 I32TruncSatF64S => Some(WasmOp::I32TruncSatF64S),
2353 I32TruncSatF64U => Some(WasmOp::I32TruncSatF64U),
2354 I64TruncSatF64S => Some(WasmOp::I64TruncSatF64S),
2355 I64TruncSatF64U => Some(WasmOp::I64TruncSatF64U),
2356
2357 // f32x4
2358 F32x4Add => Some(WasmOp::F32x4Add),
2359 F32x4Sub => Some(WasmOp::F32x4Sub),
2360 F32x4Mul => Some(WasmOp::F32x4Mul),
2361 F32x4Div => Some(WasmOp::F32x4Div),
2362 F32x4Abs => Some(WasmOp::F32x4Abs),
2363 F32x4Neg => Some(WasmOp::F32x4Neg),
2364 F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
2365 F32x4Eq => Some(WasmOp::F32x4Eq),
2366 F32x4Ne => Some(WasmOp::F32x4Ne),
2367 F32x4Lt => Some(WasmOp::F32x4Lt),
2368 F32x4Le => Some(WasmOp::F32x4Le),
2369 F32x4Gt => Some(WasmOp::F32x4Gt),
2370 F32x4Ge => Some(WasmOp::F32x4Ge),
2371 F32x4Splat => Some(WasmOp::F32x4Splat),
2372 F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
2373 F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
2374
2375 // Other operators not yet supported
2376 _ => None,
2377 }
2378}
2379
2380#[cfg(test)]
2381mod tests {
2382 use super::*;
2383
2384 #[test]
2385 fn test_decode_simple_add() {
2386 let wat = r#"
2387 (module
2388 (func (export "add") (param i32 i32) (result i32)
2389 local.get 0
2390 local.get 1
2391 i32.add
2392 )
2393 )
2394 "#;
2395
2396 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2397 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2398
2399 assert_eq!(functions.len(), 1);
2400 assert_eq!(functions[0].index, 0);
2401 assert_eq!(functions[0].export_name, Some("add".to_string()));
2402 assert_eq!(
2403 functions[0].ops,
2404 vec![
2405 WasmOp::LocalGet(0),
2406 WasmOp::LocalGet(1),
2407 WasmOp::I32Add,
2408 WasmOp::End
2409 ]
2410 );
2411 }
2412
2413 /// #782a: the 0xFC-prefixed trunc_sat family must DECODE (it was
2414 /// previously unmapped → the whole function loud-skipped as
2415 /// "unsupported operator", the falcon #782 `ts32`/`ts64` class). All
2416 /// eight forms must surface as their `WasmOp` variants so the selectors
2417 /// can lower (ARM32 i32-targets, aarch64 all) or LOUD-decline (ARM32
2418 /// i64-targets, RV32) per backend.
2419 #[test]
2420 fn test_decode_trunc_sat_family() {
2421 let wat = r#"
2422 (module
2423 (func (export "ts") (param f32 f64) (result i32)
2424 (i32.trunc_sat_f32_s (local.get 0))
2425 (i32.trunc_sat_f32_u (local.get 0))
2426 i32.add
2427 (i32.trunc_sat_f64_s (local.get 1))
2428 i32.add
2429 (i32.trunc_sat_f64_u (local.get 1))
2430 i32.add
2431 (i32.wrap_i64 (i64.trunc_sat_f32_s (local.get 0)))
2432 i32.add
2433 (i32.wrap_i64 (i64.trunc_sat_f32_u (local.get 0)))
2434 i32.add
2435 (i32.wrap_i64 (i64.trunc_sat_f64_s (local.get 1)))
2436 i32.add
2437 (i32.wrap_i64 (i64.trunc_sat_f64_u (local.get 1)))
2438 i32.add
2439 )
2440 )
2441 "#;
2442 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2443 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2444 assert_eq!(functions.len(), 1);
2445 let f = &functions[0];
2446 assert!(
2447 f.unsupported.is_none(),
2448 "trunc_sat must decode, not loud-skip: {:?}",
2449 f.unsupported
2450 );
2451 for want in [
2452 WasmOp::I32TruncSatF32S,
2453 WasmOp::I32TruncSatF32U,
2454 WasmOp::I32TruncSatF64S,
2455 WasmOp::I32TruncSatF64U,
2456 WasmOp::I64TruncSatF32S,
2457 WasmOp::I64TruncSatF32U,
2458 WasmOp::I64TruncSatF64S,
2459 WasmOp::I64TruncSatF64U,
2460 ] {
2461 assert!(
2462 f.ops.contains(&want),
2463 "decoded ops must contain {want:?}: {:?}",
2464 f.ops
2465 );
2466 }
2467 }
2468
2469 /// #204 regression: `i64.extend_i32_u`, `i64.extend_i32_s` and
2470 /// `i32.wrap_i64` must DECODE (they were previously unmapped → silently
2471 /// dropped by `convert_operator`, leaving an i32 value as a 64-bit operand
2472 /// with a garbage high half — the root cause of gale's miscompiled
2473 /// `(new_count << 32)` pack). The decoder must surface all three.
2474 #[test]
2475 fn test_decode_i64_i32_width_conversions() {
2476 let wat = r#"
2477 (module
2478 (func (export "conv") (param i32 i64) (result i32)
2479 local.get 0
2480 i64.extend_i32_u
2481 local.get 0
2482 i64.extend_i32_s
2483 i64.add
2484 local.get 1
2485 i64.add
2486 i32.wrap_i64
2487 )
2488 )
2489 "#;
2490 let wasm = wat::parse_str(wat).expect("parse");
2491 let functions = decode_wasm_functions(&wasm).expect("decode");
2492 let ops = &functions[0].ops;
2493 assert!(
2494 ops.contains(&WasmOp::I64ExtendI32U),
2495 "i64.extend_i32_u must decode (not be dropped): {ops:?}"
2496 );
2497 assert!(
2498 ops.contains(&WasmOp::I64ExtendI32S),
2499 "i64.extend_i32_s must decode (not be dropped): {ops:?}"
2500 );
2501 assert!(
2502 ops.contains(&WasmOp::I32WrapI64),
2503 "i32.wrap_i64 must decode (not be dropped): {ops:?}"
2504 );
2505 }
2506
2507 /// #204 WAKE-path regression: `br_table` must DECODE (it was unmapped in
2508 /// `convert_operator` → silently dropped, so the selector emitted no index
2509 /// dispatch and every `br_table` fell through to target 0 — gale's binary
2510 /// semaphore never took its WAKE branch). Targets + default are preserved.
2511 #[test]
2512 fn test_decode_br_table() {
2513 let wat = r#"
2514 (module
2515 (func (export "bt") (param i32) (result i32)
2516 (block (block (block
2517 local.get 0
2518 br_table 2 0 1 2)
2519 i32.const 30 return)
2520 i32.const 20 return)
2521 i32.const 10))
2522 "#;
2523 let wasm = wat::parse_str(wat).expect("parse");
2524 let functions = decode_wasm_functions(&wasm).expect("decode");
2525 let bt = functions[0]
2526 .ops
2527 .iter()
2528 .find_map(|o| match o {
2529 WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
2530 _ => None,
2531 })
2532 .expect("br_table must decode (not be dropped)");
2533 assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
2534 assert_eq!(bt.1, 2, "br_table default preserved");
2535 }
2536
2537 #[test]
2538 fn test_decode_arithmetic() {
2539 let wat = r#"
2540 (module
2541 (func (export "calc") (result i32)
2542 i32.const 5
2543 i32.const 3
2544 i32.mul
2545 i32.const 2
2546 i32.add
2547 )
2548 )
2549 "#;
2550
2551 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2552 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2553
2554 assert_eq!(functions.len(), 1);
2555 assert_eq!(functions[0].export_name, Some("calc".to_string()));
2556 assert_eq!(
2557 functions[0].ops,
2558 vec![
2559 WasmOp::I32Const(5),
2560 WasmOp::I32Const(3),
2561 WasmOp::I32Mul,
2562 WasmOp::I32Const(2),
2563 WasmOp::I32Add,
2564 WasmOp::End,
2565 ]
2566 );
2567 }
2568
2569 #[test]
2570 fn test_decode_multi_function_module() {
2571 let wat = r#"
2572 (module
2573 (func $helper)
2574 (func (export "add") (param i32 i32) (result i32)
2575 local.get 0
2576 local.get 1
2577 i32.add
2578 )
2579 (func (export "sub") (param i32 i32) (result i32)
2580 local.get 0
2581 local.get 1
2582 i32.sub
2583 )
2584 )
2585 "#;
2586
2587 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2588 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2589
2590 assert_eq!(functions.len(), 3);
2591 assert_eq!(functions[0].index, 0);
2592 assert_eq!(functions[0].export_name, None);
2593 assert_eq!(functions[1].index, 1);
2594 assert_eq!(functions[1].export_name, Some("add".to_string()));
2595 assert_eq!(functions[2].index, 2);
2596 assert_eq!(functions[2].export_name, Some("sub".to_string()));
2597 }
2598
2599 #[test]
2600 fn test_decode_module_with_imports() {
2601 let wat = r#"
2602 (module
2603 (import "env" "log" (func $log (param i32)))
2604 (import "env" "memory" (memory 1))
2605 (func (export "run") (param i32)
2606 local.get 0
2607 call 0
2608 )
2609 )
2610 "#;
2611
2612 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2613 let module = decode_wasm_module(&wasm).expect("Failed to decode");
2614
2615 // Should have 2 imports (1 func, 1 memory)
2616 assert_eq!(module.imports.len(), 2);
2617 assert_eq!(module.num_imported_funcs, 1);
2618
2619 // First import is the function
2620 assert_eq!(module.imports[0].module, "env");
2621 assert_eq!(module.imports[0].name, "log");
2622 assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
2623
2624 // Second import is memory
2625 assert_eq!(module.imports[1].module, "env");
2626 assert_eq!(module.imports[1].name, "memory");
2627 assert_eq!(module.imports[1].kind, ImportKind::Memory);
2628
2629 // Should have 1 local function (index 1, because import is index 0)
2630 assert_eq!(module.functions.len(), 1);
2631 assert_eq!(module.functions[0].index, 1);
2632 assert_eq!(module.functions[0].export_name, Some("run".to_string()));
2633 }
2634
2635 #[test]
2636 fn test_find_function_by_export_name() {
2637 let wat = r#"
2638 (module
2639 (func $helper)
2640 (func (export "add") (param i32 i32) (result i32)
2641 local.get 0
2642 local.get 1
2643 i32.add
2644 )
2645 )
2646 "#;
2647
2648 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2649 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2650
2651 let add_func = functions
2652 .iter()
2653 .find(|f| f.export_name.as_deref() == Some("add"))
2654 .expect("Should find 'add' function");
2655
2656 assert_eq!(add_func.index, 1);
2657 assert!(add_func.ops.contains(&WasmOp::I32Add));
2658 }
2659
2660 #[test]
2661 fn test_decode_subword_loads() {
2662 let wat = r#"
2663 (module
2664 (memory 1)
2665 (func (export "test") (param i32) (result i32)
2666 local.get 0
2667 i32.load8_u
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::I32Load8U {
2677 offset: 0,
2678 align: 0,
2679 }));
2680 }
2681
2682 #[test]
2683 fn test_decode_subword_stores() {
2684 let wat = r#"
2685 (module
2686 (memory 1)
2687 (func (export "test") (param i32 i32)
2688 local.get 0
2689 local.get 1
2690 i32.store8
2691 )
2692 )
2693 "#;
2694
2695 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2696 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2697
2698 assert_eq!(functions.len(), 1);
2699 assert!(functions[0].ops.contains(&WasmOp::I32Store8 {
2700 offset: 0,
2701 align: 0,
2702 }));
2703 }
2704
2705 #[test]
2706 fn test_decode_memory_size_grow() {
2707 let wat = r#"
2708 (module
2709 (memory 1)
2710 (func (export "test") (result i32)
2711 memory.size
2712 )
2713 )
2714 "#;
2715
2716 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2717 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2718
2719 assert_eq!(functions.len(), 1);
2720 assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
2721 }
2722
2723 #[test]
2724 fn test_decode_memory_grow() {
2725 let wat = r#"
2726 (module
2727 (memory 1)
2728 (func (export "test") (param i32) (result i32)
2729 local.get 0
2730 memory.grow
2731 )
2732 )
2733 "#;
2734
2735 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2736 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2737
2738 assert_eq!(functions.len(), 1);
2739 assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
2740 }
2741
2742 #[test]
2743 fn test_decode_bulk_memory_374() {
2744 // #374: memory.copy / memory.fill on the single linear memory decode to
2745 // the new WasmOp variants (was `_ => None` -> loud-skip).
2746 let wat = r#"
2747 (module
2748 (memory 1)
2749 (func (export "cpy") (param i32 i32 i32)
2750 local.get 0 local.get 1 local.get 2 memory.copy)
2751 (func (export "fil") (param i32 i32 i32)
2752 local.get 0 local.get 1 local.get 2 memory.fill)
2753 )
2754 "#;
2755 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2756 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2757 assert_eq!(functions.len(), 2);
2758 assert!(functions[0].ops.contains(&WasmOp::MemoryCopy));
2759 assert!(functions[1].ops.contains(&WasmOp::MemoryFill));
2760 // Neither function is flagged unsupported (they now lower).
2761 assert!(functions[0].unsupported.is_none());
2762 assert!(functions[1].unsupported.is_none());
2763 }
2764
2765 #[test]
2766 fn test_decode_i64_subword_loads() {
2767 let wat = r#"
2768 (module
2769 (memory 1)
2770 (func (export "test") (param i32) (result i64)
2771 local.get 0
2772 i64.load8_s
2773 )
2774 )
2775 "#;
2776
2777 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2778 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2779
2780 assert_eq!(functions.len(), 1);
2781 assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
2782 offset: 0,
2783 align: 0,
2784 }));
2785 }
2786
2787 #[test]
2788 fn test_decode_all_subword_memory_ops() {
2789 // Test that all sub-word operations are decoded from WAT
2790 let wat = r#"
2791 (module
2792 (memory 1)
2793 (func (export "test") (param i32)
2794 ;; i32 sub-word loads
2795 local.get 0
2796 i32.load8_s
2797 drop
2798 local.get 0
2799 i32.load8_u
2800 drop
2801 local.get 0
2802 i32.load16_s
2803 drop
2804 local.get 0
2805 i32.load16_u
2806 drop
2807
2808 ;; i32 sub-word stores
2809 local.get 0
2810 i32.const 42
2811 i32.store8
2812 local.get 0
2813 i32.const 42
2814 i32.store16
2815
2816 ;; i64 sub-word loads
2817 local.get 0
2818 i64.load8_s
2819 drop
2820 local.get 0
2821 i64.load8_u
2822 drop
2823 local.get 0
2824 i64.load16_s
2825 drop
2826 local.get 0
2827 i64.load16_u
2828 drop
2829 local.get 0
2830 i64.load32_s
2831 drop
2832 local.get 0
2833 i64.load32_u
2834 drop
2835
2836 ;; i64 sub-word stores
2837 local.get 0
2838 i64.const 42
2839 i64.store8
2840 local.get 0
2841 i64.const 42
2842 i64.store16
2843 local.get 0
2844 i64.const 42
2845 i64.store32
2846 )
2847 )
2848 "#;
2849
2850 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2851 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2852
2853 assert_eq!(functions.len(), 1);
2854 let ops = &functions[0].ops;
2855
2856 // Verify i32 sub-word ops are present
2857 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
2858 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
2859 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
2860 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
2861 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
2862 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
2863
2864 // Verify i64 sub-word ops are present
2865 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
2866 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
2867 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
2868 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
2869 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
2870 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
2871 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
2872 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
2873 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
2874 }
2875
2876 #[test]
2877 fn test_decode_simd_i32x4_add() {
2878 let wat = r#"
2879 (module
2880 (func (export "add_v128") (param v128 v128) (result v128)
2881 local.get 0
2882 local.get 1
2883 i32x4.add
2884 )
2885 )
2886 "#;
2887
2888 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2889 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2890
2891 assert_eq!(functions.len(), 1);
2892 assert!(
2893 functions[0].ops.contains(&WasmOp::I32x4Add),
2894 "Should decode i32x4.add: {:?}",
2895 functions[0].ops
2896 );
2897 }
2898
2899 #[test]
2900 fn test_decode_simd_v128_const() {
2901 let wat = r#"
2902 (module
2903 (func (export "const_v128") (result v128)
2904 v128.const i32x4 1 2 3 4
2905 )
2906 )
2907 "#;
2908
2909 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2910 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2911
2912 assert_eq!(functions.len(), 1);
2913 assert!(
2914 functions[0]
2915 .ops
2916 .iter()
2917 .any(|o| matches!(o, WasmOp::V128Const(_))),
2918 "Should decode v128.const: {:?}",
2919 functions[0].ops
2920 );
2921 }
2922
2923 #[test]
2924 fn test_decode_simd_v128_load_store() {
2925 let wat = r#"
2926 (module
2927 (memory 1)
2928 (func (export "load_store") (param i32)
2929 local.get 0
2930 v128.load
2931 local.get 0
2932 v128.store
2933 )
2934 )
2935 "#;
2936
2937 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2938 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2939
2940 assert_eq!(functions.len(), 1);
2941 let ops = &functions[0].ops;
2942 assert!(
2943 ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
2944 "Should decode v128.load"
2945 );
2946 assert!(
2947 ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
2948 "Should decode v128.store"
2949 );
2950 }
2951
2952 #[test]
2953 fn test_decode_simd_bitwise_ops() {
2954 let wat = r#"
2955 (module
2956 (func (export "bitwise") (param v128 v128) (result v128)
2957 local.get 0
2958 local.get 1
2959 v128.and
2960 )
2961 )
2962 "#;
2963
2964 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2965 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2966
2967 assert_eq!(functions.len(), 1);
2968 assert!(functions[0].ops.contains(&WasmOp::V128And));
2969 }
2970
2971 #[test]
2972 fn test_decode_simd_splat() {
2973 let wat = r#"
2974 (module
2975 (func (export "splat") (param i32) (result v128)
2976 local.get 0
2977 i32x4.splat
2978 )
2979 )
2980 "#;
2981
2982 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2983 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2984
2985 assert_eq!(functions.len(), 1);
2986 assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
2987 }
2988
2989 #[test]
2990 fn test_decode_simd_extract_lane() {
2991 let wat = r#"
2992 (module
2993 (func (export "extract") (param v128) (result i32)
2994 local.get 0
2995 i32x4.extract_lane 2
2996 )
2997 )
2998 "#;
2999
3000 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
3001 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
3002
3003 assert_eq!(functions.len(), 1);
3004 assert!(
3005 functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
3006 "Should decode i32x4.extract_lane 2"
3007 );
3008 }
3009
3010 #[test]
3011 fn test_decode_simd_f32x4_arithmetic() {
3012 let wat = r#"
3013 (module
3014 (func (export "f32x4_add") (param v128 v128) (result v128)
3015 local.get 0
3016 local.get 1
3017 f32x4.add
3018 )
3019 )
3020 "#;
3021
3022 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
3023 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
3024
3025 assert_eq!(functions.len(), 1);
3026 assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
3027 }
3028
3029 #[test]
3030 fn test_369_scalar_float_op_flags_function_unsupported_not_dropped() {
3031 // GI-FPU-002 (#619): the in-scope scalar f32 ops (add/sub/mul/div,
3032 // comparisons, i32.trunc_f32_s/u, f32.convert_i32_s/u, f32.const) are
3033 // now DECODED (routed to the VFP selector on FPU targets), so `f32.add`
3034 // is no longer flagged — and since phase 2 (#369) so is the lowered
3035 // f64 subset (`f64.add` here; the m7dp-only capability gate lives in
3036 // the selector). Since phase 3 the f64 op TAIL (`f64.min` here) is
3037 // decoded too; and since #869 the 64-bit integer<->float conversion
3038 // family (`i64.trunc_f64_s` here) decodes as well — the scalar float
3039 // decode surface is complete, with capability gating (m7dp-only)
3040 // living in the selector. The flag-never-drop honesty contract for
3041 // the remaining undecoded float surface is pinned by the
3042 // float-global test below. A pure-integer function stays clean.
3043 let wat = r#"
3044 (module
3045 (func (export "fadd") (param f32 f32) (result f32)
3046 local.get 0 local.get 1 f32.add)
3047 (func (export "dadd") (param f64 f64) (result f64)
3048 local.get 0 local.get 1 f64.add)
3049 (func (export "dmin") (param f64 f64) (result f64)
3050 local.get 0 local.get 1 f64.min)
3051 (func (export "dtrunc64") (param f64) (result i64)
3052 local.get 0 i64.trunc_f64_s)
3053 (func (export "iadd") (param i32 i32) (result i32)
3054 local.get 0 local.get 1 i32.add))
3055 "#;
3056 let wasm = wat::parse_str(wat).expect("parse");
3057 let functions = decode_wasm_functions(&wasm).expect("decode");
3058 let fadd = functions
3059 .iter()
3060 .find(|f| f.export_name.as_deref() == Some("fadd"))
3061 .unwrap();
3062 let dadd = functions
3063 .iter()
3064 .find(|f| f.export_name.as_deref() == Some("dadd"))
3065 .unwrap();
3066 let dmin = functions
3067 .iter()
3068 .find(|f| f.export_name.as_deref() == Some("dmin"))
3069 .unwrap();
3070 let dtrunc64 = functions
3071 .iter()
3072 .find(|f| f.export_name.as_deref() == Some("dtrunc64"))
3073 .unwrap();
3074 let iadd = functions
3075 .iter()
3076 .find(|f| f.export_name.as_deref() == Some("iadd"))
3077 .unwrap();
3078 // In-scope f32 op: now decoded (reachable), not flagged.
3079 assert!(
3080 fadd.unsupported.is_none(),
3081 "GI-FPU-002: f32.add must now decode (not be flagged), got {:?}",
3082 fadd.unsupported
3083 );
3084 assert!(
3085 fadd.ops.contains(&WasmOp::F32Add),
3086 "f32.add must decode to WasmOp::F32Add: {:?}",
3087 fadd.ops
3088 );
3089 // In-scope f64 op (phase 2, #369): now decoded, not flagged.
3090 assert!(
3091 dadd.unsupported.is_none(),
3092 "GI-FPU-002 phase 2: f64.add must now decode (not be flagged), got {:?}",
3093 dadd.unsupported
3094 );
3095 assert!(
3096 dadd.ops.contains(&WasmOp::F64Add),
3097 "f64.add must decode to WasmOp::F64Add: {:?}",
3098 dadd.ops
3099 );
3100 // In-scope f64 tail op (phase 3, #369): now decoded, not flagged.
3101 assert!(
3102 dmin.unsupported.is_none(),
3103 "GI-FPU-002 phase 3: f64.min must now decode (not be flagged), got {:?}",
3104 dmin.unsupported
3105 );
3106 assert!(
3107 dmin.ops.contains(&WasmOp::F64Min),
3108 "f64.min must decode to WasmOp::F64Min: {:?}",
3109 dmin.ops
3110 );
3111 // #869: the i64<->f64 conversions are now IN scope — decoded, not
3112 // flagged (the m7dp capability gate lives in the selector preamble).
3113 assert!(
3114 dtrunc64.unsupported.is_none(),
3115 "#869: i64.trunc_f64_s must now decode (not be flagged), got {:?}",
3116 dtrunc64.unsupported
3117 );
3118 assert!(
3119 dtrunc64.ops.contains(&WasmOp::I64TruncF64S),
3120 "i64.trunc_f64_s must decode to WasmOp::I64TruncF64S: {:?}",
3121 dtrunc64.ops
3122 );
3123 assert!(
3124 iadd.unsupported.is_none(),
3125 "a pure-integer function must NOT be flagged: {:?}",
3126 iadd.unsupported
3127 );
3128 }
3129
3130 #[test]
3131 fn test_369_float_global_access_flags_function_unsupported() {
3132 // GI-FPU-001 (#369): `global.get`/`global.set` on an f32/f64-typed
3133 // global decode fine (the ops are type-agnostic), but the float
3134 // initializer is dropped (`init_i32: None` -> slot zeroed), so a read
3135 // returned a silently-wrong 0.0 instead of the init (verified: the
3136 // 2.5f bit pattern 0x40200000 was absent from the output ELF). The
3137 // access must flag the function for the loud-skip path. Accesses to
3138 // integer globals stay clean.
3139 let wat = r#"
3140 (module
3141 (global $fg f32 (f32.const 2.5))
3142 (global $dg (mut f64) (f64.const 1.5))
3143 (global $ig (mut i32) (i32.const 7))
3144 (func (export "fget") (result f32) global.get $fg)
3145 (func (export "dset") (param f64) local.get 0 global.set $dg)
3146 (func (export "iget") (result i32) global.get $ig))
3147 "#;
3148 let wasm = wat::parse_str(wat).expect("parse");
3149
3150 // Both decode entry points must flag (the CLI compiles through both:
3151 // decode_wasm_module on the all-exports/module paths,
3152 // decode_wasm_functions on the single-function path).
3153 let module = decode_wasm_module(&wasm).expect("decode module");
3154 for functions in [
3155 &module.functions,
3156 &decode_wasm_functions(&wasm).expect("decode fns"),
3157 ] {
3158 let by_name = |n: &str| {
3159 functions
3160 .iter()
3161 .find(|f| f.export_name.as_deref() == Some(n))
3162 .unwrap()
3163 };
3164 let fget = by_name("fget");
3165 assert!(
3166 fget.unsupported.is_some(),
3167 "global.get of an f32 global must flag the function (loud-skip), got {:?}",
3168 fget.unsupported
3169 );
3170 let reason = fget.unsupported.as_deref().unwrap();
3171 assert!(
3172 reason.contains("GlobalGet") && reason.contains("GI-FPU-001"),
3173 "diagnostic should name the op and GI-FPU-001: {reason:?}"
3174 );
3175 let dset = by_name("dset");
3176 assert!(
3177 dset.unsupported
3178 .as_deref()
3179 .is_some_and(|r| r.contains("GlobalSet")),
3180 "global.set of an f64 global must flag the function, got {:?}",
3181 dset.unsupported
3182 );
3183 assert!(
3184 by_name("iget").unsupported.is_none(),
3185 "an i32 global access must NOT be flagged: {:?}",
3186 by_name("iget").unsupported
3187 );
3188 }
3189 }
3190
3191 #[test]
3192 fn test_369_imported_float_global_shifts_index_space() {
3193 // GI-FPU-001 (#369): imported globals come FIRST in the global index
3194 // space. An imported f64 global at index 0 must be flagged, and the
3195 // defined i32 global at index 1 must NOT be mistaken for it.
3196 let wat = r#"
3197 (module
3198 (import "env" "fg" (global f64))
3199 (global $ig i32 (i32.const 3))
3200 (func (export "fget") (result f64) global.get 0)
3201 (func (export "iget") (result i32) global.get 1))
3202 "#;
3203 let wasm = wat::parse_str(wat).expect("parse");
3204 let functions = decode_wasm_functions(&wasm).expect("decode");
3205 let by_name = |n: &str| {
3206 functions
3207 .iter()
3208 .find(|f| f.export_name.as_deref() == Some(n))
3209 .unwrap()
3210 };
3211 assert!(
3212 by_name("fget")
3213 .unsupported
3214 .as_deref()
3215 .is_some_and(|r| r.contains("GI-FPU-001")),
3216 "imported f64 global access must flag: {:?}",
3217 by_name("fget").unsupported
3218 );
3219 assert!(
3220 by_name("iget").unsupported.is_none(),
3221 "defined i32 global at shifted index 1 must NOT flag: {:?}",
3222 by_name("iget").unsupported
3223 );
3224 }
3225
3226 #[test]
3227 fn test_680_simd_ops_flag_function_unsupported_not_dropped() {
3228 // #680: SIMD (v128) ops decode into WasmOp variants no production
3229 // target can select (`has_helium` is test-only), so they were silently
3230 // dropped at selection — `i32x4.add` compiled to an operand
3231 // passthrough (`mov r0,r1`) and shipped a wrong result. The issue's
3232 // exact reproducer must flag the function; the scalar sibling must
3233 // stay compilable (non-vacuity).
3234 let wat = r#"
3235 (module
3236 (memory 1)
3237 (func (export "vadd") (param i32 i32) (result i32)
3238 (i32x4.extract_lane 2
3239 (i32x4.add (i32x4.splat (local.get 0))
3240 (i32x4.splat (local.get 1)))))
3241 (func (export "vstore") (param i32 i32) (result i32)
3242 (v128.store (i32.const 0) (i32x4.splat (local.get 0)))
3243 (i32.load (i32.const 0)))
3244 (func (export "iadd") (param i32 i32) (result i32)
3245 local.get 0 local.get 1 i32.add))
3246 "#;
3247 let wasm = wat::parse_str(wat).expect("parse");
3248
3249 // Both decode entry points must flag (the CLI compiles through both).
3250 let module = decode_wasm_module(&wasm).expect("decode module");
3251 for functions in [
3252 &module.functions,
3253 &decode_wasm_functions(&wasm).expect("decode fns"),
3254 ] {
3255 let by_name = |n: &str| {
3256 functions
3257 .iter()
3258 .find(|f| f.export_name.as_deref() == Some(n))
3259 .unwrap()
3260 };
3261 for name in ["vadd", "vstore"] {
3262 let reason = by_name(name).unsupported.as_deref();
3263 assert!(
3264 reason.is_some(),
3265 "{name}: v128 ops must flag the function (loud-skip), got None"
3266 );
3267 let reason = reason.unwrap();
3268 assert!(
3269 reason.contains("no SIMD lowering for this target") && reason.contains("#680"),
3270 "{name}: diagnostic must name the target gap and #680: {reason:?}"
3271 );
3272 }
3273 // The reason names the FIRST SIMD op hit (splat in both bodies).
3274 assert!(
3275 by_name("vadd")
3276 .unsupported
3277 .as_deref()
3278 .unwrap()
3279 .contains("I32x4Splat"),
3280 "diagnostic should name the op: {:?}",
3281 by_name("vadd").unsupported
3282 );
3283 assert!(
3284 by_name("iadd").unsupported.is_none(),
3285 "a scalar function in the same module must NOT be flagged: {:?}",
3286 by_name("iadd").unsupported
3287 );
3288 }
3289 }
3290
3291 #[test]
3292 fn test_680_v128_local_and_signature_flag_function() {
3293 // #680: v128 VALUES are expressible with ZERO SIMD-proposal operators
3294 // in the body — a v128-typed local or a v128 param/result is reached
3295 // through type-agnostic `local.get`/`local.set`, which the selectors
3296 // lower as 4-byte moves (silent 16-byte truncation). Both must flag.
3297 let wat = r#"
3298 (module
3299 (func (export "vlocal") (result i32) (local v128)
3300 i32.const 7)
3301 (func (export "vpass") (param v128) (result v128)
3302 local.get 0)
3303 (func (export "scalar") (param i32) (result i32)
3304 local.get 0))
3305 "#;
3306 let wasm = wat::parse_str(wat).expect("parse");
3307 let module = decode_wasm_module(&wasm).expect("decode module");
3308 for functions in [
3309 &module.functions,
3310 &decode_wasm_functions(&wasm).expect("decode fns"),
3311 ] {
3312 let by_name = |n: &str| {
3313 functions
3314 .iter()
3315 .find(|f| f.export_name.as_deref() == Some(n))
3316 .unwrap()
3317 };
3318 assert!(
3319 by_name("vlocal")
3320 .unsupported
3321 .as_deref()
3322 .is_some_and(|r| r.contains("v128-typed local") && r.contains("#680")),
3323 "a v128-typed local declaration must flag: {:?}",
3324 by_name("vlocal").unsupported
3325 );
3326 assert!(
3327 by_name("vpass")
3328 .unsupported
3329 .as_deref()
3330 .is_some_and(|r| r.contains("v128 param/result") && r.contains("#680")),
3331 "a v128 param/result signature must flag (op-free body!): {:?}",
3332 by_name("vpass").unsupported
3333 );
3334 assert!(
3335 by_name("scalar").unsupported.is_none(),
3336 "a scalar function must NOT be flagged: {:?}",
3337 by_name("scalar").unsupported
3338 );
3339 }
3340 }
3341
3342 #[test]
3343 fn test_680_v128_global_access_flags_function() {
3344 // #680: `global.get`/`global.set` on a v128-typed global decode fine
3345 // (type-agnostic ops), but the access would move 4 of the 16 bytes and
3346 // the `v128.const` initializer is never captured. Same lane as the
3347 // float globals (#648/GI-FPU-001); imported globals shift the index
3348 // space (imports first). The i32-global sibling stays compilable.
3349 let wat = r#"
3350 (module
3351 (import "env" "vg" (global v128))
3352 (global $ig (mut i32) (i32.const 7))
3353 (global $dg (mut v128) (v128.const i32x4 1 2 3 4))
3354 (func (export "vget") global.get 0 drop)
3355 (func (export "iget") (result i32) global.get $ig))
3356 "#;
3357 let wasm = wat::parse_str(wat).expect("parse");
3358 let module = decode_wasm_module(&wasm).expect("decode module");
3359 for functions in [
3360 &module.functions,
3361 &decode_wasm_functions(&wasm).expect("decode fns"),
3362 ] {
3363 let by_name = |n: &str| {
3364 functions
3365 .iter()
3366 .find(|f| f.export_name.as_deref() == Some(n))
3367 .unwrap()
3368 };
3369 let reason = by_name("vget").unsupported.as_deref();
3370 assert!(
3371 reason.is_some_and(|r| r.contains("GlobalGet")
3372 && r.contains("v128-typed global")
3373 && r.contains("#680")),
3374 "global.get of an imported v128 global must flag: {reason:?}"
3375 );
3376 assert!(
3377 by_name("iget").unsupported.is_none(),
3378 "an i32 global access must NOT be flagged: {:?}",
3379 by_name("iget").unsupported
3380 );
3381 }
3382 }
3383
3384 #[test]
3385 fn test_decode_simd_multiple_ops() {
3386 let wat = r#"
3387 (module
3388 (func (export "simd_ops") (param v128 v128 v128) (result v128)
3389 ;; (a + b) * c
3390 local.get 0
3391 local.get 1
3392 i32x4.add
3393 local.get 2
3394 i32x4.mul
3395 )
3396 )
3397 "#;
3398
3399 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
3400 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
3401
3402 assert_eq!(functions.len(), 1);
3403 let ops = &functions[0].ops;
3404 assert!(ops.contains(&WasmOp::I32x4Add));
3405 assert!(ops.contains(&WasmOp::I32x4Mul));
3406 }
3407
3408 /// VCR-DBG-001 step 1 (#394): the decoder records a module-relative wasm byte
3409 /// offset per emitted op — the DWARF-for-wasm address space that bridges
3410 /// synth's op-index `source_line` to the input wasm's `.debug_line`. Purely
3411 /// additive metadata (no codegen consumer ⇒ frozen fixtures byte-identical,
3412 /// verified separately); this test pins the structural invariants.
3413 #[test]
3414 fn test_decode_records_aligned_increasing_op_offsets_dbg001() {
3415 let wat = r#"
3416 (module
3417 (func (export "f") (param i32 i32) (result i32)
3418 local.get 0
3419 local.get 1
3420 i32.add
3421 i32.const 7
3422 i32.mul))
3423 "#;
3424 let wasm = wat::parse_str(wat).expect("parse WAT");
3425 let functions = decode_wasm_functions(&wasm).expect("decode");
3426 let f = &functions[0];
3427
3428 // One offset per emitted op, index-aligned with `ops`.
3429 assert_eq!(
3430 f.op_offsets.len(),
3431 f.ops.len(),
3432 "op_offsets must be parallel to ops"
3433 );
3434 assert!(!f.op_offsets.is_empty());
3435
3436 // Byte offsets are strictly increasing through the body (each op consumes
3437 // at least one byte) and module-relative (well past the header).
3438 assert!(
3439 f.op_offsets.windows(2).all(|w| w[1] > w[0]),
3440 "wasm byte offsets must strictly increase: {:?}",
3441 f.op_offsets
3442 );
3443 assert!(
3444 f.op_offsets[0] >= 8,
3445 "module-relative offset is past the 8-byte wasm header"
3446 );
3447 }
3448
3449 /// #237: the decoder captures a global's `i32.const` initializer + mutability,
3450 /// so the native-pointer ABI can recognize the stack-pointer global.
3451 #[test]
3452 fn test_decode_captures_global_initializer() {
3453 let wat = r#"
3454 (module
3455 (memory 2)
3456 (global $__stack_pointer (mut i32) (i32.const 65536))
3457 (global $immutable_const i32 (i32.const 7))
3458 (func (export "f") (result i32) global.get 0)
3459 )
3460 "#;
3461 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3462 let module = decode_wasm_module(&wasm).expect("Failed to decode");
3463
3464 assert_eq!(module.globals.len(), 2, "both globals captured");
3465 let sp = &module.globals[0];
3466 assert_eq!(sp.index, 0);
3467 assert_eq!(
3468 sp.init,
3469 Some(GlobalInit::I32(65536)),
3470 "stack-pointer init captured"
3471 );
3472 assert!(sp.mutable, "stack pointer is mutable");
3473 let c = &module.globals[1];
3474 assert_eq!(c.init, Some(GlobalInit::I32(7)));
3475 assert!(!c.mutable, "second global is immutable");
3476 assert_eq!(sp.slot_bytes, 4, "i32 global occupies one 4-byte slot");
3477 assert_eq!(c.slot_bytes, 4);
3478 }
3479
3480 /// #643: the decoder records the DECLARED slot width per global — an i64
3481 /// (or f64) global occupies 8 bytes, so the globals-table layout can give
3482 /// it room for both words and shift every later global's offset.
3483 #[test]
3484 fn test_decode_records_global_slot_widths_643() {
3485 let wat = r#"
3486 (module
3487 (global $c (mut i64) (i64.const 0))
3488 (global $k (mut i32) (i32.const 0))
3489 (global $f (mut f64) (f64.const 0))
3490 (func (export "f") (result i32) global.get 1)
3491 )
3492 "#;
3493 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3494 let module = decode_wasm_module(&wasm).expect("Failed to decode");
3495
3496 assert_eq!(module.globals.len(), 3);
3497 assert_eq!(module.globals[0].slot_bytes, 8, "i64 global is 8 bytes");
3498 assert_eq!(module.globals[1].slot_bytes, 4, "i32 global is 4 bytes");
3499 assert_eq!(module.globals[2].slot_bytes, 8, "f64 global is 8 bytes");
3500 }
3501
3502 /// #649: a nonzero `i64.const` initializer is captured as BOTH words — the
3503 /// `init_i32`-shaped capture dropped it to `None` and every consumer's
3504 /// `unwrap_or(0)` silently ZEROED the global. f32/f64 inits stay `None`
3505 /// (GI-FPU-001/#369 loud-skip lane — never fabricate a float bit-pattern).
3506 #[test]
3507 fn test_decode_captures_i64_global_initializer_649() {
3508 let wat = r#"
3509 (module
3510 (global $g (mut i64) (i64.const 0x123456789ABCDEF0))
3511 (global $n (mut i64) (i64.const -1))
3512 (global $f (mut f64) (f64.const 1.5))
3513 (global $h (mut f32) (f32.const 2.5))
3514 (func (export "f") (result i32) i32.const 0)
3515 )
3516 "#;
3517 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
3518 let module = decode_wasm_module(&wasm).expect("Failed to decode");
3519
3520 assert_eq!(module.globals.len(), 4);
3521 assert_eq!(
3522 module.globals[0].init,
3523 Some(GlobalInit::I64(0x123456789ABCDEF0u64 as i64)),
3524 "nonzero i64 init captured with both words"
3525 );
3526 assert_eq!(module.globals[1].init, Some(GlobalInit::I64(-1)));
3527 assert_eq!(
3528 module.globals[2].init, None,
3529 "f64 init is NOT captured (GI-FPU-001 loud-skip lane)"
3530 );
3531 assert_eq!(
3532 module.globals[3].init, None,
3533 "f32 init is NOT captured (GI-FPU-001 loud-skip lane)"
3534 );
3535 }
3536
3537 /// #509: the decoder records `(param_count, result_count)` for every
3538 /// `Block`/`Loop`/`If`, ordinal-keyed in op order, covering all three
3539 /// blocktype encodings: `Empty → (0,0)`, `ValType → (0,1)`, and
3540 /// `FuncType(i) →` counts from the type section (here a multi-result
3541 /// block, which wat encodes as a functype blocktype).
3542 #[test]
3543 fn test_decode_records_block_arity_side_table_509() {
3544 let wat = r#"
3545 (module
3546 (func (export "f") (param i32) (result i32)
3547 (block (result i32)
3548 (block (nop))
3549 (local.get 0)
3550 (if (result i32)
3551 (then (i32.const 1))
3552 (else (i32.const 2)))))
3553 (func (export "g") (result i32)
3554 (block (result i32 i32)
3555 (i32.const 1) (i32.const 2))
3556 i32.add)
3557 (func (export "h") (param i32) (result i32)
3558 (local.get 0)
3559 (loop (param i32) (result i32))))
3560 "#;
3561 let wasm = wat::parse_str(wat).expect("parse WAT");
3562
3563 // Both decode entry points must produce the same side-table.
3564 for functions in [
3565 decode_wasm_functions(&wasm).expect("decode"),
3566 decode_wasm_module(&wasm).expect("decode").functions,
3567 ] {
3568 // f: Block(result i32), Block(void), If(result i32) — in op order.
3569 assert_eq!(
3570 functions[0].block_arity,
3571 vec![(0, 1), (0, 0), (0, 1)],
3572 "f: ValType/Empty/ValType blocktypes"
3573 );
3574 // g: one multi-result block via a FuncType blocktype.
3575 assert_eq!(
3576 functions[1].block_arity,
3577 vec![(0, 2)],
3578 "g: functype blocktype result count from the type section"
3579 );
3580 // h: a parameterized loop — the input arity is what a br to the
3581 // header would carry (the #509 loud-decline discriminator).
3582 assert_eq!(
3583 functions[2].block_arity,
3584 vec![(1, 1)],
3585 "h: loop params captured"
3586 );
3587 }
3588 }
3589
3590 /// #642: the decoder captures table 0's compile-time size, per-segment
3591 /// element shapes and per-function type indices, and the closed-world
3592 /// verdict VERIFIES a fully-covered homogeneous table.
3593 #[test]
3594 fn test_call_indirect_guards_closed_world_verified_642() {
3595 // The #642 repro shape: 3-entry table, fully covered, one signature.
3596 let wat = r#"
3597 (module
3598 (type $bin (func (param i32 i32) (result i32)))
3599 (table 3 funcref)
3600 (elem (i32.const 0) $add $sub $mul)
3601 (func $add (param i32 i32) (result i32)
3602 (i32.add (local.get 0) (local.get 1)))
3603 (func $sub (param i32 i32) (result i32)
3604 (i32.sub (local.get 0) (local.get 1)))
3605 (func $mul (param i32 i32) (result i32)
3606 (i32.mul (local.get 0) (local.get 1)))
3607 (func (export "f") (param i32 i32) (result i32)
3608 (call_indirect (type $bin)
3609 (local.get 0) (i32.const 10) (local.get 1)))
3610 )
3611 "#;
3612 let wasm = wat::parse_str(wat).expect("parse");
3613 let module = decode_wasm_module(&wasm).expect("decode");
3614
3615 assert_eq!(module.table_size, Some(3), "table section min size");
3616 assert_eq!(module.table_sizes, vec![Some(3)], "#650 per-table sizes");
3617 assert_eq!(
3618 module.elem_segments,
3619 vec![ElemSegmentInfo {
3620 table_index: 0,
3621 offset: Some(0),
3622 funcs: Some(vec![0, 1, 2]),
3623 }]
3624 );
3625 // 2 type-section entries ($bin + the export's (i32 i32)->i32 dedups
3626 // to one in practice, but don't assume — just check func 0..2 share
3627 // a signature with type 0).
3628 assert_eq!(module.func_type_indices.len(), 4);
3629
3630 let guards = module.call_indirect_guards();
3631 assert_eq!(guards.tables.len(), 1);
3632 assert_eq!(guards.tables[0].table_size, Some(3));
3633 assert_eq!(
3634 guards.tables[0].base_byte_offset,
3635 Some(0),
3636 "#650: a single-table module keeps table 0 at R11 offset 0 by construction"
3637 );
3638 // Type index 0 ($bin) must be VERIFIED: every table entry has its
3639 // exact signature.
3640 assert_eq!(
3641 guards.tables[0].type_reject.first(),
3642 Some(&None),
3643 "closed-world type check must verify the homogeneous table: {:?}",
3644 guards.tables[0].type_reject
3645 );
3646 assert!(
3647 !guards.tables[0].has_null_slots,
3648 "#664: a fully-initialized table must NOT request the runtime \
3649 null check (dispatch bytes stay identical by construction)"
3650 );
3651 }
3652
3653 /// #642: a heterogeneous table (an entry whose signature differs from the
3654 /// expected type) must REJECT that expected type — the raw code-pointer
3655 /// table cannot be runtime-type-checked, so the lowering has to decline.
3656 #[test]
3657 fn test_call_indirect_guards_heterogeneous_table_rejects_642() {
3658 let wat = r#"
3659 (module
3660 (type $bin (func (param i32 i32) (result i32)))
3661 (type $un (func (param i32) (result i32)))
3662 (table 2 funcref)
3663 (elem (i32.const 0) $add $neg)
3664 (func $add (type $bin)
3665 (i32.add (local.get 0) (local.get 1)))
3666 (func $neg (type $un)
3667 (i32.sub (i32.const 0) (local.get 0)))
3668 (func (export "f") (param i32 i32) (result i32)
3669 (call_indirect (type $bin)
3670 (local.get 0) (i32.const 10) (local.get 1)))
3671 )
3672 "#;
3673 let wasm = wat::parse_str(wat).expect("parse");
3674 let module = decode_wasm_module(&wasm).expect("decode");
3675 let guards = module.call_indirect_guards();
3676 assert_eq!(guards.tables[0].table_size, Some(2));
3677 // BOTH expected types must be rejected: the table holds one function
3678 // of each signature, so neither type's closed world holds.
3679 assert!(
3680 guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3681 "heterogeneous table must reject every expected type: {:?}",
3682 guards.tables[0].type_reject
3683 );
3684 // #676: ... but the image is statically known, so the mismatch trap
3685 // is dischargeable at RUNTIME via the type-id sidecar.
3686 assert!(
3687 guards.tables[0].runtime_type_check,
3688 "heterogeneous-but-known table must offer the runtime check (#676)"
3689 );
3690 assert_eq!(
3691 guards.type_ids_byte_offset,
3692 Some(8),
3693 "sidecar sits after the 2-slot pointer region"
3694 );
3695 assert_eq!(
3696 guards.type_ids_image,
3697 vec![1, 2],
3698 "slot 0 = $bin (class 1), slot 1 = $un (class 2)"
3699 );
3700 assert_eq!(guards.type_class_ids, vec![1, 2]);
3701 }
3702
3703 /// #664 (relaxes the #642 all-reject): an uninitialized table slot (elem
3704 /// covers less than the declared size) is a null funcref — calling it
3705 /// must trap, which is now discharged at RUNTIME (null check on the
3706 /// zero-linked pointer), so the closed-world verdict verifies the
3707 /// INITIALIZED slots and sets `has_null_slots` for the lowering.
3708 #[test]
3709 fn test_call_indirect_guards_null_slot_verifies_with_flag_664() {
3710 let wat = r#"
3711 (module
3712 (type $s (func (result i32)))
3713 (table 3 funcref)
3714 (elem (i32.const 0) $f0 $f1)
3715 (func $f0 (result i32) (i32.const 10))
3716 (func $f1 (result i32) (i32.const 11))
3717 (func (export "run") (param i32) (result i32)
3718 (call_indirect (type $s) (local.get 0)))
3719 )
3720 "#;
3721 let wasm = wat::parse_str(wat).expect("parse");
3722 let module = decode_wasm_module(&wasm).expect("decode");
3723 let guards = module.call_indirect_guards();
3724 assert_eq!(guards.tables[0].table_size, Some(3));
3725 assert_eq!(
3726 guards.tables[0].type_reject.first(),
3727 Some(&None),
3728 "initialized slots are homogeneous in $s — the verdict must \
3729 verify despite the null slot (#664): {:?}",
3730 guards.tables[0].type_reject
3731 );
3732 assert!(
3733 guards.tables[0].has_null_slots,
3734 "slot 2 is uninitialized — the lowering must emit the runtime \
3735 null check (#664)"
3736 );
3737 }
3738
3739 /// #664: the falcon shape — a SPARSE table (only slots 1 and 3 of 4
3740 /// initialized, by two separate segments) verifies with the null flag;
3741 /// a sparse table whose INITIALIZED slots are heterogeneous still
3742 /// rejects (the runtime null check cannot discharge a TYPE mismatch).
3743 #[test]
3744 fn test_call_indirect_guards_sparse_table_664() {
3745 let wat = r#"
3746 (module
3747 (type $t (func (param i32) (result i32)))
3748 (table 4 4 funcref)
3749 (func $f1 (type $t) (i32.add (local.get 0) (i32.const 100)))
3750 (func $f3 (type $t) (i32.sub (i32.const 1000) (local.get 0)))
3751 (elem (i32.const 1) $f1)
3752 (elem (i32.const 3) $f3)
3753 (func (export "via") (param i32 i32) (result i32)
3754 (call_indirect (type $t) (local.get 0) (local.get 1)))
3755 )
3756 "#;
3757 let wasm = wat::parse_str(wat).expect("parse");
3758 let module = decode_wasm_module(&wasm).expect("decode");
3759 let guards = module.call_indirect_guards();
3760 assert_eq!(guards.tables[0].table_size, Some(4));
3761 assert_eq!(
3762 guards.tables[0].type_reject.first(),
3763 Some(&None),
3764 "slots 1,3 are homogeneous in $t — verified: {:?}",
3765 guards.tables[0].type_reject
3766 );
3767 assert!(guards.tables[0].has_null_slots, "slots 0,2 are null");
3768
3769 // Heterogeneous INITIALIZED slots in a sparse table: still rejected.
3770 let wat = r#"
3771 (module
3772 (type $t (func (param i32) (result i32)))
3773 (type $u (func (param i32 i32) (result i32)))
3774 (table 4 4 funcref)
3775 (func $f1 (type $t) (local.get 0))
3776 (func $f3 (type $u) (i32.add (local.get 0) (local.get 1)))
3777 (elem (i32.const 1) $f1)
3778 (elem (i32.const 3) $f3)
3779 (func (export "via") (param i32 i32) (result i32)
3780 (call_indirect (type $t) (local.get 0) (local.get 1)))
3781 )
3782 "#;
3783 let wasm = wat::parse_str(wat).expect("parse");
3784 let module = decode_wasm_module(&wasm).expect("decode");
3785 let guards = module.call_indirect_guards();
3786 assert!(
3787 guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3788 "a heterogeneous sparse table must still reject every type: {:?}",
3789 guards.tables[0].type_reject
3790 );
3791 // #676: the sparse-heterogeneous case is now dischargeable at
3792 // runtime too — null slots take the reserved class id 0, so ONE
3793 // sidecar compare covers both the type mismatch and the null trap.
3794 assert!(guards.tables[0].runtime_type_check, "#676 runtime check");
3795 assert_eq!(guards.type_ids_byte_offset, Some(16), "4 pointer slots");
3796 assert_eq!(
3797 guards.type_ids_image,
3798 vec![0, 1, 0, 2],
3799 "nulls at 0/2 carry the reserved id 0; $t slot 1 = class 1, \
3800 $u slot 3 = class 2"
3801 );
3802 }
3803
3804 /// #676: the heterogeneous type-id sidecar — structural duplicate types
3805 /// share one class id (the meld 31-decls/25-distinct shape), null slots
3806 /// take the reserved id 0, and the sidecar base is the total pointer
3807 /// region size. A module with NO heterogeneous table gets NO sidecar
3808 /// (empty image, `None` offset) — homogeneous modules stay untouched.
3809 #[test]
3810 fn test_call_indirect_guards_heterogeneous_sidecar_676() {
3811 let wat = r#"
3812 (module
3813 (type $bin (func (param i32 i32) (result i32)))
3814 (type $un (func (param i32) (result i32)))
3815 (type $bin2 (func (param i32 i32) (result i32)))
3816 (table 5 5 funcref)
3817 (func $add (type $bin) (i32.add (local.get 0) (local.get 1)))
3818 (func $neg (type $un) (i32.sub (i32.const 0) (local.get 0)))
3819 (func $sub (type $bin2) (i32.sub (local.get 0) (local.get 1)))
3820 (elem (i32.const 0) func $add $neg $sub)
3821 (func (export "via2") (param i32 i32) (result i32)
3822 (call_indirect (type $bin)
3823 (local.get 0) (i32.const 3) (local.get 1)))
3824 (func (export "via1") (param i32 i32) (result i32)
3825 (call_indirect (type $un) (local.get 0) (local.get 1)))
3826 )
3827 "#;
3828 let wasm = wat::parse_str(wat).expect("parse");
3829 let module = decode_wasm_module(&wasm).expect("decode");
3830 let guards = module.call_indirect_guards();
3831 assert!(guards.tables[0].runtime_type_check);
3832 assert_eq!(
3833 guards.type_class_ids,
3834 vec![1, 2, 1],
3835 "$bin2 is a structural duplicate of $bin — one class id (#676)"
3836 );
3837 assert_eq!(
3838 guards.type_ids_image,
3839 vec![1, 2, 1, 0, 0],
3840 "slots: $add(bin)=1, $neg(un)=2, $sub(bin2 ≡ bin)=1, null, null"
3841 );
3842 assert_eq!(
3843 guards.type_ids_byte_offset,
3844 Some(20),
3845 "sidecar starts after the 5 pointer words"
3846 );
3847
3848 // Homogeneous module → NO sidecar, no runtime check anywhere.
3849 let wat = r#"
3850 (module
3851 (type $t (func (param i32) (result i32)))
3852 (table 2 2 funcref)
3853 (func $f0 (type $t) (local.get 0))
3854 (func $f1 (type $t) (i32.const 7))
3855 (elem (i32.const 0) func $f0 $f1)
3856 (func (export "via") (param i32 i32) (result i32)
3857 (call_indirect (type $t) (local.get 0) (local.get 1)))
3858 )
3859 "#;
3860 let wasm = wat::parse_str(wat).expect("parse");
3861 let module = decode_wasm_module(&wasm).expect("decode");
3862 let guards = module.call_indirect_guards();
3863 assert!(!guards.tables[0].runtime_type_check);
3864 assert_eq!(guards.type_ids_byte_offset, None, "no heterogeneous table");
3865 assert!(guards.type_ids_image.is_empty());
3866 assert!(guards.type_class_ids.is_empty());
3867 }
3868
3869 /// #642: no table at all → no compile-time bound → table_size None and
3870 /// every type rejected (the lowering declines).
3871 #[test]
3872 fn test_call_indirect_guards_no_table_642() {
3873 let wat = r#"
3874 (module
3875 (func (export "f") (param i32) (result i32) (local.get 0))
3876 )
3877 "#;
3878 let wasm = wat::parse_str(wat).expect("parse");
3879 let module = decode_wasm_module(&wasm).expect("decode");
3880 assert_eq!(module.table_size, None);
3881 assert!(module.table_sizes.is_empty(), "#650: no tables declared");
3882 let guards = module.call_indirect_guards();
3883 assert!(
3884 guards.tables.is_empty(),
3885 "no table → no guard entry → every call_indirect declines"
3886 );
3887 }
3888
3889 /// #642: duplicate-but-structurally-identical types stay interchangeable —
3890 /// the closed-world check compares SIGNATURES, not type indices.
3891 #[test]
3892 fn test_call_indirect_guards_duplicate_types_verified_642() {
3893 let wat = r#"
3894 (module
3895 (type $a (func (result i32)))
3896 (type $b (func (result i32)))
3897 (table 1 funcref)
3898 (elem (i32.const 0) $f)
3899 (func $f (type $a) (i32.const 7))
3900 (func (export "run") (param i32) (result i32)
3901 (call_indirect (type $b) (local.get 0)))
3902 )
3903 "#;
3904 let wasm = wat::parse_str(wat).expect("parse");
3905 let module = decode_wasm_module(&wasm).expect("decode");
3906 let guards = module.call_indirect_guards();
3907 // $f has type $a; the call expects $b — structurally identical, so
3908 // BOTH type indices must verify. (A third type — the export's
3909 // (i32)->i32 — is correctly rejected: different signature.)
3910 assert_eq!(
3911 &guards.tables[0].type_reject[0..2],
3912 &[None, None],
3913 "structural signature comparison must accept duplicate types: {:?}",
3914 guards.tables[0].type_reject
3915 );
3916 assert!(
3917 guards.tables[0].type_reject[2].is_some(),
3918 "the structurally-different third type must still be rejected"
3919 );
3920 }
3921
3922 /// #650: TWO tables become a contiguous R11 region — table 0 at offset 0
3923 /// (byte-identical single-table degeneration), table 1 at
3924 /// `size(table 0) * 4`. Each table gets its OWN size, base offset, and
3925 /// per-type closed-world verdicts (segments only poison the table they
3926 /// target).
3927 #[test]
3928 fn test_call_indirect_guards_multi_table_650() {
3929 // The #650 repro shape: overlapping indices, distinct functions —
3930 // table0[1] != table1[1] (the aliasing canary).
3931 let wat = r#"
3932 (module
3933 (type $t (func (param i32) (result i32)))
3934 (type $u (func (param i32 i32) (result i32)))
3935 (table $t0 3 3 funcref)
3936 (table $t1 2 2 funcref)
3937 (func $a0 (type $t) (i32.add (local.get 0) (i32.const 100)))
3938 (func $a1 (type $t) (i32.add (local.get 0) (i32.const 200)))
3939 (func $a2 (type $t) (i32.add (local.get 0) (i32.const 300)))
3940 (func $b0 (type $u) (i32.add (local.get 0) (local.get 1)))
3941 (func $b1 (type $u) (i32.sub (local.get 0) (local.get 1)))
3942 (elem (table $t0) (i32.const 0) func $a0 $a1 $a2)
3943 (elem (table $t1) (i32.const 0) func $b0 $b1)
3944 (func (export "f") (param i32 i32) (result i32)
3945 (call_indirect $t1 (type $u)
3946 (local.get 0) (i32.const 10) (local.get 1)))
3947 )
3948 "#;
3949 let wasm = wat::parse_str(wat).expect("parse");
3950 let module = decode_wasm_module(&wasm).expect("decode");
3951 assert_eq!(module.table_sizes, vec![Some(3), Some(2)]);
3952 assert_eq!(module.table_size, Some(3), "compat accessor = table 0");
3953 assert_eq!(
3954 module.elem_segments[0].table_index, 0,
3955 "segment 0 targets table 0"
3956 );
3957 assert_eq!(
3958 module.elem_segments[1],
3959 ElemSegmentInfo {
3960 table_index: 1,
3961 offset: Some(0),
3962 funcs: Some(vec![3, 4]),
3963 },
3964 "segment 1 is statically attributed to table 1 (#650)"
3965 );
3966
3967 let guards = module.call_indirect_guards();
3968 assert_eq!(guards.tables.len(), 2);
3969 assert_eq!(guards.tables[0].table_size, Some(3));
3970 assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3971 assert_eq!(guards.tables[1].table_size, Some(2));
3972 assert_eq!(
3973 guards.tables[1].base_byte_offset,
3974 Some(12),
3975 "table 1 base = size(table 0) * 4 within the contiguous R11 region"
3976 );
3977 // Table 0 is homogeneous in $t (type 0); table 1 in $u (type 1) —
3978 // each verifies ITS type and rejects the other's.
3979 assert_eq!(guards.tables[0].type_reject[0], None, "table 0 vs $t");
3980 assert!(guards.tables[0].type_reject[1].is_some(), "table 0 vs $u");
3981 assert!(guards.tables[1].type_reject[0].is_some(), "table 1 vs $t");
3982 assert_eq!(guards.tables[1].type_reject[1], None, "table 1 vs $u");
3983 }
3984
3985 /// #650: an unknown-size table (growable import) declines ITSELF and
3986 /// makes every LATER table's base offset non-constant — but a table
3987 /// BEFORE it is unaffected.
3988 #[test]
3989 fn test_call_indirect_guards_unknown_size_poisons_later_bases_650() {
3990 let wat = r#"
3991 (module
3992 (type $t (func (result i32)))
3993 (import "env" "tbl" (table 4 funcref))
3994 (table $d 1 1 funcref)
3995 (func $f (type $t) (i32.const 7))
3996 (elem (table $d) (i32.const 0) func $f)
3997 (func (export "run") (param i32) (result i32)
3998 (call_indirect $d (type $t) (local.get 0)))
3999 )
4000 "#;
4001 let wasm = wat::parse_str(wat).expect("parse");
4002 let module = decode_wasm_module(&wasm).expect("decode");
4003 assert_eq!(
4004 module.table_sizes,
4005 vec![None, Some(1)],
4006 "growable import (no max) has no sound compile-time size"
4007 );
4008 let guards = module.call_indirect_guards();
4009 assert_eq!(guards.tables[0].base_byte_offset, Some(0));
4010 assert!(
4011 guards.tables[0].type_reject.iter().all(|r| r.is_some()),
4012 "unknown-size table rejects every type"
4013 );
4014 assert_eq!(
4015 guards.tables[1].base_byte_offset, None,
4016 "a later table's base is not a compile-time constant when a \
4017 preceding table's size is unknown (#650)"
4018 );
4019 assert_eq!(guards.tables[1].table_size, Some(1));
4020 }
4021}