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