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