logicaffeine_compile/vm/native_tier.rs
1//! Tier-up seam: the VM profiles calls and hands HOT functions to a pluggable
2//! native backend. The backend (the copy-and-patch JIT in
3//! `logicaffeine_forge`) is injected as a trait object by whatever binary
4//! links both crates — this crate publishes no new dependencies, and WASM
5//! builds simply never install a tier.
6//!
7//! Deopt contract: the native code only handles the integer subset, so the
8//! call site GUARDS — if any argument is not an Int, or compilation bailed,
9//! the bytecode path runs instead. Both paths are differentially tested to
10//! produce identical outcomes.
11
12use std::sync::atomic::{AtomicI64, Ordering};
13use std::sync::Arc;
14
15use serde::{Deserialize, Serialize};
16
17use super::instruction::{Constant, Op};
18use super::value::Value;
19
20/// The per-program native ENTRY TABLE (EXODIA 4.7's hot-swap seam): two
21/// atomic slots per function — [entry pointer, register count] — written
22/// when a function's chain compiles, read by native call stencils through a
23/// patched slot address, and the place Tier-2 swaps optimized code in. The
24/// backing Vec never reallocates (fixed at construction), so slot addresses
25/// are stable for the program's lifetime.
26#[derive(Debug)]
27pub struct FnTable {
28 slots: Vec<AtomicI64>,
29}
30
31impl FnTable {
32 pub fn new(functions: usize) -> Self {
33 FnTable { slots: (0..functions * 2).map(|_| AtomicI64::new(0)).collect() }
34 }
35
36 /// Publish a compiled entry (pointer + callee register count).
37 pub fn publish(&self, fi: usize, entry: i64, register_count: i64) {
38 self.slots[fi * 2 + 1].store(register_count, Ordering::Release);
39 self.slots[fi * 2].store(entry, Ordering::Release);
40 }
41
42 /// The address of function `fi`'s [entry, regcount] slot pair — patched
43 /// into call stencils as an immediate.
44 pub fn slot_addr(&self, fi: usize) -> i64 {
45 &self.slots[fi * 2] as *const AtomicI64 as i64
46 }
47}
48
49/// Per-program cells the native tier shares with every chain it compiles:
50/// the deopt status (any side-exit anywhere unwinds the whole native stack),
51/// and the LIVE LOGOS DEPTH the call stencils count against MAX_CALL_DEPTH.
52/// `Clone` is an `Arc` bump on each cell — a background-compile request carries a
53/// clone so the worker patches the same per-program table/status/depth the
54/// interpreter does.
55#[derive(Debug, Clone)]
56pub struct NativeCtx {
57 pub table: Arc<FnTable>,
58 pub status: Arc<AtomicI64>,
59 pub depth: Arc<AtomicI64>,
60}
61
62/// What one native function call produced.
63#[derive(Debug)]
64pub enum NativeOutcome {
65 /// A fully re-boxed return value (list-returning functions: the
66 /// backend owns the re-boxing because the allocation registry lives
67 /// on its side).
68 ReturnValue(Value),
69 /// Precise side exit: effects already landed stay landed; the VM
70 /// materializes `frames` as real CallFrames and resumes interpreting
71 /// at `resume_pc` (region-grade deopt for functions).
72 DeoptAt { resume_pc: usize, frames: Vec<NativeFrame> },
73 /// The function ran to its return.
74 Return(i64),
75 /// A checked op side-exited (zero divisor). The native code touched only
76 /// its private frame, and every adaptable body is effect-free, so the
77 /// caller simply re-runs the call on bytecode — where the kernel raises
78 /// the exact error at the exact point.
79 Deopt,
80}
81
82/// What one native region run produced.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum RegionOutcome {
85 /// The region ran to its exit; write-back may proceed.
86 Completed,
87 /// Side exit: discard the private frame (NO write-back) and let the
88 /// bytecode loop re-run from the head — VM registers are untouched, so
89 /// the replay recomputes deterministically up to the faulting op.
90 Deopt,
91 /// PRECISE side exit: the region's non-array scalar registers are live in
92 /// the frame; materialize them into the VM registers and resume the
93 /// bytecode AT `resume_pc` (the faulting op) — no replay-from-head, no
94 /// buffer truncate. This is the contract that lets a region with a
95 /// `ListPush` coexisting with an in-place `SetIndex` (BFS-style worklists)
96 /// tier up soundly: completed iterations' effects stand, and the faulting
97 /// op re-runs once on bytecode (raising the exact error or continuing).
98 DeoptAt { resume_pc: usize },
99}
100
101/// A `Return` statement INSIDE the region (the siftDown early-exit shape):
102/// the value slot, its re-boxing kind, and the flag slot the return path
103/// sets — after write-back the VM performs the actual function return.
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub struct RegionReturn {
106 pub flag_slot: u16,
107 pub value_slot: u16,
108 pub kind: RegionReturnKind,
109}
110
111/// How a region-return value travels.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum RegionReturnKind {
114 /// The value slot holds the raw representation, re-boxed by SlotKind.
115 Slot(SlotKind),
116 /// The value slot holds a VM REGISTER NUMBER whose current value
117 /// returns (lists: mutated in place through their pins, the register
118 /// still holds the same Rc).
119 Register,
120}
121
122/// A natively-compiled function: integer registers in, integer result out.
123/// `depth` is the LIVE LOGOS frame count at the call (the callee's own frame
124/// included) — native self-calls count against the same MAX_CALL_DEPTH the
125/// bytecode enforces.
126pub trait NativeFn: Send + Sync {
127 /// `args` are the marshalled parameter slots (scalars by value, floats
128 /// as bits, list params as don't-care placeholders); `pins` are the
129 /// pre-extracted `[vec_handle, ptr, len]` triples, one per list
130 /// parameter in declaration order, landing in the frame's pin slots.
131 fn call(&self, args: &[i64], pins: &[i64], depth: usize) -> NativeOutcome;
132 /// How the result re-boxes: a scalar from the raw i64, or the IDENTITY
133 /// of one of the caller's list arguments (return-by-parameter).
134 fn ret(&self) -> NativeRet {
135 NativeRet::Scalar(SlotKind::Int)
136 }
137 /// The chain's entry pointer, for the program's [`FnTable`].
138 fn entry_ptr(&self) -> i64;
139 /// The regcount value to PUBLISH next to the entry pointer:
140 /// `frame_size − 3`, which the call stencil's bound math and limit
141 /// planting are calibrated against (the plain register count for the
142 /// classic layout).
143 fn published_regc(&self) -> i64;
144}
145
146/// How a native function's return value re-boxes.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum NativeRet {
149 Scalar(SlotKind),
150 /// The function returns its j-th parameter (a list passed through —
151 /// the caller re-boxes by cloning the argument's Rc, preserving
152 /// identity).
153 ListParam(u8),
154 /// The function returns a list BY VEC HANDLE: a registry-owned fresh
155 /// allocation (detach and wrap) or one of the caller's list arguments
156 /// (match the pin handles, clone that argument). The backend resolves
157 /// it and emits [`NativeOutcome::ReturnValue`].
158 ListByHandle,
159}
160
161/// A DECLARED parameter's native representation.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163pub enum ParamKind {
164 Scalar(SlotKind),
165 /// `Seq of <scalar>` — pinned at the boundary exactly like a region
166 /// array (one borrow for the whole call, buffer pointer + length in
167 /// dedicated frame slots).
168 List(PinElem),
169}
170
171/// One materialized native frame for a PRECISE deopt (outermost first).
172/// `offset`/`return_pc`/`return_reg` describe the link from the PREVIOUS
173/// frame (they are meaningless for index 0, whose link belongs to the
174/// interpreted call site).
175#[derive(Debug)]
176pub struct NativeFrame {
177 pub offset: usize,
178 pub return_pc: usize,
179 pub return_reg: u16,
180 pub regs: Vec<i64>,
181 pub kinds: Vec<RegBox>,
182 /// Registers whose values the BACKEND already re-boxed (native-owned
183 /// fresh lists detached from the allocation registry at deopt — they
184 /// must survive into the materialized frame).
185 pub resolved: Vec<(u16, Value)>,
186}
187
188/// How one raw register slot re-boxes during frame materialization.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum RegBox {
191 /// Not defined at this point — leave the register as Nothing.
192 Dead,
193 Int,
194 Bool,
195 Float,
196 /// Holds the identity of list parameter j.
197 ListParam(u8),
198 /// Backend-resolved (see [`NativeFrame::resolved`]); the kind row
199 /// entry is a placeholder.
200 Resolved,
201}
202
203/// A backend that can try to compile one VM function to native code.
204pub trait NativeTier: Send + Sync {
205 /// Attempt to compile the function whose bytecode is `code`
206 /// (`code[0]` is the instruction at `entry_pc`; jump targets inside are
207 /// ABSOLUTE program pcs and need rebasing by `entry_pc`). Return None to
208 /// leave the function on the bytecode path forever.
209 ///
210 /// `callees` carries every program function's declared signature,
211 /// indexed by `FuncIdx` — calls to OTHER functions compile to table
212 /// dispatch when the callee's signature is all-scalar (an unpublished
213 /// callee deopts at the call until it tiers up on its own).
214 #[allow(clippy::too_many_arguments)]
215 fn compile_function(
216 &self,
217 code: &[Op],
218 entry_pc: usize,
219 constants: &[Constant],
220 param_count: u16,
221 register_count: u16,
222 self_fi: u16,
223 param_kinds: &[Option<ParamKind>],
224 ret_kind: Option<SlotKind>,
225 ctx: &NativeCtx,
226 callees: &[CalleeSig],
227 ) -> Option<Box<dyn NativeFn>>;
228
229 /// Attempt to compile a loop region. `code[0]` is the op at `head_pc`
230 /// (the back-edge target); the slice ends at the back-edge jump
231 /// (inclusive). Every jump out of the region must target `exit_pc`.
232 /// `named[r]` marks frame registers carrying a user-visible name — the
233 /// only slots whose post-region values are observable (scratches are
234 /// dead at statement boundaries by the compiler's allocation
235 /// discipline). `observed[r]` is the register's runtime kind at this hot
236 /// crossing — the speculation seed, re-checked by the guard set on every
237 /// entry. Default: regions stay on bytecode.
238 fn compile_region(
239 &self,
240 code: &[Op],
241 head_pc: usize,
242 exit_pc: usize,
243 constants: &[Constant],
244 register_count: u16,
245 named: &[bool],
246 observed: &[ObservedKind],
247 ctx: &NativeCtx,
248 callees: &[CalleeSig],
249 ) -> Option<Box<dyn RegionFn>> {
250 let _ = (
251 code, head_pc, exit_pc, constants, register_count, named, observed, ctx, callees,
252 );
253 None
254 }
255}
256
257/// The runtime kind a native frame slot carries: entry guards check the VM
258/// register's discriminant against it and copy the raw representation in
259/// (f64 travels as bits); write-back re-boxes by it.
260#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
261pub enum SlotKind {
262 Int,
263 Bool,
264 Float,
265}
266
267/// What the VM OBSERVED in each register at the hot back-edge that triggered
268/// region compilation — the kinds the adapter SPECULATES on (sound because
269/// every later entry re-checks them via the guard set).
270#[derive(Clone, Copy, Debug, PartialEq, Eq)]
271pub enum ObservedKind {
272 Int,
273 Bool,
274 Float,
275 /// A List whose payload is the unboxed all-Int repr.
276 IntList,
277 /// A List whose payload is the half-width (`Vec<i32>`) narrowed Int repr
278 /// (`ListRepr::IntsI32`). Pinned as a 4-byte-element buffer (sign-extending
279 /// loads, truncating stores) under `LOGOS_NARROW_VM`.
280 IntListI32,
281 /// A List whose payload is the unboxed all-Float repr.
282 FloatList,
283 /// A List whose payload is the unboxed all-Bool repr.
284 BoolList,
285 /// A Map (the boxed kernel storage rides a pinned pointer; the int
286 /// fast lane verifies keys/values per helper call).
287 Map,
288 /// An ASCII `Text` carried AS BYTES: char index == byte index and char
289 /// count == byte length, so `item i of text` is a pinned 1-byte load and
290 /// char compares are integer byte compares. Observed ONLY when the Text is
291 /// ASCII; a non-ASCII Text stays [`ObservedKind::Other`] (keeps bailing) so
292 /// the per-char decode path runs and the JIT can never diverge.
293 TextBytes,
294 /// Anything the native frame cannot carry (non-ASCII Text, boxed lists,
295 /// Nothing…).
296 Other,
297}
298
299/// One pinned array for a region run: the register holding the list, the
300/// frame slots that receive its buffer pointer and length at entry, and the
301/// element kind the region speculated on. The VM borrows each DISTINCT
302/// `Rc<RefCell<…>>` exactly once for the whole native run (aliased registers
303/// resolve to the same buffer, so native writes through one name are visible
304/// through every other — and there is zero refcount or borrow traffic inside
305/// the loop). Arrays mutate IN PLACE: no write-back, and the deopt replay is
306/// sound by prefix-idempotence (the replay recomputes exactly the values the
307/// native prefix already wrote).
308/// Element kind of a pinned buffer (decides the stencil's access width:
309/// 8-byte for Int/Float bits, 1-byte for Bool).
310#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
311pub enum PinElem {
312 Int,
313 /// A pinned half-width Int buffer (`ListRepr::IntsI32` = `Vec<i32>`): the
314 /// stencil/regalloc access width is 4 bytes — loads sign-extend (`movsxd`),
315 /// stores truncate the low 4 bytes. The narrowing proof guarantees every
316 /// stored value fits `i32`, so truncation is lossless; a region-entry pin
317 /// only admits an `IntsI32` buffer for this lane.
318 IntI32,
319 Float,
320 Bool,
321 /// A pinned MAP: `vec_slot` carries `&mut MapStorage as *mut _`;
322 /// the ptr/len slots are unused. Get/set/contains go through pure
323 /// helpers against the kernel's own storage (iteration order is
324 /// untouched by construction).
325 Map,
326 /// A pinned ASCII `Text` carried AS BYTES: `ptr_slot` receives the string's
327 /// byte buffer pointer (`Rc<String>::as_bytes().as_ptr()`) and `len_slot`
328 /// its BYTE length (== char count for ASCII). Read-only — the `Rc<String>`
329 /// is never mutated in place, so no snapshot/rollback applies; the entry
330 /// pin RE-CHECKS ASCII and declines (deopt) on a non-ASCII Text.
331 TextBytes,
332 /// A pinned MUTABLE `Text` accumulator (`Set text to text + ch`): `vec_slot`
333 /// receives a `*mut Value` pointing AT THE VM REGISTER CELL (the cell is
334 /// stable for the whole native run, even though the `Rc<String>` inside it
335 /// reallocs/COWs on append). The `ptr_slot`/`len_slot` are unused. The
336 /// `logos_rt_str_append` helper grows the accumulator THROUGH this pointer
337 /// with EXACTLY the VM's `add_assign` semantics — in place when the
338 /// `Rc<String>` is sole-owned, copy-on-write (a fresh `Rc` written back into
339 /// the cell, the alias untouched) otherwise — so the tiered run is
340 /// bit-identical to the tree-walker for every alias case. The entry pin
341 /// declines (deopt to bytecode) if the observed value is not a `Text`. A
342 /// classic replay-from-head `Deopt` is NOT replay-idempotent over an
343 /// already-grown accumulator (it would double-append), so the VM SNAPSHOTS
344 /// the register's `Value` on entry and restores it before a classic replay
345 /// (see [`ArrayPin::mutated`]); a precise region resumes at the faulting op
346 /// and keeps the live grown value.
347 TextMut,
348}
349
350#[derive(Clone, Copy, Debug, PartialEq, Eq)]
351pub struct ArrayPin {
352 pub reg: u16,
353 /// Frame slot receiving `&mut Vec<…> as *mut _ as i64` — the handle the
354 /// push helper extends through.
355 pub vec_slot: u16,
356 pub ptr_slot: u16,
357 pub len_slot: u16,
358 pub elem: PinElem,
359 /// This buffer is written IN PLACE (`SetIndex`) in a region that can take a
360 /// recoverable side exit AND replays from the head on deopt (non-precise).
361 /// Such a write is NOT replay-idempotent (a read-modify-write or swap would
362 /// double-apply), so the VM snapshots this buffer's full contents on region
363 /// entry and restores them on a classic `Deopt` before replaying. Read-only
364 /// buffers, push-only buffers (length truncate suffices), and precise
365 /// regions (resume-at-op, no replay) leave this `false`.
366 pub mutated: bool,
367}
368
369/// A natively-compiled LOOP REGION (OSR-lite): no arguments, no return —
370/// every effect flows through the enclosing frame's registers.
371/// What a REGION needs to know about a function it might call: the
372/// declared signature (all-scalar = replay-pure = inlinable) and the
373/// function index for its table slot.
374#[derive(Debug, Clone)]
375pub struct CalleeSig {
376 pub param_kinds: Vec<Option<ParamKind>>,
377 /// Declared scalar return; `None` keeps the callee out of regions.
378 pub ret: Option<SlotKind>,
379 /// LEVER B (a region may CALL a list-parameter function). Sound only when
380 /// the callee never reallocates a list-param buffer (no `Push` reaching a
381 /// list param) — then the caller's derived raw buffer pointer stays valid
382 /// across the call. `true` ⇒ list args don't move under the call.
383 pub list_params_stable: bool,
384 /// Every `Return` traces (through `Move`s) to a list PARAMETER, so the
385 /// returned handle aliases a passed-in (already-pinned) buffer rather than a
386 /// fresh one. Lets a LIST-returning callee be admitted; a scalar return is
387 /// governed by `ret` and ignores this. `false` for non-list / fresh-list
388 /// / multi-element-kind returns.
389 pub returns_list_param: bool,
390}
391
392/// A hoisted region-entry bounds check (V8 loop bound-check elimination): the
393/// loop's covered accesses run unchecked iff, at entry, the pinned array is
394/// long enough for the whole loop and the induction floor is in range. The VM
395/// declines the region (replays on bytecode) if it fails.
396#[derive(Debug, Clone, Copy)]
397pub struct HoistGuard {
398 /// Frame slot holding the pinned array's length (loaded by the prologue).
399 pub len_slot: u16,
400 /// VM register holding the loop's upper bound.
401 pub bound_reg: u16,
402 /// VM register holding the induction variable (its value at entry is the
403 /// loop's minimum for the remaining run).
404 pub iv_reg: u16,
405 /// `length >= bound + add_max` (max access index over the loop).
406 pub add_max: i32,
407 /// `iv + add_min >= 1` (min access index over the loop, 1-based).
408 pub add_min: i32,
409}
410
411pub trait RegionFn: Send + Sync {
412 /// Slots whose CURRENT values the region may read before writing (or
413 /// must preserve across a conditional write): the VM guards each one's
414 /// kind and copies its raw representation into the native frame.
415 fn guard_set(&self) -> &[(u16, SlotKind)];
416 /// Slots whose incoming values are provably DEAD (written before read,
417 /// e.g. the loop-condition scratch): no guard, no copy-in.
418 fn free_set(&self) -> &[u16];
419 /// Slots the region writes, with their re-boxing kinds.
420 fn write_set(&self) -> &[(u16, SlotKind)];
421 /// Arrays the region reads/writes through pinned buffers (see
422 /// [`ArrayPin`]). Default: none.
423 fn array_set(&self) -> &[ArrayPin] {
424 &[]
425 }
426 /// Hoisted region-entry bounds checks (see [`HoistGuard`]). Default: none.
427 fn hoist_guards(&self) -> &[HoistGuard] {
428 &[]
429 }
430 /// In-region `Return` support (see [`RegionReturn`]). Default: none —
431 /// such regions bail.
432 fn region_return(&self) -> Option<RegionReturn> {
433 None
434 }
435 fn frame_size(&self) -> usize;
436 /// Extra arena headroom beyond `frame_size` (regions that CALL
437 /// functions window their callees past the frame and need real call
438 /// arena depth). Default: none.
439 fn arena_slots(&self) -> usize {
440 0
441 }
442 /// PRECISE-deopt re-box kinds at `resume_pc`: one entry per VM register —
443 /// `Some(kind)` re-boxes the frame slot's raw bits, `None` keeps the VM
444 /// register's current value (a pinned array, or a read-only/unknown slot).
445 /// `None` overall ⇒ this region does not use precise deopt. Only meaningful
446 /// for a `RegionOutcome::DeoptAt { resume_pc }` outcome.
447 fn precise_kinds(&self, _resume_pc: usize) -> Option<&[Option<SlotKind>]> {
448 None
449 }
450 fn run(&self, frame: &mut [i64], depth: usize) -> RegionOutcome;
451}
452
453/// The process-wide tier, installed once by the binary that links a backend
454/// (e.g. `logicaffeine-jit`). The live VM constructors attach it to every
455/// program they run; nothing installs it on WASM, so the browser stays pure
456/// bytecode.
457static INSTALLED_TIER: std::sync::OnceLock<&'static (dyn NativeTier + 'static)> =
458 std::sync::OnceLock::new();
459
460/// Install `tier` as the process-wide native tier. Idempotent: the first
461/// install wins and later calls return `false`.
462pub fn install_native_tier(tier: &'static (dyn NativeTier + 'static)) -> bool {
463 INSTALLED_TIER.set(tier).is_ok()
464}
465
466/// The installed process-wide tier, if any.
467pub fn installed_native_tier() -> Option<&'static (dyn NativeTier + 'static)> {
468 INSTALLED_TIER.get().copied()
469}
470
471/// Calls before a function is considered hot.
472pub const NATIVE_TIER_THRESHOLD: u32 = 100;
473
474/// Back-edge crossings before a Main loop is considered hot.
475pub const REGION_TIER_THRESHOLD: u32 = 100;
476
477/// Per-region tier state (keyed by loop-head pc).
478pub(crate) enum RegionSlot {
479 Failed,
480 Ready {
481 rf: Box<dyn RegionFn>,
482 exit_pc: usize,
483 /// Guard failures + side exits since compile. A region that keeps
484 /// missing (bounds-probing loops, repr churn) re-runs work on every
485 /// entry — demote it to Failed so pure bytecode takes over.
486 misses: u32,
487 },
488}
489
490/// Consecutive guard failures / side exits before a Ready region demotes.
491pub(crate) const REGION_DEMOTE_AFTER: u32 = 8;
492
493/// Per-function tier state.
494pub(crate) enum NativeSlot {
495 /// Still profiling (or below threshold).
496 Untried,
497 /// Submitted to the BACKGROUND compiler (HOTSWAP §6); runs bytecode until the
498 /// worker's result is drained and published. Not re-submitted while pending.
499 Pending,
500 /// Compilation was attempted and bailed — never retried.
501 Failed,
502 /// Compiled; the guard still applies per call.
503 Ready(Box<dyn NativeFn>),
504}