Skip to main content

synth_core/
wasm_op.rs

1//! WebAssembly operation patterns — universal input IR for all backends
2//!
3//! Every backend (ARM, aWsm, wasker, w2c2) consumes `WasmOp` sequences.
4//! This enum lives in synth-core so backends can depend on it without
5//! pulling in ARM-specific synthesis types.
6
7use serde::{Deserialize, Serialize};
8
9/// WebAssembly operation patterns
10/// Note: Cannot derive Eq because f32/f64 don't implement Eq (NaN != NaN)
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub enum WasmOp {
13    // Arithmetic
14    I32Add,
15    I32Sub,
16    I32Mul,
17    I32DivS,
18    I32DivU,
19    I32RemS,
20    I32RemU,
21
22    // Bitwise
23    I32And,
24    I32Or,
25    I32Xor,
26    I32Shl,
27    I32ShrS,
28    I32ShrU,
29    I32Rotl,   // Rotate left
30    I32Rotr,   // Rotate right
31    I32Clz,    // Count leading zeros
32    I32Ctz,    // Count trailing zeros
33    I32Popcnt, // Population count (count 1 bits)
34
35    // Sign extension
36    I32Extend8S,  // Sign-extend low 8 bits to 32 bits
37    I32Extend16S, // Sign-extend low 16 bits to 32 bits
38
39    // Comparison
40    I32Eqz, // Equal to zero (unary)
41    I32Eq,
42    I32Ne,
43    I32LtS,
44    I32LtU,
45    I32LeS,
46    I32LeU,
47    I32GtS,
48    I32GtU,
49    I32GeS,
50    I32GeU,
51
52    // Constants
53    I32Const(i32),
54
55    // Memory
56    I32Load {
57        offset: u32,
58        align: u32,
59    },
60    I32Store {
61        offset: u32,
62        align: u32,
63    },
64
65    // Sub-word loads (i32)
66    I32Load8S {
67        offset: u32,
68        align: u32,
69    }, // byte load, sign-extend to i32
70    I32Load8U {
71        offset: u32,
72        align: u32,
73    }, // byte load, zero-extend to i32
74    I32Load16S {
75        offset: u32,
76        align: u32,
77    }, // halfword load, sign-extend to i32
78    I32Load16U {
79        offset: u32,
80        align: u32,
81    }, // halfword load, zero-extend to i32
82
83    // Sub-word stores (i32)
84    I32Store8 {
85        offset: u32,
86        align: u32,
87    }, // store low byte
88    I32Store16 {
89        offset: u32,
90        align: u32,
91    }, // store low halfword
92
93    // Control flow
94    Block,
95    Loop,
96    Br(u32),   // Branch to label
97    BrIf(u32), // Conditional branch
98    BrTable {
99        targets: Vec<u32>,
100        default: u32,
101    },
102    Return,
103    Call(u32),
104    CallIndirect {
105        type_index: u32,
106        table_index: u32,
107    },
108    LocalGet(u32),
109    LocalSet(u32),
110    LocalTee(u32),
111    GlobalGet(u32),
112    GlobalSet(u32),
113
114    // Memory management
115    MemorySize(u32), // returns current memory size in pages (memory index)
116    MemoryGrow(u32), // grow memory by N pages, returns previous size or -1 (memory index)
117
118    // Bulk memory (#374) — single linear memory (memory 0) only; the decoder
119    // loud-skips any non-zero memory index. Each pops (dst, src/val, len) = 3
120    // i32 operands and pushes nothing.
121    MemoryCopy, // memory.copy: copy `len` bytes from `src` to `dst` (memmove semantics)
122    MemoryFill, // memory.fill: set `len` bytes at `dst` to the low byte of `val`
123
124    /// VCR-MEM-002 phase 1 (#406): a load/store whose `memarg` targets a
125    /// NON-DEFAULT linear memory (`memidx > 0`, multi-memory proposal). The
126    /// decoder wraps the plain memory-0 variant instead of DROPPING the index
127    /// (the pre-#406 silent aliasing: every memory lowered to the one R11
128    /// base, so a store to memory `$b` clobbered memory `$a`). Keeping
129    /// memory-0 ops as the bare variants means every existing single-memory
130    /// match arm — and therefore every frozen fixture byte — is untouched by
131    /// construction; the multi-memory-aware path (the `--relocatable` direct
132    /// selector) unwraps this and addresses via the per-memory base symbol
133    /// (`__synth_wasm_data_<k>`), and every other path declines LOUDLY
134    /// (never a silent alias).
135    ///
136    /// `memory.size`/`memory.grow` are NOT wrapped — their variants already
137    /// carry the memory index. Invariant (decoder-enforced): `memory > 0` and
138    /// `op` is never itself a `MultiMemory`.
139    MultiMemory {
140        memory: u32,
141        op: Box<WasmOp>,
142    },
143
144    // More ops
145    Drop,
146    Select,
147    If,
148    Else,
149    End,
150    Unreachable,
151    Nop,
152
153    // ========================================================================
154    // i64 Operations
155    // ========================================================================
156
157    // i64 Arithmetic
158    I64Add,
159    I64Sub,
160    I64Mul,
161    I64DivS,
162    I64DivU,
163    I64RemS,
164    I64RemU,
165
166    // i64 Bitwise
167    I64And,
168    I64Or,
169    I64Xor,
170    I64Shl,
171    I64ShrS,
172    I64ShrU,
173    I64Rotl,
174    I64Rotr,
175    I64Clz,
176    I64Ctz,
177    I64Popcnt,
178
179    // i64 Comparison
180    I64Eqz,
181    I64Eq,
182    I64Ne,
183    I64LtS,
184    I64LtU,
185    I64LeS,
186    I64LeU,
187    I64GtS,
188    I64GtU,
189    I64GeS,
190    I64GeU,
191
192    // i64 Constants and Memory
193    I64Const(i64),
194    I64Load {
195        offset: u32,
196        align: u32,
197    },
198    I64Store {
199        offset: u32,
200        align: u32,
201    },
202
203    // Sub-word loads (i64) — load sub-word, extend to i64
204    I64Load8S {
205        offset: u32,
206        align: u32,
207    },
208    I64Load8U {
209        offset: u32,
210        align: u32,
211    },
212    I64Load16S {
213        offset: u32,
214        align: u32,
215    },
216    I64Load16U {
217        offset: u32,
218        align: u32,
219    },
220    I64Load32S {
221        offset: u32,
222        align: u32,
223    },
224    I64Load32U {
225        offset: u32,
226        align: u32,
227    },
228
229    // Sub-word stores (i64) — store low N bits
230    I64Store8 {
231        offset: u32,
232        align: u32,
233    },
234    I64Store16 {
235        offset: u32,
236        align: u32,
237    },
238    I64Store32 {
239        offset: u32,
240        align: u32,
241    },
242
243    // Conversion operations
244    I64ExtendI32S, // Sign-extend i32 to i64
245    I64ExtendI32U, // Zero-extend i32 to i64
246    I32WrapI64,    // Wrap i64 to i32 (truncate)
247
248    // i64 In-place sign extension
249    I64Extend8S,  // Sign-extend low 8 bits to 64 bits
250    I64Extend16S, // Sign-extend low 16 bits to 64 bits
251    I64Extend32S, // Sign-extend low 32 bits to 64 bits
252
253    // ========================================================================
254    // f32 Operations
255    // ========================================================================
256
257    // f32 Arithmetic
258    F32Add,
259    F32Sub,
260    F32Mul,
261    F32Div,
262
263    // f32 Comparisons
264    F32Eq,
265    F32Ne,
266    F32Lt,
267    F32Le,
268    F32Gt,
269    F32Ge,
270
271    // f32 Math Functions
272    F32Abs,
273    F32Neg,
274    F32Ceil,
275    F32Floor,
276    F32Trunc,
277    F32Nearest,
278    F32Sqrt,
279    F32Min,
280    F32Max,
281    F32Copysign,
282
283    // f32 Constants and Memory
284    F32Const(f32),
285    F32Load {
286        offset: u32,
287        align: u32,
288    },
289    F32Store {
290        offset: u32,
291        align: u32,
292    },
293
294    // f32 Conversions
295    F32ConvertI32S,    // Convert signed i32 to f32
296    F32ConvertI32U,    // Convert unsigned i32 to f32
297    F32ConvertI64S,    // Convert signed i64 to f32
298    F32ConvertI64U,    // Convert unsigned i64 to f32
299    F32DemoteF64,      // Convert f64 to f32
300    F32ReinterpretI32, // Reinterpret i32 bits as f32
301    I32ReinterpretF32, // Reinterpret f32 bits as i32
302    I32TruncF32S,      // Truncate f32 to signed i32
303    I32TruncF32U,      // Truncate f32 to unsigned i32
304
305    // Nontrapping float→int (WASM saturating-float-to-int proposal, 0xFC
306    // prefix). TOTAL ops — never trap: NaN → 0, below INT_MIN → INT_MIN,
307    // above INT_MAX → INT_MAX, else truncate toward zero (§4.3.2 trunc_sat).
308    // Rust emits these for `as` casts, so real modules (falcon, #782) carry
309    // them even when the trapping forms are absent.
310    I32TruncSatF32S, // Saturating truncate f32 to signed i32
311    I32TruncSatF32U, // Saturating truncate f32 to unsigned i32
312    I64TruncSatF32S, // Saturating truncate f32 to signed i64
313    I64TruncSatF32U, // Saturating truncate f32 to unsigned i64
314
315    // ========================================================================
316    // f64 Operations
317    // ========================================================================
318
319    // f64 Arithmetic
320    F64Add,
321    F64Sub,
322    F64Mul,
323    F64Div,
324
325    // f64 Comparisons
326    F64Eq,
327    F64Ne,
328    F64Lt,
329    F64Le,
330    F64Gt,
331    F64Ge,
332
333    // f64 Math Functions
334    F64Abs,
335    F64Neg,
336    F64Ceil,
337    F64Floor,
338    F64Trunc,
339    F64Nearest,
340    F64Sqrt,
341    F64Min,
342    F64Max,
343    F64Copysign,
344
345    // f64 Constants and Memory
346    F64Const(f64),
347    F64Load {
348        offset: u32,
349        align: u32,
350    },
351    F64Store {
352        offset: u32,
353        align: u32,
354    },
355
356    // f64 Conversions
357    F64ConvertI32S,    // Convert signed i32 to f64
358    F64ConvertI32U,    // Convert unsigned i32 to f64
359    F64ConvertI64S,    // Convert signed i64 to f64
360    F64ConvertI64U,    // Convert unsigned i64 to f64
361    F64PromoteF32,     // Convert f32 to f64
362    F64ReinterpretI64, // Reinterpret i64 bits as f64
363    I64ReinterpretF64, // Reinterpret f64 bits as i64
364    I64TruncF64S,      // Truncate f64 to signed i64
365    I64TruncF64U,      // Truncate f64 to unsigned i64
366    I32TruncF64S,      // Truncate f64 to signed i32
367    I32TruncF64U,      // Truncate f64 to unsigned i32
368    // #869: the f32-source i64-target TRAPPING truncations — the only two
369    // members of the 64-bit integer<->float conversion family that had no
370    // WasmOp variant at all (the rest existed but were dropped at decode).
371    I64TruncF32S, // Truncate f32 to signed i64 (traps on NaN/out-of-range)
372    I64TruncF32U, // Truncate f32 to unsigned i64 (traps on NaN/out-of-range)
373
374    // Nontrapping f64→int (saturating-float-to-int, §4.3.2 trunc_sat — see
375    // the f32 group above for the semantics).
376    I32TruncSatF64S, // Saturating truncate f64 to signed i32
377    I32TruncSatF64U, // Saturating truncate f64 to unsigned i32
378    I64TruncSatF64S, // Saturating truncate f64 to signed i64
379    I64TruncSatF64U, // Saturating truncate f64 to unsigned i64
380
381    // ========================================================================
382    // v128 SIMD Operations (WASM SIMD proposal)
383    // ========================================================================
384    // Targets ARM Cortex-M55 Helium MVE (M-Profile Vector Extension)
385
386    // v128 Constants and Memory
387    V128Const([u8; 16]), // 128-bit constant
388    V128Load {
389        offset: u32,
390        align: u32,
391    }, // v128.load
392    V128Store {
393        offset: u32,
394        align: u32,
395    }, // v128.store
396
397    // v128 Bitwise operations
398    V128And,    // v128.and
399    V128Or,     // v128.or
400    V128Xor,    // v128.xor
401    V128Not,    // v128.not
402    V128AndNot, // v128.andnot
403
404    // i8x16 integer SIMD
405    I8x16Add,               // i8x16.add
406    I8x16Sub,               // i8x16.sub
407    I8x16Neg,               // i8x16.neg
408    I8x16Eq,                // i8x16.eq
409    I8x16Ne,                // i8x16.ne
410    I8x16LtS,               // i8x16.lt_s
411    I8x16LtU,               // i8x16.lt_u
412    I8x16GtS,               // i8x16.gt_s
413    I8x16GtU,               // i8x16.gt_u
414    I8x16LeS,               // i8x16.le_s
415    I8x16LeU,               // i8x16.le_u
416    I8x16GeS,               // i8x16.ge_s
417    I8x16GeU,               // i8x16.ge_u
418    I8x16Splat,             // i8x16.splat
419    I8x16ExtractLaneS(u8),  // i8x16.extract_lane_s
420    I8x16ExtractLaneU(u8),  // i8x16.extract_lane_u
421    I8x16ReplaceLane(u8),   // i8x16.replace_lane
422    I8x16Shuffle([u8; 16]), // i8x16.shuffle
423    I8x16Swizzle,           // i8x16.swizzle
424
425    // i16x8 integer SIMD
426    I16x8Add,              // i16x8.add
427    I16x8Sub,              // i16x8.sub
428    I16x8Mul,              // i16x8.mul
429    I16x8Neg,              // i16x8.neg
430    I16x8Eq,               // i16x8.eq
431    I16x8Ne,               // i16x8.ne
432    I16x8LtS,              // i16x8.lt_s
433    I16x8LtU,              // i16x8.lt_u
434    I16x8GtS,              // i16x8.gt_s
435    I16x8GtU,              // i16x8.gt_u
436    I16x8LeS,              // i16x8.le_s
437    I16x8LeU,              // i16x8.le_u
438    I16x8GeS,              // i16x8.ge_s
439    I16x8GeU,              // i16x8.ge_u
440    I16x8Splat,            // i16x8.splat
441    I16x8ExtractLaneS(u8), // i16x8.extract_lane_s
442    I16x8ExtractLaneU(u8), // i16x8.extract_lane_u
443    I16x8ReplaceLane(u8),  // i16x8.replace_lane
444
445    // i32x4 integer SIMD
446    I32x4Add,             // i32x4.add
447    I32x4Sub,             // i32x4.sub
448    I32x4Mul,             // i32x4.mul
449    I32x4Neg,             // i32x4.neg
450    I32x4Eq,              // i32x4.eq
451    I32x4Ne,              // i32x4.ne
452    I32x4LtS,             // i32x4.lt_s
453    I32x4LtU,             // i32x4.lt_u
454    I32x4GtS,             // i32x4.gt_s
455    I32x4GtU,             // i32x4.gt_u
456    I32x4LeS,             // i32x4.le_s
457    I32x4LeU,             // i32x4.le_u
458    I32x4GeS,             // i32x4.ge_s
459    I32x4GeU,             // i32x4.ge_u
460    I32x4Splat,           // i32x4.splat
461    I32x4ExtractLane(u8), // i32x4.extract_lane
462    I32x4ReplaceLane(u8), // i32x4.replace_lane
463
464    // i64x2 integer SIMD
465    I64x2Add,             // i64x2.add
466    I64x2Sub,             // i64x2.sub
467    I64x2Mul,             // i64x2.mul
468    I64x2Neg,             // i64x2.neg
469    I64x2Eq,              // i64x2.eq
470    I64x2Ne,              // i64x2.ne
471    I64x2LtS,             // i64x2.lt_s
472    I64x2GtS,             // i64x2.gt_s
473    I64x2LeS,             // i64x2.le_s
474    I64x2GeS,             // i64x2.ge_s
475    I64x2Splat,           // i64x2.splat
476    I64x2ExtractLane(u8), // i64x2.extract_lane
477    I64x2ReplaceLane(u8), // i64x2.replace_lane
478
479    // f32x4 floating-point SIMD
480    F32x4Add,             // f32x4.add
481    F32x4Sub,             // f32x4.sub
482    F32x4Mul,             // f32x4.mul
483    F32x4Div,             // f32x4.div
484    F32x4Abs,             // f32x4.abs
485    F32x4Neg,             // f32x4.neg
486    F32x4Sqrt,            // f32x4.sqrt
487    F32x4Eq,              // f32x4.eq
488    F32x4Ne,              // f32x4.ne
489    F32x4Lt,              // f32x4.lt
490    F32x4Le,              // f32x4.le
491    F32x4Gt,              // f32x4.gt
492    F32x4Ge,              // f32x4.ge
493    F32x4Splat,           // f32x4.splat
494    F32x4ExtractLane(u8), // f32x4.extract_lane
495    F32x4ReplaceLane(u8), // f32x4.replace_lane
496}
497
498/// The highest local index the body references, +1 (0 when it touches none).
499///
500/// #970 (RQ-57-CONDPARAM): this is the param-count bound every backend uses
501/// when the driver SUPPLIED a declared count, because `min(referenced,
502/// declared)` is EXACT — it names every index that is really a param, and the
503/// clamp means a genuine non-param local can never be mistaken for one.
504///
505/// It replaces the `count_params` access-pattern heuristic on that path, which
506/// was UNSOUND: that heuristic counts only indices READ BEFORE WRITTEN in
507/// LINEAR op order, so a param written before it is read — but only
508/// CONDITIONALLY — was reclassified as a non-param local. Worse, its first
509/// access being a WRITE meant the read-before-write zero-init (#457) skipped it
510/// too, so the branch that does NOT write it read an UNINITIALISED frame slot:
511///
512/// ```wat
513/// (func (export "f") (param i32 i32) (result i32)
514///   (if (local.get 0) (then (local.set 1 (i32.const 5))))
515///   (local.get 1))          ;; f(0, 42): wasmtime 42, synth <previous frame>
516/// ```
517///
518/// Measured on cb80e60c under unicorn with the sub-SP stack poisoned, BOTH the
519/// ARM and RV32 backends returned the poison word rather than 42 — an
520/// information-disclosure shape, not merely a wrong value.
521///
522/// Using `min(referenced, declared)` rather than plain `declared` PRESERVES the
523/// leniency for a function with more declared params than the backend can
524/// register-home that only touches the first few: such a body still lowers,
525/// exactly as before.
526///
527/// Shared by the ARM (`synth-backend`), RISC-V (`synth-backend-riscv`) and
528/// AArch64 (`synth-backend-aarch64`) backends — three private copies of the
529/// same three-line rule is precisely the drift `rewrite_memory_grow_zero`
530/// below was centralised to avoid (#242, VCR-SEL-005).
531pub fn referenced_locals(wasm_ops: &[WasmOp]) -> u32 {
532    wasm_ops
533        .iter()
534        .filter_map(|op| match op {
535            WasmOp::LocalGet(i) | WasmOp::LocalSet(i) | WasmOp::LocalTee(i) => Some(*i + 1),
536            _ => None,
537        })
538        .max()
539        .unwrap_or(0)
540}
541
542/// The read-before-write param-count HEURISTIC, used only when the driver
543/// supplied NO declared count.
544///
545/// RQ-58-MIRRORS (#242): this existed as THREE byte-equivalent private copies —
546/// `synth-backend/src/arm_backend.rs`, `synth-backend-riscv/src/backend.rs` and
547/// `synth-backend-aarch64/src/backend.rs` — differing only in local variable
548/// names and rustfmt line breaks. #974 collapsed [`referenced_locals`], which
549/// REPLACED this heuristic on the declared-count path, but left the heuristic
550/// itself triplicated: the fix was centralised and the bug's original carrier
551/// was not. Three copies of an UNSOUND rule is worse than three copies of a
552/// sound one, because a correction applied to one of them silently does not
553/// reach the other two.
554///
555/// UNSOUND, deliberately kept and deliberately named: it counts only indices
556/// READ BEFORE WRITTEN in LINEAR op order, so a conditionally-written param is
557/// misclassified — see [`referenced_locals`] for the measured
558/// information-disclosure shape (#970). It survives ONLY on the no-declared-
559/// count path (direct `compile_function` callers and hand-built op streams),
560/// where there is no signature to clamp against. Every caller that HAS a
561/// declared count must use `min(referenced_locals(ops), declared)` instead.
562pub fn count_params_heuristic(wasm_ops: &[WasmOp]) -> u32 {
563    let mut first_access: std::collections::HashMap<u32, bool> = std::collections::HashMap::new();
564    for op in wasm_ops {
565        match op {
566            WasmOp::LocalGet(idx) => {
567                first_access.entry(*idx).or_insert(true);
568            }
569            WasmOp::LocalSet(idx) | WasmOp::LocalTee(idx) => {
570                first_access.entry(*idx).or_insert(false);
571            }
572            _ => {}
573        }
574    }
575    first_access
576        .iter()
577        .filter_map(|(&idx, &is_read_first)| if is_read_first { Some(idx + 1) } else { None })
578        .max()
579        .unwrap_or(0)
580}
581
582/// Fold `i32.const 0; memory.grow` → `memory.size` up front, on every backend.
583///
584/// WASM Core §4.4.7: growing a memory by ZERO pages can never fail — it returns
585/// the current size. But every backend's `memory.grow` lowering on FIXED
586/// (non-growable) linear memory returns the "grow failed" sentinel `-1`, which
587/// would wrongly report failure for the legal `memory.grow(0)` "read current
588/// size" idiom. Rewriting the const-0 case to the semantically identical
589/// `memory.size` BEFORE selection fixes it uniformly. (A runtime-variable page
590/// count that happens to be 0 still lowers to `-1` — that is a documented
591/// follow-up, not this fold's concern; only the SYNTACTIC `i32.const 0` form is
592/// the well-known idiom.)
593///
594/// Shared by the ARM (`synth-backend`) and RISC-V (`synth-backend-riscv`)
595/// backend entry points so the two cannot drift (#242, VCR-SEL-005) — it lives
596/// here in `synth-core` next to `WasmOp` because both crates depend on it.
597pub fn rewrite_memory_grow_zero(wasm_ops: &[WasmOp]) -> Vec<WasmOp> {
598    let mut out = Vec::with_capacity(wasm_ops.len());
599    let mut i = 0;
600    while i < wasm_ops.len() {
601        if matches!(wasm_ops[i], WasmOp::I32Const(0))
602            && let Some(WasmOp::MemoryGrow(m)) = wasm_ops.get(i + 1)
603        {
604            out.push(WasmOp::MemorySize(*m));
605            i += 2;
606        } else {
607            out.push(wasm_ops[i].clone());
608            i += 1;
609        }
610    }
611    out
612}
613
614#[cfg(test)]
615mod grow_zero_tests {
616    use super::*;
617
618    #[test]
619    fn folds_const_zero_grow_to_size() {
620        assert_eq!(
621            rewrite_memory_grow_zero(&[WasmOp::I32Const(0), WasmOp::MemoryGrow(0)]),
622            vec![WasmOp::MemorySize(0)]
623        );
624    }
625
626    #[test]
627    fn leaves_nonzero_grow_alone() {
628        assert_eq!(
629            rewrite_memory_grow_zero(&[WasmOp::I32Const(2), WasmOp::MemoryGrow(0)]),
630            vec![WasmOp::I32Const(2), WasmOp::MemoryGrow(0)]
631        );
632    }
633
634    #[test]
635    fn leaves_variable_grow_alone() {
636        assert_eq!(
637            rewrite_memory_grow_zero(&[WasmOp::LocalGet(0), WasmOp::MemoryGrow(0)]),
638            vec![WasmOp::LocalGet(0), WasmOp::MemoryGrow(0)]
639        );
640    }
641
642    #[test]
643    fn preserves_memory_index() {
644        assert_eq!(
645            rewrite_memory_grow_zero(&[WasmOp::I32Const(0), WasmOp::MemoryGrow(3)]),
646            vec![WasmOp::MemorySize(3)]
647        );
648    }
649}