Expand description
JIT hook trait — the seam through which lex-bytecode’s
dispatch loop can delegate eligible Op::Call invocations to
a JIT tier without taking a compile-time dependency on the
JIT crate.
§Why a trait
lex-jit already depends on lex-bytecode (for Op,
Function, Value, etc.), so lex-bytecode cannot in turn
depend on lex-jit directly. The trait inverts that: callers
that want JIT register a JitHook implementation on the
Vm at construction; the dispatch loop
consults the hook on each Op::Call and falls through to the
interpreter if it returns Ok(None). No JIT in the build →
vm.jit_hook stays None and the hook check is one branch
on a null option (the optimizer should fold it).
§Step accounting (#465 architectural fix)
try_call takes a raw pointer to the VM’s step counter and
the limit value. JITed code is expected to increment the
counter at every backward jump it emits (loop headers, self-
recursive tail calls) and abort cleanly when the counter
reaches the limit — surfacing a VmError::Panic("step limit exceeded") to the dispatcher.
Without this contract, JITed native loops bypass the
interpreter’s per-op step counter — the
set_step_limit-as-DoS-guard story the interpreter
documents would silently not hold under --jit. With it,
--max-steps is honored on both paths (modulo coarser
granularity in JIT: 1 step per loop iteration vs 1 step per
op for the interpreter; same wall-clock bound to within a
constant factor).
The pointer is valid for the duration of the call — the
dispatcher passes &mut self.steps as *mut u64 from the same
&mut self borrow it just took to invoke the hook. JITed
code dereferences and mutates it; on return the dispatcher
resumes seeing the updated value.
§Contract
Implementations must be observationally equivalent to the interpreter on the calls they accept:
- Effects. Don’t accept calls into effectful functions — the dispatcher doesn’t route effect ops through the hook, so any effect call would be silently dropped.
- Refinements. The dispatch arm runs refinement checks
before calling the hook (
Op::Call’s existing path); hook implementors don’t need to re-check them, but must decline (returnOk(None)) for functions whose refinement evaluation could change observable behavior of the call. The MVP JIT’s eligibility predicate (is_jit_eligible) excludes any function with non-Nonerefinements precisely for this reason. - Memoization. The hook fires after the memo cache check, so a JIT call only happens on memo misses (or functions with memo disabled). This preserves the memo’s observable behavior (same trace-event shape on a hit).
- Tracing. The dispatch arm emits
tracer.enter_callfor the call before invoking the hook; on a hook hit, the arm emitstracer.exit_okitself. Hook implementors must not touch the tracer.
Traits§
- JitHook
- Hook into the VM dispatch loop for
Op::Call.