Skip to main content

logicaffeine_compile/vm/wasm/
region_jit.rs

1//! WS6 (FINISH_INTERPRETER Phase 13) — the browser WASM-JIT tier.
2//!
3//! The native copy-and-patch JIT (`logicaffeine_forge`) patches x86 stencils into
4//! executable memory — impossible in a browser, where there is no executable memory to
5//! patch and only WebAssembly bytecode runs. The only path to JIT-level speed in WASM is a
6//! *second code generator* that emits a fresh WebAssembly module per hot region and
7//! instantiates it via the host's `WebAssembly.instantiate`. The byte emitter is
8//! [`func`](crate::vm::wasm::func); this module is the *tier* around it — the tier-up
9//! bookkeeping plus the host seam that instantiates and calls the emitted modules.
10//!
11//! It lives in the VM crate (not `logicaffeine_forge`) because forge is
12//! `#![cfg(not(target_arch = "wasm32"))]` — it cannot build for wasm32 — whereas this
13//! backend must build for and run on wasm32. The native 2.55×-C x86 JIT is untouched.
14
15use super::func::compile_function_to_wasm;
16use crate::vm::instruction::CompiledProgram;
17
18/// The runtime WASM-JIT tier: per-function call counters plus a cache of compiled +
19/// instantiated modules. When a function crosses the hot threshold it is lowered to WASM and
20/// instantiated; subsequent calls dispatch to the compiled module. Functions the codegen
21/// declines are remembered as `Ineligible` and stay on the bytecode tier. The VM's
22/// `Op::Call` consults this only under `#[cfg(feature = "wasm-jit")]`, so the default build
23/// never carries it.
24///
25/// The host differs by target — and this is the whole point of the WASM backend:
26/// - **native**: the pure-Rust [`wasmi`] interpreter. This is also the codegen oracle the
27///   differential tests cross-check the emitter against.
28/// - **wasm32**: the platform's *real* `WebAssembly` (V8 in the browser / node) via
29///   `js_sys::WebAssembly`. This is the production tier — a hot function tiers up to a
30///   freshly-compiled native WebAssembly module the host JITs, exactly as the spec requires.
31///
32/// `on_call` and the tier-up bookkeeping are target-independent; only [`instantiate`] and
33/// [`ReadyModule::call`] (the host seam) are `#[cfg]`-split.
34pub struct WasmTier {
35    entries: std::collections::HashMap<u16, TierEntry>,
36    threshold: u32,
37    hits: u64,
38}
39
40enum TierEntry {
41    Pending(u32),
42    Ready(ReadyModule),
43    Ineligible,
44}
45
46/// A compiled + instantiated module ready to call. Native holds the `wasmi` store + export;
47/// wasm32 holds the host `WebAssembly.Instance`'s exported function.
48#[cfg(not(target_arch = "wasm32"))]
49struct ReadyModule {
50    store: wasmi::Store<()>,
51    func: wasmi::Func,
52}
53
54#[cfg(target_arch = "wasm32")]
55struct ReadyModule {
56    func: js_sys::Function,
57}
58
59impl ReadyModule {
60    /// Call the module's `f` with i64 args, returning its i64 result.
61    #[cfg(not(target_arch = "wasm32"))]
62    fn call(&mut self, args: &[i64]) -> Option<i64> {
63        let argv: Vec<wasmi::Value> = args.iter().map(|&a| wasmi::Value::I64(a)).collect();
64        let mut results = [wasmi::Value::I64(0)];
65        self.func.call(&mut self.store, &argv, &mut results).ok()?;
66        match results[0] {
67            wasmi::Value::I64(v) => Some(v),
68            _ => None,
69        }
70    }
71
72    #[cfg(target_arch = "wasm32")]
73    fn call(&mut self, args: &[i64]) -> Option<i64> {
74        call_host_func(&self.func, args)
75    }
76}
77
78impl WasmTier {
79    /// A tier that compiles a function after `threshold` calls (clamped to ≥1).
80    pub fn new(threshold: u32) -> Self {
81        WasmTier {
82            entries: std::collections::HashMap::new(),
83            threshold: threshold.max(1),
84            hits: 0,
85        }
86    }
87
88    /// How many calls have dispatched to a compiled WASM module — a diagnostic/test hook
89    /// proving the tier actually fired.
90    pub fn hits(&self) -> u64 {
91        self.hits
92    }
93
94    /// Run `program.functions[func](args)` on the WASM-JIT tier, or `None` to fall back to
95    /// the bytecode tier (not yet hot, or ineligible). A `Some` result is the emitted
96    /// module's output — cross-checked against the VM by the differential tests.
97    pub fn on_call(&mut self, program: &CompiledProgram, func: u16, args: &[i64]) -> Option<i64> {
98        self.entries.entry(func).or_insert(TierEntry::Pending(0));
99        // Count the call; cross the threshold ⇒ compile + instantiate (or mark ineligible).
100        if let Some(TierEntry::Pending(count)) = self.entries.get_mut(&func) {
101            *count += 1;
102            if *count < self.threshold {
103                return None;
104            }
105        }
106        if matches!(self.entries.get(&func), Some(TierEntry::Pending(_))) {
107            match instantiate(program, func) {
108                Some(m) => {
109                    self.entries.insert(func, TierEntry::Ready(m));
110                }
111                None => {
112                    self.entries.insert(func, TierEntry::Ineligible);
113                    return None;
114                }
115            }
116        }
117        // Dispatch to the compiled module.
118        if let Some(TierEntry::Ready(m)) = self.entries.get_mut(&func) {
119            if let Some(v) = m.call(args) {
120                self.hits += 1;
121                return Some(v);
122            }
123        }
124        None
125    }
126}
127
128/// Lower function `func` to WASM and instantiate it through the native `wasmi` host.
129#[cfg(not(target_arch = "wasm32"))]
130fn instantiate(program: &CompiledProgram, func: u16) -> Option<ReadyModule> {
131    let bytes = compile_function_to_wasm(program, func as usize)?;
132    let engine = wasmi::Engine::default();
133    let module = wasmi::Module::new(&engine, &bytes[..]).ok()?;
134    let mut store = wasmi::Store::new(&engine, ());
135    let instance = wasmi::Linker::<()>::new(&engine)
136        .instantiate(&mut store, &module)
137        .ok()?
138        .start(&mut store)
139        .ok()?;
140    let f = instance.get_func(&store, "f")?;
141    Some(ReadyModule { store, func: f })
142}
143
144/// Lower function `func` to WASM and instantiate it through the host's real `WebAssembly`.
145#[cfg(target_arch = "wasm32")]
146fn instantiate(program: &CompiledProgram, func: u16) -> Option<ReadyModule> {
147    let bytes = compile_function_to_wasm(program, func as usize)?;
148    Some(ReadyModule { func: instantiate_on_host(&bytes)? })
149}
150
151/// Compile + instantiate raw WASM bytes through the host's `WebAssembly` (V8 in the browser
152/// / node), returning the module's exported `f`. The constructors `new WebAssembly.Module`
153/// and `new WebAssembly.Instance` are synchronous (unlike `WebAssembly.instantiate`), so
154/// tier-up stays a plain synchronous step inside the VM's `Op::Call`. wasm32 only.
155#[cfg(target_arch = "wasm32")]
156fn instantiate_on_host(bytes: &[u8]) -> Option<js_sys::Function> {
157    use wasm_bindgen::JsCast;
158    let arr = js_sys::Uint8Array::from(bytes);
159    let module = js_sys::WebAssembly::Module::new(arr.as_ref()).ok()?;
160    let instance = js_sys::WebAssembly::Instance::new(&module, &js_sys::Object::new()).ok()?;
161    js_sys::Reflect::get(instance.exports().as_ref(), &wasm_bindgen::JsValue::from_str("f"))
162        .ok()?
163        .dyn_into::<js_sys::Function>()
164        .ok()
165}
166
167/// Call a host `WebAssembly` export taking/returning i64. WebAssembly i64 crosses the JS
168/// boundary as `BigInt`; args are marshaled in as `BigInt`s and the `BigInt` result is read
169/// back losslessly via its base-10 string (no f64 round-trip). wasm32 only.
170#[cfg(target_arch = "wasm32")]
171fn call_host_func(f: &js_sys::Function, args: &[i64]) -> Option<i64> {
172    use wasm_bindgen::JsCast;
173    let arr = js_sys::Array::new();
174    for &a in args {
175        arr.push(&wasm_bindgen::JsValue::from(js_sys::BigInt::from(a)));
176    }
177    let result = f.apply(&wasm_bindgen::JsValue::NULL, &arr).ok()?;
178    let bigint = result.unchecked_into::<js_sys::BigInt>();
179    let decimal = wasm_bindgen::JsValue::from(bigint.to_string(10).ok()?).as_string()?;
180    decimal.parse::<i64>().ok()
181}
182
183/// Instantiate raw WASM bytes through the host's real `WebAssembly` and call export `f` with
184/// i64 args — the browser-native execution primitive the WS6 browser tests use to prove the
185/// emitted modules run on V8 (not just wasmi). wasm32 only.
186#[cfg(target_arch = "wasm32")]
187pub fn run_on_host(bytes: &[u8], args: &[i64]) -> Option<i64> {
188    call_host_func(&instantiate_on_host(bytes)?, args)
189}