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