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