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}