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