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