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