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