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