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