Skip to main content

lex_bytecode/
jit_hook.rs

1//! JIT hook trait — the seam through which `lex-bytecode`'s
2//! dispatch loop can delegate eligible `Op::Call` invocations to
3//! a JIT tier without taking a compile-time dependency on the
4//! JIT crate.
5//!
6//! ## Why a trait
7//!
8//! `lex-jit` already depends on `lex-bytecode` (for `Op`,
9//! `Function`, `Value`, etc.), so `lex-bytecode` cannot in turn
10//! depend on `lex-jit` directly. The trait inverts that: callers
11//! that want JIT register a [`JitHook`] implementation on the
12//! [`Vm`](crate::vm::Vm) at construction; the dispatch loop
13//! consults the hook on each `Op::Call` and falls through to the
14//! interpreter if it returns `Ok(None)`. No JIT in the build →
15//! `vm.jit_hook` stays `None` and the hook check is one branch
16//! on a null option (the optimizer should fold it).
17//!
18//! ## Step accounting (#465 architectural fix)
19//!
20//! `try_call` takes a raw pointer to the VM's step counter and
21//! the limit value. JITed code is expected to increment the
22//! counter at every backward jump it emits (loop headers, self-
23//! recursive tail calls) and abort cleanly when the counter
24//! reaches the limit — surfacing a `VmError::Panic("step limit
25//! exceeded")` to the dispatcher.
26//!
27//! Without this contract, JITed native loops bypass the
28//! interpreter's per-op step counter — the
29//! `set_step_limit`-as-DoS-guard story the interpreter
30//! documents would silently not hold under `--jit`. With it,
31//! `--max-steps` is honored on both paths (modulo coarser
32//! granularity in JIT: 1 step per loop iteration vs 1 step per
33//! op for the interpreter; same wall-clock bound to within a
34//! constant factor).
35//!
36//! The pointer is valid for the duration of the call — the
37//! dispatcher passes `&mut self.steps as *mut u64` from the same
38//! `&mut self` borrow it just took to invoke the hook. JITed
39//! code dereferences and mutates it; on return the dispatcher
40//! resumes seeing the updated value.
41//!
42//! ## Contract
43//!
44//! Implementations must be *observationally equivalent* to the
45//! interpreter on the calls they accept:
46//!
47//! - **Effects.** Don't accept calls into effectful functions —
48//!   the dispatcher doesn't route effect ops through the hook,
49//!   so any effect call would be silently dropped.
50//! - **Refinements.** The dispatch arm runs refinement checks
51//!   *before* calling the hook (`Op::Call`'s existing path);
52//!   hook implementors don't need to re-check them, but must
53//!   decline (return `Ok(None)`) for functions whose refinement
54//!   evaluation could change observable behavior of the call.
55//!   The MVP JIT's eligibility predicate (`is_jit_eligible`)
56//!   excludes any function with non-`None` refinements precisely
57//!   for this reason.
58//! - **Memoization.** The hook fires *after* the memo cache
59//!   check, so a JIT call only happens on memo misses (or
60//!   functions with memo disabled). This preserves the memo's
61//!   observable behavior (same trace-event shape on a hit).
62//! - **Tracing.** The dispatch arm emits `tracer.enter_call` for
63//!   the call before invoking the hook; on a hook hit, the arm
64//!   emits `tracer.exit_ok` itself. Hook implementors must not
65//!   touch the tracer.
66
67use crate::value::Value;
68use crate::vm::VmError;
69
70/// Hook into the VM dispatch loop for `Op::Call`.
71///
72/// See the module docs for the contract.
73pub trait JitHook: Send {
74    /// The dispatch loop has just verified refinements and missed
75    /// the memo cache for `fn_id`. The arguments are at the top
76    /// of the value stack — `args` is a borrowed view; do not
77    /// mutate.
78    ///
79    /// `step_counter_ptr` points at the VM's `steps: u64` field;
80    /// `step_limit` is the current cap. JITed code is expected
81    /// to atomically `*step_counter_ptr += 1` at every backward
82    /// jump (loop iteration) and abort if the new value would
83    /// meet or exceed `step_limit`, surfacing
84    /// `VmError::Panic("step limit exceeded")` so the dispatcher
85    /// can propagate the same shape it would have on the
86    /// interpreter path.
87    ///
88    /// Return:
89    /// - `Ok(Some(v))` — hook handled the call; the dispatcher
90    ///   will pop `args.len()` values from the stack, push `v`,
91    ///   emit the synthetic `exit_ok` trace event, and continue.
92    /// - `Ok(None)` — hook declines; the dispatcher proceeds with
93    ///   normal frame setup as if the hook weren't installed.
94    /// - `Err(e)` — JITed code raised an error (typically
95    ///   `VmError::Panic("step limit exceeded")`). The dispatcher
96    ///   surfaces it as the call's error.
97    fn try_call(
98        &mut self,
99        fn_id: u32,
100        args: &[Value],
101        step_counter_ptr: *mut u64,
102        step_limit: u64,
103    ) -> Result<Option<Value>, VmError>;
104}