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//! ## Contract
19//!
20//! Implementations must be *observationally equivalent* to the
21//! interpreter on the calls they accept:
22//!
23//! - **Effects.** Don't accept calls into effectful functions —
24//!   the dispatcher doesn't route effect ops through the hook,
25//!   so any effect call would be silently dropped.
26//! - **Refinements.** The dispatch arm runs refinement checks
27//!   *before* calling the hook (`Op::Call`'s existing path);
28//!   hook implementors don't need to re-check them, but must
29//!   decline (return `Ok(None)`) for functions whose refinement
30//!   evaluation could change observable behavior of the call.
31//!   The MVP JIT's eligibility predicate (`is_jit_eligible`)
32//!   excludes any function with non-`None` refinements precisely
33//!   for this reason.
34//! - **Memoization.** The hook fires *after* the memo cache
35//!   check, so a JIT call only happens on memo misses (or
36//!   functions with memo disabled). This preserves the memo's
37//!   observable behavior (same trace-event shape on a hit).
38//! - **Tracing.** The dispatch arm emits `tracer.enter_call` for
39//!   the call before invoking the hook; on a hook hit, the arm
40//!   emits `tracer.exit_ok` itself. Hook implementors must not
41//!   touch the tracer.
42
43use crate::value::Value;
44use crate::vm::VmError;
45
46/// Hook into the VM dispatch loop for `Op::Call`.
47///
48/// See the module docs for the contract.
49pub trait JitHook: Send {
50    /// The dispatch loop has just verified refinements and missed
51    /// the memo cache for `fn_id`. The arguments are at the top
52    /// of the value stack — `args` is a borrowed view; do not
53    /// mutate.
54    ///
55    /// Return:
56    /// - `Ok(Some(v))` — hook handled the call; the dispatcher
57    ///   will pop `args.len()` values from the stack, push `v`,
58    ///   emit the synthetic `exit_ok` trace event, and continue.
59    /// - `Ok(None)` — hook declines; the dispatcher proceeds with
60    ///   normal frame setup as if the hook weren't installed.
61    /// - `Err(e)` — JITed code raised an error. The dispatcher
62    ///   surfaces it as the call's error.
63    fn try_call(&mut self, fn_id: u32, args: &[Value]) -> Result<Option<Value>, VmError>;
64}