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 // F32Load / F32Store (linear-memory f32 access) stay dropped in phase 1
1910 // — VFP VLDR/VSTR take a [Rn,#imm] address, so the computed base+offset
1911 // needs an extra materialization the phase-1 selector does not yet emit.
1912 // They loud-skip at decode (phase 1b).
1913 F32ConvertI32S => Some(WasmOp::F32ConvertI32S),
1914 F32ConvertI32U => Some(WasmOp::F32ConvertI32U),
1915 I32TruncF32S => Some(WasmOp::I32TruncF32S),
1916 I32TruncF32U => Some(WasmOp::I32TruncF32U),
1917
1918 // f32x4
1919 F32x4Add => Some(WasmOp::F32x4Add),
1920 F32x4Sub => Some(WasmOp::F32x4Sub),
1921 F32x4Mul => Some(WasmOp::F32x4Mul),
1922 F32x4Div => Some(WasmOp::F32x4Div),
1923 F32x4Abs => Some(WasmOp::F32x4Abs),
1924 F32x4Neg => Some(WasmOp::F32x4Neg),
1925 F32x4Sqrt => Some(WasmOp::F32x4Sqrt),
1926 F32x4Eq => Some(WasmOp::F32x4Eq),
1927 F32x4Ne => Some(WasmOp::F32x4Ne),
1928 F32x4Lt => Some(WasmOp::F32x4Lt),
1929 F32x4Le => Some(WasmOp::F32x4Le),
1930 F32x4Gt => Some(WasmOp::F32x4Gt),
1931 F32x4Ge => Some(WasmOp::F32x4Ge),
1932 F32x4Splat => Some(WasmOp::F32x4Splat),
1933 F32x4ExtractLane { lane } => Some(WasmOp::F32x4ExtractLane(*lane)),
1934 F32x4ReplaceLane { lane } => Some(WasmOp::F32x4ReplaceLane(*lane)),
1935
1936 // Other operators not yet supported
1937 _ => None,
1938 }
1939}
1940
1941#[cfg(test)]
1942mod tests {
1943 use super::*;
1944
1945 #[test]
1946 fn test_decode_simple_add() {
1947 let wat = r#"
1948 (module
1949 (func (export "add") (param i32 i32) (result i32)
1950 local.get 0
1951 local.get 1
1952 i32.add
1953 )
1954 )
1955 "#;
1956
1957 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1958 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
1959
1960 assert_eq!(functions.len(), 1);
1961 assert_eq!(functions[0].index, 0);
1962 assert_eq!(functions[0].export_name, Some("add".to_string()));
1963 assert_eq!(
1964 functions[0].ops,
1965 vec![
1966 WasmOp::LocalGet(0),
1967 WasmOp::LocalGet(1),
1968 WasmOp::I32Add,
1969 WasmOp::End
1970 ]
1971 );
1972 }
1973
1974 /// #204 regression: `i64.extend_i32_u`, `i64.extend_i32_s` and
1975 /// `i32.wrap_i64` must DECODE (they were previously unmapped → silently
1976 /// dropped by `convert_operator`, leaving an i32 value as a 64-bit operand
1977 /// with a garbage high half — the root cause of gale's miscompiled
1978 /// `(new_count << 32)` pack). The decoder must surface all three.
1979 #[test]
1980 fn test_decode_i64_i32_width_conversions() {
1981 let wat = r#"
1982 (module
1983 (func (export "conv") (param i32 i64) (result i32)
1984 local.get 0
1985 i64.extend_i32_u
1986 local.get 0
1987 i64.extend_i32_s
1988 i64.add
1989 local.get 1
1990 i64.add
1991 i32.wrap_i64
1992 )
1993 )
1994 "#;
1995 let wasm = wat::parse_str(wat).expect("parse");
1996 let functions = decode_wasm_functions(&wasm).expect("decode");
1997 let ops = &functions[0].ops;
1998 assert!(
1999 ops.contains(&WasmOp::I64ExtendI32U),
2000 "i64.extend_i32_u must decode (not be dropped): {ops:?}"
2001 );
2002 assert!(
2003 ops.contains(&WasmOp::I64ExtendI32S),
2004 "i64.extend_i32_s must decode (not be dropped): {ops:?}"
2005 );
2006 assert!(
2007 ops.contains(&WasmOp::I32WrapI64),
2008 "i32.wrap_i64 must decode (not be dropped): {ops:?}"
2009 );
2010 }
2011
2012 /// #204 WAKE-path regression: `br_table` must DECODE (it was unmapped in
2013 /// `convert_operator` → silently dropped, so the selector emitted no index
2014 /// dispatch and every `br_table` fell through to target 0 — gale's binary
2015 /// semaphore never took its WAKE branch). Targets + default are preserved.
2016 #[test]
2017 fn test_decode_br_table() {
2018 let wat = r#"
2019 (module
2020 (func (export "bt") (param i32) (result i32)
2021 (block (block (block
2022 local.get 0
2023 br_table 2 0 1 2)
2024 i32.const 30 return)
2025 i32.const 20 return)
2026 i32.const 10))
2027 "#;
2028 let wasm = wat::parse_str(wat).expect("parse");
2029 let functions = decode_wasm_functions(&wasm).expect("decode");
2030 let bt = functions[0]
2031 .ops
2032 .iter()
2033 .find_map(|o| match o {
2034 WasmOp::BrTable { targets, default } => Some((targets.clone(), *default)),
2035 _ => None,
2036 })
2037 .expect("br_table must decode (not be dropped)");
2038 assert_eq!(bt.0, vec![2, 0, 1], "br_table targets preserved in order");
2039 assert_eq!(bt.1, 2, "br_table default preserved");
2040 }
2041
2042 #[test]
2043 fn test_decode_arithmetic() {
2044 let wat = r#"
2045 (module
2046 (func (export "calc") (result i32)
2047 i32.const 5
2048 i32.const 3
2049 i32.mul
2050 i32.const 2
2051 i32.add
2052 )
2053 )
2054 "#;
2055
2056 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2057 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2058
2059 assert_eq!(functions.len(), 1);
2060 assert_eq!(functions[0].export_name, Some("calc".to_string()));
2061 assert_eq!(
2062 functions[0].ops,
2063 vec![
2064 WasmOp::I32Const(5),
2065 WasmOp::I32Const(3),
2066 WasmOp::I32Mul,
2067 WasmOp::I32Const(2),
2068 WasmOp::I32Add,
2069 WasmOp::End,
2070 ]
2071 );
2072 }
2073
2074 #[test]
2075 fn test_decode_multi_function_module() {
2076 let wat = r#"
2077 (module
2078 (func $helper)
2079 (func (export "add") (param i32 i32) (result i32)
2080 local.get 0
2081 local.get 1
2082 i32.add
2083 )
2084 (func (export "sub") (param i32 i32) (result i32)
2085 local.get 0
2086 local.get 1
2087 i32.sub
2088 )
2089 )
2090 "#;
2091
2092 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2093 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2094
2095 assert_eq!(functions.len(), 3);
2096 assert_eq!(functions[0].index, 0);
2097 assert_eq!(functions[0].export_name, None);
2098 assert_eq!(functions[1].index, 1);
2099 assert_eq!(functions[1].export_name, Some("add".to_string()));
2100 assert_eq!(functions[2].index, 2);
2101 assert_eq!(functions[2].export_name, Some("sub".to_string()));
2102 }
2103
2104 #[test]
2105 fn test_decode_module_with_imports() {
2106 let wat = r#"
2107 (module
2108 (import "env" "log" (func $log (param i32)))
2109 (import "env" "memory" (memory 1))
2110 (func (export "run") (param i32)
2111 local.get 0
2112 call 0
2113 )
2114 )
2115 "#;
2116
2117 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2118 let module = decode_wasm_module(&wasm).expect("Failed to decode");
2119
2120 // Should have 2 imports (1 func, 1 memory)
2121 assert_eq!(module.imports.len(), 2);
2122 assert_eq!(module.num_imported_funcs, 1);
2123
2124 // First import is the function
2125 assert_eq!(module.imports[0].module, "env");
2126 assert_eq!(module.imports[0].name, "log");
2127 assert!(matches!(module.imports[0].kind, ImportKind::Function(_)));
2128
2129 // Second import is memory
2130 assert_eq!(module.imports[1].module, "env");
2131 assert_eq!(module.imports[1].name, "memory");
2132 assert_eq!(module.imports[1].kind, ImportKind::Memory);
2133
2134 // Should have 1 local function (index 1, because import is index 0)
2135 assert_eq!(module.functions.len(), 1);
2136 assert_eq!(module.functions[0].index, 1);
2137 assert_eq!(module.functions[0].export_name, Some("run".to_string()));
2138 }
2139
2140 #[test]
2141 fn test_find_function_by_export_name() {
2142 let wat = r#"
2143 (module
2144 (func $helper)
2145 (func (export "add") (param i32 i32) (result i32)
2146 local.get 0
2147 local.get 1
2148 i32.add
2149 )
2150 )
2151 "#;
2152
2153 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2154 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2155
2156 let add_func = functions
2157 .iter()
2158 .find(|f| f.export_name.as_deref() == Some("add"))
2159 .expect("Should find 'add' function");
2160
2161 assert_eq!(add_func.index, 1);
2162 assert!(add_func.ops.contains(&WasmOp::I32Add));
2163 }
2164
2165 #[test]
2166 fn test_decode_subword_loads() {
2167 let wat = r#"
2168 (module
2169 (memory 1)
2170 (func (export "test") (param i32) (result i32)
2171 local.get 0
2172 i32.load8_u
2173 )
2174 )
2175 "#;
2176
2177 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2178 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2179
2180 assert_eq!(functions.len(), 1);
2181 assert!(functions[0].ops.contains(&WasmOp::I32Load8U {
2182 offset: 0,
2183 align: 0,
2184 }));
2185 }
2186
2187 #[test]
2188 fn test_decode_subword_stores() {
2189 let wat = r#"
2190 (module
2191 (memory 1)
2192 (func (export "test") (param i32 i32)
2193 local.get 0
2194 local.get 1
2195 i32.store8
2196 )
2197 )
2198 "#;
2199
2200 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2201 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2202
2203 assert_eq!(functions.len(), 1);
2204 assert!(functions[0].ops.contains(&WasmOp::I32Store8 {
2205 offset: 0,
2206 align: 0,
2207 }));
2208 }
2209
2210 #[test]
2211 fn test_decode_memory_size_grow() {
2212 let wat = r#"
2213 (module
2214 (memory 1)
2215 (func (export "test") (result i32)
2216 memory.size
2217 )
2218 )
2219 "#;
2220
2221 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2222 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2223
2224 assert_eq!(functions.len(), 1);
2225 assert!(functions[0].ops.contains(&WasmOp::MemorySize(0)));
2226 }
2227
2228 #[test]
2229 fn test_decode_memory_grow() {
2230 let wat = r#"
2231 (module
2232 (memory 1)
2233 (func (export "test") (param i32) (result i32)
2234 local.get 0
2235 memory.grow
2236 )
2237 )
2238 "#;
2239
2240 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2241 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2242
2243 assert_eq!(functions.len(), 1);
2244 assert!(functions[0].ops.contains(&WasmOp::MemoryGrow(0)));
2245 }
2246
2247 #[test]
2248 fn test_decode_bulk_memory_374() {
2249 // #374: memory.copy / memory.fill on the single linear memory decode to
2250 // the new WasmOp variants (was `_ => None` -> loud-skip).
2251 let wat = r#"
2252 (module
2253 (memory 1)
2254 (func (export "cpy") (param i32 i32 i32)
2255 local.get 0 local.get 1 local.get 2 memory.copy)
2256 (func (export "fil") (param i32 i32 i32)
2257 local.get 0 local.get 1 local.get 2 memory.fill)
2258 )
2259 "#;
2260 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2261 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2262 assert_eq!(functions.len(), 2);
2263 assert!(functions[0].ops.contains(&WasmOp::MemoryCopy));
2264 assert!(functions[1].ops.contains(&WasmOp::MemoryFill));
2265 // Neither function is flagged unsupported (they now lower).
2266 assert!(functions[0].unsupported.is_none());
2267 assert!(functions[1].unsupported.is_none());
2268 }
2269
2270 #[test]
2271 fn test_decode_i64_subword_loads() {
2272 let wat = r#"
2273 (module
2274 (memory 1)
2275 (func (export "test") (param i32) (result i64)
2276 local.get 0
2277 i64.load8_s
2278 )
2279 )
2280 "#;
2281
2282 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2283 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2284
2285 assert_eq!(functions.len(), 1);
2286 assert!(functions[0].ops.contains(&WasmOp::I64Load8S {
2287 offset: 0,
2288 align: 0,
2289 }));
2290 }
2291
2292 #[test]
2293 fn test_decode_all_subword_memory_ops() {
2294 // Test that all sub-word operations are decoded from WAT
2295 let wat = r#"
2296 (module
2297 (memory 1)
2298 (func (export "test") (param i32)
2299 ;; i32 sub-word loads
2300 local.get 0
2301 i32.load8_s
2302 drop
2303 local.get 0
2304 i32.load8_u
2305 drop
2306 local.get 0
2307 i32.load16_s
2308 drop
2309 local.get 0
2310 i32.load16_u
2311 drop
2312
2313 ;; i32 sub-word stores
2314 local.get 0
2315 i32.const 42
2316 i32.store8
2317 local.get 0
2318 i32.const 42
2319 i32.store16
2320
2321 ;; i64 sub-word loads
2322 local.get 0
2323 i64.load8_s
2324 drop
2325 local.get 0
2326 i64.load8_u
2327 drop
2328 local.get 0
2329 i64.load16_s
2330 drop
2331 local.get 0
2332 i64.load16_u
2333 drop
2334 local.get 0
2335 i64.load32_s
2336 drop
2337 local.get 0
2338 i64.load32_u
2339 drop
2340
2341 ;; i64 sub-word stores
2342 local.get 0
2343 i64.const 42
2344 i64.store8
2345 local.get 0
2346 i64.const 42
2347 i64.store16
2348 local.get 0
2349 i64.const 42
2350 i64.store32
2351 )
2352 )
2353 "#;
2354
2355 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2356 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2357
2358 assert_eq!(functions.len(), 1);
2359 let ops = &functions[0].ops;
2360
2361 // Verify i32 sub-word ops are present
2362 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8S { .. })));
2363 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load8U { .. })));
2364 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16S { .. })));
2365 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Load16U { .. })));
2366 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store8 { .. })));
2367 assert!(ops.iter().any(|o| matches!(o, WasmOp::I32Store16 { .. })));
2368
2369 // Verify i64 sub-word ops are present
2370 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8S { .. })));
2371 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load8U { .. })));
2372 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16S { .. })));
2373 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load16U { .. })));
2374 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32S { .. })));
2375 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Load32U { .. })));
2376 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store8 { .. })));
2377 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store16 { .. })));
2378 assert!(ops.iter().any(|o| matches!(o, WasmOp::I64Store32 { .. })));
2379 }
2380
2381 #[test]
2382 fn test_decode_simd_i32x4_add() {
2383 let wat = r#"
2384 (module
2385 (func (export "add_v128") (param v128 v128) (result v128)
2386 local.get 0
2387 local.get 1
2388 i32x4.add
2389 )
2390 )
2391 "#;
2392
2393 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2394 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2395
2396 assert_eq!(functions.len(), 1);
2397 assert!(
2398 functions[0].ops.contains(&WasmOp::I32x4Add),
2399 "Should decode i32x4.add: {:?}",
2400 functions[0].ops
2401 );
2402 }
2403
2404 #[test]
2405 fn test_decode_simd_v128_const() {
2406 let wat = r#"
2407 (module
2408 (func (export "const_v128") (result v128)
2409 v128.const i32x4 1 2 3 4
2410 )
2411 )
2412 "#;
2413
2414 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2415 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2416
2417 assert_eq!(functions.len(), 1);
2418 assert!(
2419 functions[0]
2420 .ops
2421 .iter()
2422 .any(|o| matches!(o, WasmOp::V128Const(_))),
2423 "Should decode v128.const: {:?}",
2424 functions[0].ops
2425 );
2426 }
2427
2428 #[test]
2429 fn test_decode_simd_v128_load_store() {
2430 let wat = r#"
2431 (module
2432 (memory 1)
2433 (func (export "load_store") (param i32)
2434 local.get 0
2435 v128.load
2436 local.get 0
2437 v128.store
2438 )
2439 )
2440 "#;
2441
2442 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2443 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2444
2445 assert_eq!(functions.len(), 1);
2446 let ops = &functions[0].ops;
2447 assert!(
2448 ops.iter().any(|o| matches!(o, WasmOp::V128Load { .. })),
2449 "Should decode v128.load"
2450 );
2451 assert!(
2452 ops.iter().any(|o| matches!(o, WasmOp::V128Store { .. })),
2453 "Should decode v128.store"
2454 );
2455 }
2456
2457 #[test]
2458 fn test_decode_simd_bitwise_ops() {
2459 let wat = r#"
2460 (module
2461 (func (export "bitwise") (param v128 v128) (result v128)
2462 local.get 0
2463 local.get 1
2464 v128.and
2465 )
2466 )
2467 "#;
2468
2469 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2470 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2471
2472 assert_eq!(functions.len(), 1);
2473 assert!(functions[0].ops.contains(&WasmOp::V128And));
2474 }
2475
2476 #[test]
2477 fn test_decode_simd_splat() {
2478 let wat = r#"
2479 (module
2480 (func (export "splat") (param i32) (result v128)
2481 local.get 0
2482 i32x4.splat
2483 )
2484 )
2485 "#;
2486
2487 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2488 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2489
2490 assert_eq!(functions.len(), 1);
2491 assert!(functions[0].ops.contains(&WasmOp::I32x4Splat));
2492 }
2493
2494 #[test]
2495 fn test_decode_simd_extract_lane() {
2496 let wat = r#"
2497 (module
2498 (func (export "extract") (param v128) (result i32)
2499 local.get 0
2500 i32x4.extract_lane 2
2501 )
2502 )
2503 "#;
2504
2505 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2506 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2507
2508 assert_eq!(functions.len(), 1);
2509 assert!(
2510 functions[0].ops.contains(&WasmOp::I32x4ExtractLane(2)),
2511 "Should decode i32x4.extract_lane 2"
2512 );
2513 }
2514
2515 #[test]
2516 fn test_decode_simd_f32x4_arithmetic() {
2517 let wat = r#"
2518 (module
2519 (func (export "f32x4_add") (param v128 v128) (result v128)
2520 local.get 0
2521 local.get 1
2522 f32x4.add
2523 )
2524 )
2525 "#;
2526
2527 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2528 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2529
2530 assert_eq!(functions.len(), 1);
2531 assert!(functions[0].ops.contains(&WasmOp::F32x4Add));
2532 }
2533
2534 #[test]
2535 fn test_369_scalar_float_op_flags_function_unsupported_not_dropped() {
2536 // GI-FPU-002 (#619): the in-scope scalar f32 ops (add/sub/mul/div,
2537 // comparisons, i32.trunc_f32_s/u, f32.convert_i32_s/u, f32.const) are
2538 // now DECODED (routed to the VFP selector on FPU targets), so `f32.add`
2539 // is no longer flagged. An out-of-scope scalar float op (`f64.add`,
2540 // phase 2) is STILL flagged (loud-skip), never silently dropped — the
2541 // #369 honesty contract holds for the not-yet-lowered surface. A
2542 // pure-integer function stays clean.
2543 let wat = r#"
2544 (module
2545 (func (export "fadd") (param f32 f32) (result f32)
2546 local.get 0 local.get 1 f32.add)
2547 (func (export "dadd") (param f64 f64) (result f64)
2548 local.get 0 local.get 1 f64.add)
2549 (func (export "iadd") (param i32 i32) (result i32)
2550 local.get 0 local.get 1 i32.add))
2551 "#;
2552 let wasm = wat::parse_str(wat).expect("parse");
2553 let functions = decode_wasm_functions(&wasm).expect("decode");
2554 let fadd = functions
2555 .iter()
2556 .find(|f| f.export_name.as_deref() == Some("fadd"))
2557 .unwrap();
2558 let dadd = functions
2559 .iter()
2560 .find(|f| f.export_name.as_deref() == Some("dadd"))
2561 .unwrap();
2562 let iadd = functions
2563 .iter()
2564 .find(|f| f.export_name.as_deref() == Some("iadd"))
2565 .unwrap();
2566 // In-scope f32 op: now decoded (reachable), not flagged.
2567 assert!(
2568 fadd.unsupported.is_none(),
2569 "GI-FPU-002: f32.add must now decode (not be flagged), got {:?}",
2570 fadd.unsupported
2571 );
2572 assert!(
2573 fadd.ops.contains(&WasmOp::F32Add),
2574 "f32.add must decode to WasmOp::F32Add: {:?}",
2575 fadd.ops
2576 );
2577 // Out-of-scope scalar double op: still flagged (phase 2), never dropped.
2578 assert!(
2579 dadd.unsupported.is_some(),
2580 "f64.add must still flag the function unsupported (phase 2), got {:?}",
2581 dadd.unsupported
2582 );
2583 assert!(
2584 iadd.unsupported.is_none(),
2585 "a pure-integer function must NOT be flagged: {:?}",
2586 iadd.unsupported
2587 );
2588 }
2589
2590 #[test]
2591 fn test_369_float_global_access_flags_function_unsupported() {
2592 // GI-FPU-001 (#369): `global.get`/`global.set` on an f32/f64-typed
2593 // global decode fine (the ops are type-agnostic), but the float
2594 // initializer is dropped (`init_i32: None` -> slot zeroed), so a read
2595 // returned a silently-wrong 0.0 instead of the init (verified: the
2596 // 2.5f bit pattern 0x40200000 was absent from the output ELF). The
2597 // access must flag the function for the loud-skip path. Accesses to
2598 // integer globals stay clean.
2599 let wat = r#"
2600 (module
2601 (global $fg f32 (f32.const 2.5))
2602 (global $dg (mut f64) (f64.const 1.5))
2603 (global $ig (mut i32) (i32.const 7))
2604 (func (export "fget") (result f32) global.get $fg)
2605 (func (export "dset") (param f64) local.get 0 global.set $dg)
2606 (func (export "iget") (result i32) global.get $ig))
2607 "#;
2608 let wasm = wat::parse_str(wat).expect("parse");
2609
2610 // Both decode entry points must flag (the CLI compiles through both:
2611 // decode_wasm_module on the all-exports/module paths,
2612 // decode_wasm_functions on the single-function path).
2613 let module = decode_wasm_module(&wasm).expect("decode module");
2614 for functions in [
2615 &module.functions,
2616 &decode_wasm_functions(&wasm).expect("decode fns"),
2617 ] {
2618 let by_name = |n: &str| {
2619 functions
2620 .iter()
2621 .find(|f| f.export_name.as_deref() == Some(n))
2622 .unwrap()
2623 };
2624 let fget = by_name("fget");
2625 assert!(
2626 fget.unsupported.is_some(),
2627 "global.get of an f32 global must flag the function (loud-skip), got {:?}",
2628 fget.unsupported
2629 );
2630 let reason = fget.unsupported.as_deref().unwrap();
2631 assert!(
2632 reason.contains("GlobalGet") && reason.contains("GI-FPU-001"),
2633 "diagnostic should name the op and GI-FPU-001: {reason:?}"
2634 );
2635 let dset = by_name("dset");
2636 assert!(
2637 dset.unsupported
2638 .as_deref()
2639 .is_some_and(|r| r.contains("GlobalSet")),
2640 "global.set of an f64 global must flag the function, got {:?}",
2641 dset.unsupported
2642 );
2643 assert!(
2644 by_name("iget").unsupported.is_none(),
2645 "an i32 global access must NOT be flagged: {:?}",
2646 by_name("iget").unsupported
2647 );
2648 }
2649 }
2650
2651 #[test]
2652 fn test_369_imported_float_global_shifts_index_space() {
2653 // GI-FPU-001 (#369): imported globals come FIRST in the global index
2654 // space. An imported f64 global at index 0 must be flagged, and the
2655 // defined i32 global at index 1 must NOT be mistaken for it.
2656 let wat = r#"
2657 (module
2658 (import "env" "fg" (global f64))
2659 (global $ig i32 (i32.const 3))
2660 (func (export "fget") (result f64) global.get 0)
2661 (func (export "iget") (result i32) global.get 1))
2662 "#;
2663 let wasm = wat::parse_str(wat).expect("parse");
2664 let functions = decode_wasm_functions(&wasm).expect("decode");
2665 let by_name = |n: &str| {
2666 functions
2667 .iter()
2668 .find(|f| f.export_name.as_deref() == Some(n))
2669 .unwrap()
2670 };
2671 assert!(
2672 by_name("fget")
2673 .unsupported
2674 .as_deref()
2675 .is_some_and(|r| r.contains("GI-FPU-001")),
2676 "imported f64 global access must flag: {:?}",
2677 by_name("fget").unsupported
2678 );
2679 assert!(
2680 by_name("iget").unsupported.is_none(),
2681 "defined i32 global at shifted index 1 must NOT flag: {:?}",
2682 by_name("iget").unsupported
2683 );
2684 }
2685
2686 #[test]
2687 fn test_680_simd_ops_flag_function_unsupported_not_dropped() {
2688 // #680: SIMD (v128) ops decode into WasmOp variants no production
2689 // target can select (`has_helium` is test-only), so they were silently
2690 // dropped at selection — `i32x4.add` compiled to an operand
2691 // passthrough (`mov r0,r1`) and shipped a wrong result. The issue's
2692 // exact reproducer must flag the function; the scalar sibling must
2693 // stay compilable (non-vacuity).
2694 let wat = r#"
2695 (module
2696 (memory 1)
2697 (func (export "vadd") (param i32 i32) (result i32)
2698 (i32x4.extract_lane 2
2699 (i32x4.add (i32x4.splat (local.get 0))
2700 (i32x4.splat (local.get 1)))))
2701 (func (export "vstore") (param i32 i32) (result i32)
2702 (v128.store (i32.const 0) (i32x4.splat (local.get 0)))
2703 (i32.load (i32.const 0)))
2704 (func (export "iadd") (param i32 i32) (result i32)
2705 local.get 0 local.get 1 i32.add))
2706 "#;
2707 let wasm = wat::parse_str(wat).expect("parse");
2708
2709 // Both decode entry points must flag (the CLI compiles through both).
2710 let module = decode_wasm_module(&wasm).expect("decode module");
2711 for functions in [
2712 &module.functions,
2713 &decode_wasm_functions(&wasm).expect("decode fns"),
2714 ] {
2715 let by_name = |n: &str| {
2716 functions
2717 .iter()
2718 .find(|f| f.export_name.as_deref() == Some(n))
2719 .unwrap()
2720 };
2721 for name in ["vadd", "vstore"] {
2722 let reason = by_name(name).unsupported.as_deref();
2723 assert!(
2724 reason.is_some(),
2725 "{name}: v128 ops must flag the function (loud-skip), got None"
2726 );
2727 let reason = reason.unwrap();
2728 assert!(
2729 reason.contains("no SIMD lowering for this target") && reason.contains("#680"),
2730 "{name}: diagnostic must name the target gap and #680: {reason:?}"
2731 );
2732 }
2733 // The reason names the FIRST SIMD op hit (splat in both bodies).
2734 assert!(
2735 by_name("vadd")
2736 .unsupported
2737 .as_deref()
2738 .unwrap()
2739 .contains("I32x4Splat"),
2740 "diagnostic should name the op: {:?}",
2741 by_name("vadd").unsupported
2742 );
2743 assert!(
2744 by_name("iadd").unsupported.is_none(),
2745 "a scalar function in the same module must NOT be flagged: {:?}",
2746 by_name("iadd").unsupported
2747 );
2748 }
2749 }
2750
2751 #[test]
2752 fn test_680_v128_local_and_signature_flag_function() {
2753 // #680: v128 VALUES are expressible with ZERO SIMD-proposal operators
2754 // in the body — a v128-typed local or a v128 param/result is reached
2755 // through type-agnostic `local.get`/`local.set`, which the selectors
2756 // lower as 4-byte moves (silent 16-byte truncation). Both must flag.
2757 let wat = r#"
2758 (module
2759 (func (export "vlocal") (result i32) (local v128)
2760 i32.const 7)
2761 (func (export "vpass") (param v128) (result v128)
2762 local.get 0)
2763 (func (export "scalar") (param i32) (result i32)
2764 local.get 0))
2765 "#;
2766 let wasm = wat::parse_str(wat).expect("parse");
2767 let module = decode_wasm_module(&wasm).expect("decode module");
2768 for functions in [
2769 &module.functions,
2770 &decode_wasm_functions(&wasm).expect("decode fns"),
2771 ] {
2772 let by_name = |n: &str| {
2773 functions
2774 .iter()
2775 .find(|f| f.export_name.as_deref() == Some(n))
2776 .unwrap()
2777 };
2778 assert!(
2779 by_name("vlocal")
2780 .unsupported
2781 .as_deref()
2782 .is_some_and(|r| r.contains("v128-typed local") && r.contains("#680")),
2783 "a v128-typed local declaration must flag: {:?}",
2784 by_name("vlocal").unsupported
2785 );
2786 assert!(
2787 by_name("vpass")
2788 .unsupported
2789 .as_deref()
2790 .is_some_and(|r| r.contains("v128 param/result") && r.contains("#680")),
2791 "a v128 param/result signature must flag (op-free body!): {:?}",
2792 by_name("vpass").unsupported
2793 );
2794 assert!(
2795 by_name("scalar").unsupported.is_none(),
2796 "a scalar function must NOT be flagged: {:?}",
2797 by_name("scalar").unsupported
2798 );
2799 }
2800 }
2801
2802 #[test]
2803 fn test_680_v128_global_access_flags_function() {
2804 // #680: `global.get`/`global.set` on a v128-typed global decode fine
2805 // (type-agnostic ops), but the access would move 4 of the 16 bytes and
2806 // the `v128.const` initializer is never captured. Same lane as the
2807 // float globals (#648/GI-FPU-001); imported globals shift the index
2808 // space (imports first). The i32-global sibling stays compilable.
2809 let wat = r#"
2810 (module
2811 (import "env" "vg" (global v128))
2812 (global $ig (mut i32) (i32.const 7))
2813 (global $dg (mut v128) (v128.const i32x4 1 2 3 4))
2814 (func (export "vget") global.get 0 drop)
2815 (func (export "iget") (result i32) global.get $ig))
2816 "#;
2817 let wasm = wat::parse_str(wat).expect("parse");
2818 let module = decode_wasm_module(&wasm).expect("decode module");
2819 for functions in [
2820 &module.functions,
2821 &decode_wasm_functions(&wasm).expect("decode fns"),
2822 ] {
2823 let by_name = |n: &str| {
2824 functions
2825 .iter()
2826 .find(|f| f.export_name.as_deref() == Some(n))
2827 .unwrap()
2828 };
2829 let reason = by_name("vget").unsupported.as_deref();
2830 assert!(
2831 reason.is_some_and(|r| r.contains("GlobalGet")
2832 && r.contains("v128-typed global")
2833 && r.contains("#680")),
2834 "global.get of an imported v128 global must flag: {reason:?}"
2835 );
2836 assert!(
2837 by_name("iget").unsupported.is_none(),
2838 "an i32 global access must NOT be flagged: {:?}",
2839 by_name("iget").unsupported
2840 );
2841 }
2842 }
2843
2844 #[test]
2845 fn test_decode_simd_multiple_ops() {
2846 let wat = r#"
2847 (module
2848 (func (export "simd_ops") (param v128 v128 v128) (result v128)
2849 ;; (a + b) * c
2850 local.get 0
2851 local.get 1
2852 i32x4.add
2853 local.get 2
2854 i32x4.mul
2855 )
2856 )
2857 "#;
2858
2859 let wasm = wat::parse_str(wat).expect("Failed to parse WAT with SIMD");
2860 let functions = decode_wasm_functions(&wasm).expect("Failed to decode");
2861
2862 assert_eq!(functions.len(), 1);
2863 let ops = &functions[0].ops;
2864 assert!(ops.contains(&WasmOp::I32x4Add));
2865 assert!(ops.contains(&WasmOp::I32x4Mul));
2866 }
2867
2868 /// VCR-DBG-001 step 1 (#394): the decoder records a module-relative wasm byte
2869 /// offset per emitted op — the DWARF-for-wasm address space that bridges
2870 /// synth's op-index `source_line` to the input wasm's `.debug_line`. Purely
2871 /// additive metadata (no codegen consumer ⇒ frozen fixtures byte-identical,
2872 /// verified separately); this test pins the structural invariants.
2873 #[test]
2874 fn test_decode_records_aligned_increasing_op_offsets_dbg001() {
2875 let wat = r#"
2876 (module
2877 (func (export "f") (param i32 i32) (result i32)
2878 local.get 0
2879 local.get 1
2880 i32.add
2881 i32.const 7
2882 i32.mul))
2883 "#;
2884 let wasm = wat::parse_str(wat).expect("parse WAT");
2885 let functions = decode_wasm_functions(&wasm).expect("decode");
2886 let f = &functions[0];
2887
2888 // One offset per emitted op, index-aligned with `ops`.
2889 assert_eq!(
2890 f.op_offsets.len(),
2891 f.ops.len(),
2892 "op_offsets must be parallel to ops"
2893 );
2894 assert!(!f.op_offsets.is_empty());
2895
2896 // Byte offsets are strictly increasing through the body (each op consumes
2897 // at least one byte) and module-relative (well past the header).
2898 assert!(
2899 f.op_offsets.windows(2).all(|w| w[1] > w[0]),
2900 "wasm byte offsets must strictly increase: {:?}",
2901 f.op_offsets
2902 );
2903 assert!(
2904 f.op_offsets[0] >= 8,
2905 "module-relative offset is past the 8-byte wasm header"
2906 );
2907 }
2908
2909 /// #237: the decoder captures a global's `i32.const` initializer + mutability,
2910 /// so the native-pointer ABI can recognize the stack-pointer global.
2911 #[test]
2912 fn test_decode_captures_global_initializer() {
2913 let wat = r#"
2914 (module
2915 (memory 2)
2916 (global $__stack_pointer (mut i32) (i32.const 65536))
2917 (global $immutable_const i32 (i32.const 7))
2918 (func (export "f") (result i32) global.get 0)
2919 )
2920 "#;
2921 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2922 let module = decode_wasm_module(&wasm).expect("Failed to decode");
2923
2924 assert_eq!(module.globals.len(), 2, "both globals captured");
2925 let sp = &module.globals[0];
2926 assert_eq!(sp.index, 0);
2927 assert_eq!(
2928 sp.init,
2929 Some(GlobalInit::I32(65536)),
2930 "stack-pointer init captured"
2931 );
2932 assert!(sp.mutable, "stack pointer is mutable");
2933 let c = &module.globals[1];
2934 assert_eq!(c.init, Some(GlobalInit::I32(7)));
2935 assert!(!c.mutable, "second global is immutable");
2936 assert_eq!(sp.slot_bytes, 4, "i32 global occupies one 4-byte slot");
2937 assert_eq!(c.slot_bytes, 4);
2938 }
2939
2940 /// #643: the decoder records the DECLARED slot width per global — an i64
2941 /// (or f64) global occupies 8 bytes, so the globals-table layout can give
2942 /// it room for both words and shift every later global's offset.
2943 #[test]
2944 fn test_decode_records_global_slot_widths_643() {
2945 let wat = r#"
2946 (module
2947 (global $c (mut i64) (i64.const 0))
2948 (global $k (mut i32) (i32.const 0))
2949 (global $f (mut f64) (f64.const 0))
2950 (func (export "f") (result i32) global.get 1)
2951 )
2952 "#;
2953 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2954 let module = decode_wasm_module(&wasm).expect("Failed to decode");
2955
2956 assert_eq!(module.globals.len(), 3);
2957 assert_eq!(module.globals[0].slot_bytes, 8, "i64 global is 8 bytes");
2958 assert_eq!(module.globals[1].slot_bytes, 4, "i32 global is 4 bytes");
2959 assert_eq!(module.globals[2].slot_bytes, 8, "f64 global is 8 bytes");
2960 }
2961
2962 /// #649: a nonzero `i64.const` initializer is captured as BOTH words — the
2963 /// `init_i32`-shaped capture dropped it to `None` and every consumer's
2964 /// `unwrap_or(0)` silently ZEROED the global. f32/f64 inits stay `None`
2965 /// (GI-FPU-001/#369 loud-skip lane — never fabricate a float bit-pattern).
2966 #[test]
2967 fn test_decode_captures_i64_global_initializer_649() {
2968 let wat = r#"
2969 (module
2970 (global $g (mut i64) (i64.const 0x123456789ABCDEF0))
2971 (global $n (mut i64) (i64.const -1))
2972 (global $f (mut f64) (f64.const 1.5))
2973 (global $h (mut f32) (f32.const 2.5))
2974 (func (export "f") (result i32) i32.const 0)
2975 )
2976 "#;
2977 let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
2978 let module = decode_wasm_module(&wasm).expect("Failed to decode");
2979
2980 assert_eq!(module.globals.len(), 4);
2981 assert_eq!(
2982 module.globals[0].init,
2983 Some(GlobalInit::I64(0x123456789ABCDEF0u64 as i64)),
2984 "nonzero i64 init captured with both words"
2985 );
2986 assert_eq!(module.globals[1].init, Some(GlobalInit::I64(-1)));
2987 assert_eq!(
2988 module.globals[2].init, None,
2989 "f64 init is NOT captured (GI-FPU-001 loud-skip lane)"
2990 );
2991 assert_eq!(
2992 module.globals[3].init, None,
2993 "f32 init is NOT captured (GI-FPU-001 loud-skip lane)"
2994 );
2995 }
2996
2997 /// #509: the decoder records `(param_count, result_count)` for every
2998 /// `Block`/`Loop`/`If`, ordinal-keyed in op order, covering all three
2999 /// blocktype encodings: `Empty → (0,0)`, `ValType → (0,1)`, and
3000 /// `FuncType(i) →` counts from the type section (here a multi-result
3001 /// block, which wat encodes as a functype blocktype).
3002 #[test]
3003 fn test_decode_records_block_arity_side_table_509() {
3004 let wat = r#"
3005 (module
3006 (func (export "f") (param i32) (result i32)
3007 (block (result i32)
3008 (block (nop))
3009 (local.get 0)
3010 (if (result i32)
3011 (then (i32.const 1))
3012 (else (i32.const 2)))))
3013 (func (export "g") (result i32)
3014 (block (result i32 i32)
3015 (i32.const 1) (i32.const 2))
3016 i32.add)
3017 (func (export "h") (param i32) (result i32)
3018 (local.get 0)
3019 (loop (param i32) (result i32))))
3020 "#;
3021 let wasm = wat::parse_str(wat).expect("parse WAT");
3022
3023 // Both decode entry points must produce the same side-table.
3024 for functions in [
3025 decode_wasm_functions(&wasm).expect("decode"),
3026 decode_wasm_module(&wasm).expect("decode").functions,
3027 ] {
3028 // f: Block(result i32), Block(void), If(result i32) — in op order.
3029 assert_eq!(
3030 functions[0].block_arity,
3031 vec![(0, 1), (0, 0), (0, 1)],
3032 "f: ValType/Empty/ValType blocktypes"
3033 );
3034 // g: one multi-result block via a FuncType blocktype.
3035 assert_eq!(
3036 functions[1].block_arity,
3037 vec![(0, 2)],
3038 "g: functype blocktype result count from the type section"
3039 );
3040 // h: a parameterized loop — the input arity is what a br to the
3041 // header would carry (the #509 loud-decline discriminator).
3042 assert_eq!(
3043 functions[2].block_arity,
3044 vec![(1, 1)],
3045 "h: loop params captured"
3046 );
3047 }
3048 }
3049
3050 /// #642: the decoder captures table 0's compile-time size, per-segment
3051 /// element shapes and per-function type indices, and the closed-world
3052 /// verdict VERIFIES a fully-covered homogeneous table.
3053 #[test]
3054 fn test_call_indirect_guards_closed_world_verified_642() {
3055 // The #642 repro shape: 3-entry table, fully covered, one signature.
3056 let wat = r#"
3057 (module
3058 (type $bin (func (param i32 i32) (result i32)))
3059 (table 3 funcref)
3060 (elem (i32.const 0) $add $sub $mul)
3061 (func $add (param i32 i32) (result i32)
3062 (i32.add (local.get 0) (local.get 1)))
3063 (func $sub (param i32 i32) (result i32)
3064 (i32.sub (local.get 0) (local.get 1)))
3065 (func $mul (param i32 i32) (result i32)
3066 (i32.mul (local.get 0) (local.get 1)))
3067 (func (export "f") (param i32 i32) (result i32)
3068 (call_indirect (type $bin)
3069 (local.get 0) (i32.const 10) (local.get 1)))
3070 )
3071 "#;
3072 let wasm = wat::parse_str(wat).expect("parse");
3073 let module = decode_wasm_module(&wasm).expect("decode");
3074
3075 assert_eq!(module.table_size, Some(3), "table section min size");
3076 assert_eq!(module.table_sizes, vec![Some(3)], "#650 per-table sizes");
3077 assert_eq!(
3078 module.elem_segments,
3079 vec![ElemSegmentInfo {
3080 table_index: 0,
3081 offset: Some(0),
3082 funcs: Some(vec![0, 1, 2]),
3083 }]
3084 );
3085 // 2 type-section entries ($bin + the export's (i32 i32)->i32 dedups
3086 // to one in practice, but don't assume — just check func 0..2 share
3087 // a signature with type 0).
3088 assert_eq!(module.func_type_indices.len(), 4);
3089
3090 let guards = module.call_indirect_guards();
3091 assert_eq!(guards.tables.len(), 1);
3092 assert_eq!(guards.tables[0].table_size, Some(3));
3093 assert_eq!(
3094 guards.tables[0].base_byte_offset,
3095 Some(0),
3096 "#650: a single-table module keeps table 0 at R11 offset 0 by construction"
3097 );
3098 // Type index 0 ($bin) must be VERIFIED: every table entry has its
3099 // exact signature.
3100 assert_eq!(
3101 guards.tables[0].type_reject.first(),
3102 Some(&None),
3103 "closed-world type check must verify the homogeneous table: {:?}",
3104 guards.tables[0].type_reject
3105 );
3106 assert!(
3107 !guards.tables[0].has_null_slots,
3108 "#664: a fully-initialized table must NOT request the runtime \
3109 null check (dispatch bytes stay identical by construction)"
3110 );
3111 }
3112
3113 /// #642: a heterogeneous table (an entry whose signature differs from the
3114 /// expected type) must REJECT that expected type — the raw code-pointer
3115 /// table cannot be runtime-type-checked, so the lowering has to decline.
3116 #[test]
3117 fn test_call_indirect_guards_heterogeneous_table_rejects_642() {
3118 let wat = r#"
3119 (module
3120 (type $bin (func (param i32 i32) (result i32)))
3121 (type $un (func (param i32) (result i32)))
3122 (table 2 funcref)
3123 (elem (i32.const 0) $add $neg)
3124 (func $add (type $bin)
3125 (i32.add (local.get 0) (local.get 1)))
3126 (func $neg (type $un)
3127 (i32.sub (i32.const 0) (local.get 0)))
3128 (func (export "f") (param i32 i32) (result i32)
3129 (call_indirect (type $bin)
3130 (local.get 0) (i32.const 10) (local.get 1)))
3131 )
3132 "#;
3133 let wasm = wat::parse_str(wat).expect("parse");
3134 let module = decode_wasm_module(&wasm).expect("decode");
3135 let guards = module.call_indirect_guards();
3136 assert_eq!(guards.tables[0].table_size, Some(2));
3137 // BOTH expected types must be rejected: the table holds one function
3138 // of each signature, so neither type's closed world holds.
3139 assert!(
3140 guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3141 "heterogeneous table must reject every expected type: {:?}",
3142 guards.tables[0].type_reject
3143 );
3144 // #676: ... but the image is statically known, so the mismatch trap
3145 // is dischargeable at RUNTIME via the type-id sidecar.
3146 assert!(
3147 guards.tables[0].runtime_type_check,
3148 "heterogeneous-but-known table must offer the runtime check (#676)"
3149 );
3150 assert_eq!(
3151 guards.type_ids_byte_offset,
3152 Some(8),
3153 "sidecar sits after the 2-slot pointer region"
3154 );
3155 assert_eq!(
3156 guards.type_ids_image,
3157 vec![1, 2],
3158 "slot 0 = $bin (class 1), slot 1 = $un (class 2)"
3159 );
3160 assert_eq!(guards.type_class_ids, vec![1, 2]);
3161 }
3162
3163 /// #664 (relaxes the #642 all-reject): an uninitialized table slot (elem
3164 /// covers less than the declared size) is a null funcref — calling it
3165 /// must trap, which is now discharged at RUNTIME (null check on the
3166 /// zero-linked pointer), so the closed-world verdict verifies the
3167 /// INITIALIZED slots and sets `has_null_slots` for the lowering.
3168 #[test]
3169 fn test_call_indirect_guards_null_slot_verifies_with_flag_664() {
3170 let wat = r#"
3171 (module
3172 (type $s (func (result i32)))
3173 (table 3 funcref)
3174 (elem (i32.const 0) $f0 $f1)
3175 (func $f0 (result i32) (i32.const 10))
3176 (func $f1 (result i32) (i32.const 11))
3177 (func (export "run") (param i32) (result i32)
3178 (call_indirect (type $s) (local.get 0)))
3179 )
3180 "#;
3181 let wasm = wat::parse_str(wat).expect("parse");
3182 let module = decode_wasm_module(&wasm).expect("decode");
3183 let guards = module.call_indirect_guards();
3184 assert_eq!(guards.tables[0].table_size, Some(3));
3185 assert_eq!(
3186 guards.tables[0].type_reject.first(),
3187 Some(&None),
3188 "initialized slots are homogeneous in $s — the verdict must \
3189 verify despite the null slot (#664): {:?}",
3190 guards.tables[0].type_reject
3191 );
3192 assert!(
3193 guards.tables[0].has_null_slots,
3194 "slot 2 is uninitialized — the lowering must emit the runtime \
3195 null check (#664)"
3196 );
3197 }
3198
3199 /// #664: the falcon shape — a SPARSE table (only slots 1 and 3 of 4
3200 /// initialized, by two separate segments) verifies with the null flag;
3201 /// a sparse table whose INITIALIZED slots are heterogeneous still
3202 /// rejects (the runtime null check cannot discharge a TYPE mismatch).
3203 #[test]
3204 fn test_call_indirect_guards_sparse_table_664() {
3205 let wat = r#"
3206 (module
3207 (type $t (func (param i32) (result i32)))
3208 (table 4 4 funcref)
3209 (func $f1 (type $t) (i32.add (local.get 0) (i32.const 100)))
3210 (func $f3 (type $t) (i32.sub (i32.const 1000) (local.get 0)))
3211 (elem (i32.const 1) $f1)
3212 (elem (i32.const 3) $f3)
3213 (func (export "via") (param i32 i32) (result i32)
3214 (call_indirect (type $t) (local.get 0) (local.get 1)))
3215 )
3216 "#;
3217 let wasm = wat::parse_str(wat).expect("parse");
3218 let module = decode_wasm_module(&wasm).expect("decode");
3219 let guards = module.call_indirect_guards();
3220 assert_eq!(guards.tables[0].table_size, Some(4));
3221 assert_eq!(
3222 guards.tables[0].type_reject.first(),
3223 Some(&None),
3224 "slots 1,3 are homogeneous in $t — verified: {:?}",
3225 guards.tables[0].type_reject
3226 );
3227 assert!(guards.tables[0].has_null_slots, "slots 0,2 are null");
3228
3229 // Heterogeneous INITIALIZED slots in a sparse table: still rejected.
3230 let wat = r#"
3231 (module
3232 (type $t (func (param i32) (result i32)))
3233 (type $u (func (param i32 i32) (result i32)))
3234 (table 4 4 funcref)
3235 (func $f1 (type $t) (local.get 0))
3236 (func $f3 (type $u) (i32.add (local.get 0) (local.get 1)))
3237 (elem (i32.const 1) $f1)
3238 (elem (i32.const 3) $f3)
3239 (func (export "via") (param i32 i32) (result i32)
3240 (call_indirect (type $t) (local.get 0) (local.get 1)))
3241 )
3242 "#;
3243 let wasm = wat::parse_str(wat).expect("parse");
3244 let module = decode_wasm_module(&wasm).expect("decode");
3245 let guards = module.call_indirect_guards();
3246 assert!(
3247 guards.tables[0].type_reject[0].is_some() && guards.tables[0].type_reject[1].is_some(),
3248 "a heterogeneous sparse table must still reject every type: {:?}",
3249 guards.tables[0].type_reject
3250 );
3251 // #676: the sparse-heterogeneous case is now dischargeable at
3252 // runtime too — null slots take the reserved class id 0, so ONE
3253 // sidecar compare covers both the type mismatch and the null trap.
3254 assert!(guards.tables[0].runtime_type_check, "#676 runtime check");
3255 assert_eq!(guards.type_ids_byte_offset, Some(16), "4 pointer slots");
3256 assert_eq!(
3257 guards.type_ids_image,
3258 vec![0, 1, 0, 2],
3259 "nulls at 0/2 carry the reserved id 0; $t slot 1 = class 1, \
3260 $u slot 3 = class 2"
3261 );
3262 }
3263
3264 /// #676: the heterogeneous type-id sidecar — structural duplicate types
3265 /// share one class id (the meld 31-decls/25-distinct shape), null slots
3266 /// take the reserved id 0, and the sidecar base is the total pointer
3267 /// region size. A module with NO heterogeneous table gets NO sidecar
3268 /// (empty image, `None` offset) — homogeneous modules stay untouched.
3269 #[test]
3270 fn test_call_indirect_guards_heterogeneous_sidecar_676() {
3271 let wat = r#"
3272 (module
3273 (type $bin (func (param i32 i32) (result i32)))
3274 (type $un (func (param i32) (result i32)))
3275 (type $bin2 (func (param i32 i32) (result i32)))
3276 (table 5 5 funcref)
3277 (func $add (type $bin) (i32.add (local.get 0) (local.get 1)))
3278 (func $neg (type $un) (i32.sub (i32.const 0) (local.get 0)))
3279 (func $sub (type $bin2) (i32.sub (local.get 0) (local.get 1)))
3280 (elem (i32.const 0) func $add $neg $sub)
3281 (func (export "via2") (param i32 i32) (result i32)
3282 (call_indirect (type $bin)
3283 (local.get 0) (i32.const 3) (local.get 1)))
3284 (func (export "via1") (param i32 i32) (result i32)
3285 (call_indirect (type $un) (local.get 0) (local.get 1)))
3286 )
3287 "#;
3288 let wasm = wat::parse_str(wat).expect("parse");
3289 let module = decode_wasm_module(&wasm).expect("decode");
3290 let guards = module.call_indirect_guards();
3291 assert!(guards.tables[0].runtime_type_check);
3292 assert_eq!(
3293 guards.type_class_ids,
3294 vec![1, 2, 1],
3295 "$bin2 is a structural duplicate of $bin — one class id (#676)"
3296 );
3297 assert_eq!(
3298 guards.type_ids_image,
3299 vec![1, 2, 1, 0, 0],
3300 "slots: $add(bin)=1, $neg(un)=2, $sub(bin2 ≡ bin)=1, null, null"
3301 );
3302 assert_eq!(
3303 guards.type_ids_byte_offset,
3304 Some(20),
3305 "sidecar starts after the 5 pointer words"
3306 );
3307
3308 // Homogeneous module → NO sidecar, no runtime check anywhere.
3309 let wat = r#"
3310 (module
3311 (type $t (func (param i32) (result i32)))
3312 (table 2 2 funcref)
3313 (func $f0 (type $t) (local.get 0))
3314 (func $f1 (type $t) (i32.const 7))
3315 (elem (i32.const 0) func $f0 $f1)
3316 (func (export "via") (param i32 i32) (result i32)
3317 (call_indirect (type $t) (local.get 0) (local.get 1)))
3318 )
3319 "#;
3320 let wasm = wat::parse_str(wat).expect("parse");
3321 let module = decode_wasm_module(&wasm).expect("decode");
3322 let guards = module.call_indirect_guards();
3323 assert!(!guards.tables[0].runtime_type_check);
3324 assert_eq!(guards.type_ids_byte_offset, None, "no heterogeneous table");
3325 assert!(guards.type_ids_image.is_empty());
3326 assert!(guards.type_class_ids.is_empty());
3327 }
3328
3329 /// #642: no table at all → no compile-time bound → table_size None and
3330 /// every type rejected (the lowering declines).
3331 #[test]
3332 fn test_call_indirect_guards_no_table_642() {
3333 let wat = r#"
3334 (module
3335 (func (export "f") (param i32) (result i32) (local.get 0))
3336 )
3337 "#;
3338 let wasm = wat::parse_str(wat).expect("parse");
3339 let module = decode_wasm_module(&wasm).expect("decode");
3340 assert_eq!(module.table_size, None);
3341 assert!(module.table_sizes.is_empty(), "#650: no tables declared");
3342 let guards = module.call_indirect_guards();
3343 assert!(
3344 guards.tables.is_empty(),
3345 "no table → no guard entry → every call_indirect declines"
3346 );
3347 }
3348
3349 /// #642: duplicate-but-structurally-identical types stay interchangeable —
3350 /// the closed-world check compares SIGNATURES, not type indices.
3351 #[test]
3352 fn test_call_indirect_guards_duplicate_types_verified_642() {
3353 let wat = r#"
3354 (module
3355 (type $a (func (result i32)))
3356 (type $b (func (result i32)))
3357 (table 1 funcref)
3358 (elem (i32.const 0) $f)
3359 (func $f (type $a) (i32.const 7))
3360 (func (export "run") (param i32) (result i32)
3361 (call_indirect (type $b) (local.get 0)))
3362 )
3363 "#;
3364 let wasm = wat::parse_str(wat).expect("parse");
3365 let module = decode_wasm_module(&wasm).expect("decode");
3366 let guards = module.call_indirect_guards();
3367 // $f has type $a; the call expects $b — structurally identical, so
3368 // BOTH type indices must verify. (A third type — the export's
3369 // (i32)->i32 — is correctly rejected: different signature.)
3370 assert_eq!(
3371 &guards.tables[0].type_reject[0..2],
3372 &[None, None],
3373 "structural signature comparison must accept duplicate types: {:?}",
3374 guards.tables[0].type_reject
3375 );
3376 assert!(
3377 guards.tables[0].type_reject[2].is_some(),
3378 "the structurally-different third type must still be rejected"
3379 );
3380 }
3381
3382 /// #650: TWO tables become a contiguous R11 region — table 0 at offset 0
3383 /// (byte-identical single-table degeneration), table 1 at
3384 /// `size(table 0) * 4`. Each table gets its OWN size, base offset, and
3385 /// per-type closed-world verdicts (segments only poison the table they
3386 /// target).
3387 #[test]
3388 fn test_call_indirect_guards_multi_table_650() {
3389 // The #650 repro shape: overlapping indices, distinct functions —
3390 // table0[1] != table1[1] (the aliasing canary).
3391 let wat = r#"
3392 (module
3393 (type $t (func (param i32) (result i32)))
3394 (type $u (func (param i32 i32) (result i32)))
3395 (table $t0 3 3 funcref)
3396 (table $t1 2 2 funcref)
3397 (func $a0 (type $t) (i32.add (local.get 0) (i32.const 100)))
3398 (func $a1 (type $t) (i32.add (local.get 0) (i32.const 200)))
3399 (func $a2 (type $t) (i32.add (local.get 0) (i32.const 300)))
3400 (func $b0 (type $u) (i32.add (local.get 0) (local.get 1)))
3401 (func $b1 (type $u) (i32.sub (local.get 0) (local.get 1)))
3402 (elem (table $t0) (i32.const 0) func $a0 $a1 $a2)
3403 (elem (table $t1) (i32.const 0) func $b0 $b1)
3404 (func (export "f") (param i32 i32) (result i32)
3405 (call_indirect $t1 (type $u)
3406 (local.get 0) (i32.const 10) (local.get 1)))
3407 )
3408 "#;
3409 let wasm = wat::parse_str(wat).expect("parse");
3410 let module = decode_wasm_module(&wasm).expect("decode");
3411 assert_eq!(module.table_sizes, vec![Some(3), Some(2)]);
3412 assert_eq!(module.table_size, Some(3), "compat accessor = table 0");
3413 assert_eq!(
3414 module.elem_segments[0].table_index, 0,
3415 "segment 0 targets table 0"
3416 );
3417 assert_eq!(
3418 module.elem_segments[1],
3419 ElemSegmentInfo {
3420 table_index: 1,
3421 offset: Some(0),
3422 funcs: Some(vec![3, 4]),
3423 },
3424 "segment 1 is statically attributed to table 1 (#650)"
3425 );
3426
3427 let guards = module.call_indirect_guards();
3428 assert_eq!(guards.tables.len(), 2);
3429 assert_eq!(guards.tables[0].table_size, Some(3));
3430 assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3431 assert_eq!(guards.tables[1].table_size, Some(2));
3432 assert_eq!(
3433 guards.tables[1].base_byte_offset,
3434 Some(12),
3435 "table 1 base = size(table 0) * 4 within the contiguous R11 region"
3436 );
3437 // Table 0 is homogeneous in $t (type 0); table 1 in $u (type 1) —
3438 // each verifies ITS type and rejects the other's.
3439 assert_eq!(guards.tables[0].type_reject[0], None, "table 0 vs $t");
3440 assert!(guards.tables[0].type_reject[1].is_some(), "table 0 vs $u");
3441 assert!(guards.tables[1].type_reject[0].is_some(), "table 1 vs $t");
3442 assert_eq!(guards.tables[1].type_reject[1], None, "table 1 vs $u");
3443 }
3444
3445 /// #650: an unknown-size table (growable import) declines ITSELF and
3446 /// makes every LATER table's base offset non-constant — but a table
3447 /// BEFORE it is unaffected.
3448 #[test]
3449 fn test_call_indirect_guards_unknown_size_poisons_later_bases_650() {
3450 let wat = r#"
3451 (module
3452 (type $t (func (result i32)))
3453 (import "env" "tbl" (table 4 funcref))
3454 (table $d 1 1 funcref)
3455 (func $f (type $t) (i32.const 7))
3456 (elem (table $d) (i32.const 0) func $f)
3457 (func (export "run") (param i32) (result i32)
3458 (call_indirect $d (type $t) (local.get 0)))
3459 )
3460 "#;
3461 let wasm = wat::parse_str(wat).expect("parse");
3462 let module = decode_wasm_module(&wasm).expect("decode");
3463 assert_eq!(
3464 module.table_sizes,
3465 vec![None, Some(1)],
3466 "growable import (no max) has no sound compile-time size"
3467 );
3468 let guards = module.call_indirect_guards();
3469 assert_eq!(guards.tables[0].base_byte_offset, Some(0));
3470 assert!(
3471 guards.tables[0].type_reject.iter().all(|r| r.is_some()),
3472 "unknown-size table rejects every type"
3473 );
3474 assert_eq!(
3475 guards.tables[1].base_byte_offset, None,
3476 "a later table's base is not a compile-time constant when a \
3477 preceding table's size is unknown (#650)"
3478 );
3479 assert_eq!(guards.tables[1].table_size, Some(1));
3480 }
3481}