Skip to main content

logicaffeine_compile/vm/wasm/
encode.rs

1//! WebAssembly binary-encoding primitives — the shared byte layer beneath both the browser
2//! WASM-JIT tier (`super::region_jit`) and the direct AOT backend ([`super::module`]).
3//!
4//! Hand-rolled LEB128 + section/instruction encoders (no external wasm crate): the same
5//! routines `wasm_jit.rs` proved correct against `wasmi` and real V8. Nothing here depends on
6//! the `wasm-jit` feature — byte emission is target- and feature-independent; only *running*
7//! the emitted modules (`super::region_jit`) touches `wasmi`/`js_sys`.
8
9use crate::vm::instruction::Reg;
10
11pub(crate) const I64: u8 = 0x7E;
12pub(crate) const I32: u8 = 0x7F;
13pub(crate) const F64: u8 = 0x7C;
14pub(crate) const VOID_BLOCKTYPE: u8 = 0x40;
15
16pub(crate) fn leb_u32(out: &mut Vec<u8>, mut v: u32) {
17    loop {
18        let mut byte = (v & 0x7f) as u8;
19        v >>= 7;
20        if v != 0 {
21            byte |= 0x80;
22        }
23        out.push(byte);
24        if v == 0 {
25            break;
26        }
27    }
28}
29
30pub(crate) fn leb_i64(out: &mut Vec<u8>, mut v: i64) {
31    loop {
32        let byte = (v & 0x7f) as u8;
33        v >>= 7; // arithmetic shift — sign-extends
34        let sign = byte & 0x40 != 0;
35        if (v == 0 && !sign) || (v == -1 && sign) {
36            out.push(byte);
37            break;
38        }
39        out.push(byte | 0x80);
40    }
41}
42
43pub(crate) fn section(out: &mut Vec<u8>, id: u8, content: &[u8]) {
44    out.push(id);
45    leb_u32(out, content.len() as u32);
46    out.extend_from_slice(content);
47}
48
49pub(crate) fn local_get(out: &mut Vec<u8>, idx: u32) {
50    out.push(0x20);
51    leb_u32(out, idx);
52}
53
54pub(crate) fn local_set(out: &mut Vec<u8>, idx: u32) {
55    out.push(0x21);
56    leb_u32(out, idx);
57}
58
59pub(crate) fn local_tee(out: &mut Vec<u8>, idx: u32) {
60    out.push(0x22);
61    leb_u32(out, idx);
62}
63
64pub(crate) fn global_get(out: &mut Vec<u8>, idx: u32) {
65    out.push(0x23);
66    leb_u32(out, idx);
67}
68
69pub(crate) fn global_set(out: &mut Vec<u8>, idx: u32) {
70    out.push(0x24);
71    leb_u32(out, idx);
72}
73
74/// Signed LEB128 of an `i32` (for `i32.const`).
75pub(crate) fn leb_i32(out: &mut Vec<u8>, mut v: i32) {
76    loop {
77        let byte = (v & 0x7f) as u8;
78        v >>= 7; // arithmetic shift — sign-extends
79        let sign = byte & 0x40 != 0;
80        if (v == 0 && !sign) || (v == -1 && sign) {
81            out.push(byte);
82            break;
83        }
84        out.push(byte | 0x80);
85    }
86}
87
88pub(crate) fn i32_const(out: &mut Vec<u8>, v: i32) {
89    out.push(0x41);
90    leb_i32(out, v);
91}
92
93/// `<base on stack> <opcode> align offset` — a linear-memory load/store. `align` is the
94/// natural-alignment log2 (2 for i32/f32, 3 for i64/f64).
95fn mem_op(out: &mut Vec<u8>, opcode: u8, align: u32, offset: u32) {
96    out.push(opcode);
97    leb_u32(out, align);
98    leb_u32(out, offset);
99}
100
101pub(crate) fn i32_load(out: &mut Vec<u8>, offset: u32) {
102    mem_op(out, 0x28, 2, offset);
103}
104pub(crate) fn i32_store(out: &mut Vec<u8>, offset: u32) {
105    mem_op(out, 0x36, 2, offset);
106}
107pub(crate) fn i64_load(out: &mut Vec<u8>, offset: u32) {
108    mem_op(out, 0x29, 3, offset);
109}
110pub(crate) fn i64_store(out: &mut Vec<u8>, offset: u32) {
111    mem_op(out, 0x37, 3, offset);
112}
113pub(crate) fn f64_load(out: &mut Vec<u8>, offset: u32) {
114    mem_op(out, 0x2B, 3, offset);
115}
116pub(crate) fn f64_store(out: &mut Vec<u8>, offset: u32) {
117    mem_op(out, 0x39, 3, offset);
118}
119/// `i32.load8_u` / `i32.store8` — single-byte access (alignment 0), for UTF-8 `Text` buffers.
120pub(crate) fn i32_load8_u(out: &mut Vec<u8>, offset: u32) {
121    mem_op(out, 0x2D, 0, offset);
122}
123pub(crate) fn i32_store8(out: &mut Vec<u8>, offset: u32) {
124    mem_op(out, 0x3A, 0, offset);
125}
126
127/// `local.get lhs; local.get rhs; <opcode>; local.set dst`.
128pub(crate) fn arith(out: &mut Vec<u8>, opcode: u8, dst: Reg, lhs: Reg, rhs: Reg) {
129    local_get(out, lhs as u32);
130    local_get(out, rhs as u32);
131    out.push(opcode);
132    local_set(out, dst as u32);
133}
134
135/// A signed i64 comparison producing a 0/1 *i64* (matching the VM's truthy-Int booleans):
136/// `local.get lhs; local.get rhs; <cmp i32>; i64.extend_i32_u; local.set dst`.
137pub(crate) fn compare(out: &mut Vec<u8>, cmp_opcode: u8, dst: Reg, lhs: Reg, rhs: Reg) {
138    local_get(out, lhs as u32);
139    local_get(out, rhs as u32);
140    out.push(cmp_opcode);
141    out.push(0xAD); // i64.extend_i32_u
142    local_set(out, dst as u32);
143}
144
145/// Emit a CHECKED `add` / `sub`: compute the wrapping i64 result, then `unreachable` (trap)
146/// on signed overflow. This keeps the WASM-JIT IN SYNC with the rest of the engine: the VM's
147/// integer arithmetic is EXACT (`checked_add`/`checked_sub` → promote to BigInt on overflow,
148/// see `semantics/arith.rs`), and the native forge tiers side-exit (`jo`) on overflow for the
149/// same reason. A trap propagates as a host error, so `WasmTier::call` returns `None` and the
150/// task falls back to the bytecode VM, which promotes correctly — never a silent wrap. Signed
151/// overflow test: add ⟺ `(lhs ^ dst) & (rhs ^ dst) < 0`; sub ⟺ `(lhs ^ rhs) & (lhs ^ dst) < 0`.
152pub(crate) fn emit_checked_addsub(out: &mut Vec<u8>, is_sub: bool, dst: Reg, lhs: Reg, rhs: Reg) {
153    local_get(out, lhs as u32);
154    local_get(out, rhs as u32);
155    out.push(if is_sub { 0x7D } else { 0x7C }); // i64.sub / i64.add
156    local_set(out, dst as u32);
157    if is_sub {
158        local_get(out, lhs as u32);
159        local_get(out, rhs as u32);
160        out.push(0x85); // (lhs ^ rhs)
161        local_get(out, lhs as u32);
162        local_get(out, dst as u32);
163        out.push(0x85); // (lhs ^ dst)
164    } else {
165        local_get(out, lhs as u32);
166        local_get(out, dst as u32);
167        out.push(0x85); // (lhs ^ dst)
168        local_get(out, rhs as u32);
169        local_get(out, dst as u32);
170        out.push(0x85); // (rhs ^ dst)
171    }
172    out.push(0x83); // i64.and
173    out.push(0x42);
174    out.push(0x00); // i64.const 0
175    out.push(0x53); // i64.lt_s  → 1 if the overflow word is negative
176    out.push(0x04);
177    out.push(0x40); // if (void)
178    out.push(0x00); // unreachable (overflow → trap → VM fallback)
179    out.push(0x0B); // end
180}
181
182/// Emit a CHECKED `mul`: wrapping i64 product, then trap on signed overflow. Overflow ⟺
183/// `lhs != 0 && dst / lhs != rhs` (the division-check, since WASM has no i128). The
184/// `i64.div_s` runs only on the `lhs != 0` branch; the one input that would still trap it
185/// (`dst == i64::MIN && lhs == -1`) is itself an overflow case, so the trap is the correct
186/// deopt. Same SYNC rationale as [`emit_checked_addsub`] (`checked_mul` → BigInt in the VM).
187pub(crate) fn emit_checked_mul(out: &mut Vec<u8>, dst: Reg, lhs: Reg, rhs: Reg) {
188    local_get(out, lhs as u32);
189    local_get(out, rhs as u32);
190    out.push(0x7E); // i64.mul
191    local_set(out, dst as u32);
192    local_get(out, lhs as u32);
193    out.push(0x50); // i64.eqz → 1 if lhs == 0 (then: no overflow possible)
194    out.push(0x04);
195    out.push(0x40); // if (void)  — lhs == 0: nothing to check
196    out.push(0x05); // else       — lhs != 0: verify dst / lhs == rhs
197    local_get(out, dst as u32);
198    local_get(out, lhs as u32);
199    out.push(0x7F); // i64.div_s  (lhs != 0 here)
200    local_get(out, rhs as u32);
201    out.push(0x52); // i64.ne → 1 if (dst / lhs) != rhs
202    out.push(0x04);
203    out.push(0x40); // if (void)
204    out.push(0x00); // unreachable (overflow → trap → VM fallback)
205    out.push(0x0B); // end (inner if)
206    out.push(0x0B); // end (outer if/else)
207}
208
209/// Emit a conditional jump's terminator: select the next block (target vs fallthrough) by
210/// the condition's truthiness, then re-dispatch. `jump_when_false` is true for
211/// `JumpIfFalse` (jump to `target` when the cond is 0), false for `JumpIfTrue`.
212pub(crate) fn emit_cond_jump(
213    code: &mut Vec<u8>,
214    cond: Reg,
215    jump_when_false: bool,
216    target_block: usize,
217    fallthrough_block: usize,
218    pc_local: u32,
219    loop_depth: u32,
220) {
221    // Stack for `select`: [target, fallthrough, selector] → selector!=0 ? target : fallthrough.
222    code.push(0x41); // i32.const target
223    leb_u32(code, target_block as u32);
224    code.push(0x41); // i32.const fallthrough
225    leb_u32(code, fallthrough_block as u32);
226    local_get(code, cond as u32);
227    if jump_when_false {
228        code.push(0x50); // i64.eqz → i32 1 when cond is 0 (falsey) ⇒ pick target
229    } else {
230        // truthy: cond != 0 ⇒ pick target. (i64.eqz then i32.eqz = "is non-zero".)
231        code.push(0x50); // i64.eqz
232        code.push(0x45); // i32.eqz
233    }
234    code.push(0x1B); // select
235    local_set(code, pc_local);
236    code.push(0x0C); // br $loop
237    leb_u32(code, loop_depth);
238}