Skip to content

Commit 107e732

Browse files
Haofeiclaude
andcommitted
JIT tier-0: real specializing executor (gap-free, 3-way verified)
The tier-0 JIT now actually executes — it was an analysis-only seam before. - `RegVm::run_jit`: a specializing executor for JIT-eligible functions (the numeric/control core). It reuses the interpreter's exact semantics — same `eval_numeric_binary` / `eval_numeric_compare` / `expect_int_ref` helpers and the same `reg`/`set_reg`/`take_reg` register methods — so it is gap-free by construction (no duplicated semantics). - Integrated at the top of `drive`'s frame loop: a fresh JIT-eligible frame runs via `run_jit`, then completes exactly like the `Return` arm. Eligible functions never suspend, so this is safe. Non-eligible functions stay on the interpreter (fallback rule). Eligibility is cached per function (`Rc` identity). - `eval_main_with_args_jit` enables it; `reg_vm_eval_source_main_jit` uses it. Verification (interp == jit == compiled): - the generative differential now puts its arithmetic in a JIT-eligible `compute()` helper, so the JIT path is actually exercised; - hand-written 3-way tests cover parameters (DeepCopy), comparisons + branches (LessInt/JumpIfBool/JumpIfIntCompare), and a loop. Whole workspace 1176 passing. Native machine-code generation (separate crate, forbid(unsafe)) remains the next tier per docs/jit-roadmap.md; it plugs into the same per-function dispatch and the same N-way gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d7b4d15 commit 107e732

2 files changed

Lines changed: 302 additions & 8 deletions

File tree

src/reg_vm.rs

Lines changed: 229 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,7 @@ pub fn reg_vm_eval_source_main_jit(
6565
source: &str,
6666
args: impl IntoIterator<Item = impl Into<String>>,
6767
) -> Result<EvalOutput, EvalError> {
68-
let executable = reg_vm_compile_source(file, source)?;
69-
// Run the eligibility pass on the real program (the JIT "compile" step); its
70-
// result drives native codegen in the next tier. Execution stays on the
71-
// shared interpreter, so output is identical to `reg_vm_eval_source_main`.
72-
let _plan = executable.jit_plan();
73-
executable.eval_main_with_args(args)
68+
reg_vm_compile_source(file, source)?.eval_main_with_args_jit(args)
7469
}
7570

7671
/// Per-program JIT eligibility: how many functions are fully covered by the
@@ -242,6 +237,31 @@ impl RegVmExecutable {
242237
)
243238
}
244239

240+
/// Run `main` with the tier-0 JIT enabled: JIT-eligible functions execute via
241+
/// the specializing executor, the rest via the interpreter. Output is
242+
/// identical to `eval_main_with_args` (verified by the N-way differential).
243+
pub fn eval_main_with_args_jit(
244+
&self,
245+
args: impl IntoIterator<Item = impl Into<String>>,
246+
) -> Result<EvalOutput, EvalError> {
247+
let mut vm = RegVm::new(
248+
Rc::clone(&self.unit),
249+
args.into_iter().map(Into::into).collect(),
250+
HashMap::new(),
251+
);
252+
vm.jit_enabled = true;
253+
let value = vm.run_program("main")?;
254+
let display_value = value.display();
255+
let native_value = value.native_value();
256+
Ok(EvalOutput {
257+
value: display_value.clone(),
258+
display_value,
259+
native_value,
260+
stdout: vm.stdout,
261+
stderr: vm.stderr,
262+
})
263+
}
264+
245265
pub fn eval_main_with_args_and_native_bindings(
246266
&self,
247267
args: impl IntoIterator<Item = impl Into<String>>,
@@ -4716,6 +4736,12 @@ struct RegVm {
47164736
websockets: HashMap<i64, TcpStream>,
47174737
next_pool_id: i64,
47184738
pools: HashMap<i64, VmResourcePool>,
4739+
/// Tier-0 JIT: when set, JIT-eligible functions run via the specializing
4740+
/// executor `run_jit` (which reuses the interpreter's value/register
4741+
/// semantics, so it is gap-free by construction).
4742+
jit_enabled: bool,
4743+
/// Cached JIT eligibility per function (keyed by `Rc` identity).
4744+
jit_eligibility: HashMap<*const RegFunction, bool>,
47194745
}
47204746

47214747
#[derive(Debug, Clone)]
@@ -4785,7 +4811,189 @@ impl RegVm {
47854811
websockets: HashMap::new(),
47864812
next_pool_id: 1,
47874813
pools: HashMap::new(),
4814+
jit_enabled: false,
4815+
jit_eligibility: HashMap::new(),
4816+
}
4817+
}
4818+
4819+
/// Whether `func` is fully covered by the tier-0 JIT instruction subset
4820+
/// (cached by `Rc` identity).
4821+
fn is_jit_eligible(&mut self, func: &Rc<RegFunction>) -> bool {
4822+
let key = Rc::as_ptr(func);
4823+
if let Some(&eligible) = self.jit_eligibility.get(&key) {
4824+
return eligible;
4825+
}
4826+
let eligible = func.code.iter().all(jit_supported_instruction);
4827+
self.jit_eligibility.insert(key, eligible);
4828+
eligible
4829+
}
4830+
4831+
/// Tier-0 JIT executor for a JIT-eligible function. Runs the body via the
4832+
/// same shared helpers (`eval_numeric_binary`, `eval_numeric_compare`, …) and
4833+
/// register methods (`reg`/`set_reg`/`take_reg`) the interpreter uses, so its
4834+
/// result is identical to `drive` by construction. Eligible functions contain
4835+
/// no calls, awaits, or blocking ops, so this never suspends or pushes frames.
4836+
fn run_jit(&mut self, func: &RegFunction, base: usize) -> Result<VmValue, EvalError> {
4837+
let mut ip = 0usize;
4838+
while let Some(instr) = func.code.get(ip) {
4839+
ip += 1;
4840+
match instr {
4841+
RegInstr::LoadUnit { dst } => self.set_reg(base + *dst, VmValue::Unit),
4842+
RegInstr::LoadInt { dst, value } => self.set_reg(base + *dst, VmValue::Int(*value)),
4843+
RegInstr::LoadFloat { dst, value } => {
4844+
self.set_reg(base + *dst, VmValue::Float(*value))
4845+
}
4846+
RegInstr::LoadBool { dst, value } => {
4847+
self.set_reg(base + *dst, VmValue::Bool(*value))
4848+
}
4849+
RegInstr::Move { dst, src } => {
4850+
let value = self.reg(base + *src).clone();
4851+
self.set_reg(base + *dst, value);
4852+
}
4853+
RegInstr::DeepCopy { reg } => {
4854+
let copied = deep_copy_value(self.reg(base + *reg));
4855+
self.set_reg(base + *reg, copied);
4856+
}
4857+
RegInstr::AddInt { dst, lhs, rhs } => {
4858+
let value = eval_numeric_binary(
4859+
BinaryOp::Add,
4860+
self.reg(base + *lhs),
4861+
self.reg(base + *rhs),
4862+
)?;
4863+
self.set_reg(base + *dst, value);
4864+
}
4865+
RegInstr::SubInt { dst, lhs, rhs } => {
4866+
let value = eval_numeric_binary(
4867+
BinaryOp::Subtract,
4868+
self.reg(base + *lhs),
4869+
self.reg(base + *rhs),
4870+
)?;
4871+
self.set_reg(base + *dst, value);
4872+
}
4873+
RegInstr::MulInt { dst, lhs, rhs } => {
4874+
let value = eval_numeric_binary(
4875+
BinaryOp::Multiply,
4876+
self.reg(base + *lhs),
4877+
self.reg(base + *rhs),
4878+
)?;
4879+
self.set_reg(base + *dst, value);
4880+
}
4881+
RegInstr::DivInt { dst, lhs, rhs } => {
4882+
let value = eval_numeric_binary(
4883+
BinaryOp::Divide,
4884+
self.reg(base + *lhs),
4885+
self.reg(base + *rhs),
4886+
)?;
4887+
self.set_reg(base + *dst, value);
4888+
}
4889+
RegInstr::ModInt { dst, lhs, rhs } => {
4890+
let value = eval_numeric_binary(
4891+
BinaryOp::Modulo,
4892+
self.reg(base + *lhs),
4893+
self.reg(base + *rhs),
4894+
)?;
4895+
self.set_reg(base + *dst, value);
4896+
}
4897+
RegInstr::BitAndInt { dst, lhs, rhs } => {
4898+
let l = expect_int_ref(self.reg(base + *lhs))?;
4899+
let r = expect_int_ref(self.reg(base + *rhs))?;
4900+
self.set_reg(base + *dst, VmValue::Int(l & r));
4901+
}
4902+
RegInstr::BitOrInt { dst, lhs, rhs } => {
4903+
let l = expect_int_ref(self.reg(base + *lhs))?;
4904+
let r = expect_int_ref(self.reg(base + *rhs))?;
4905+
self.set_reg(base + *dst, VmValue::Int(l | r));
4906+
}
4907+
RegInstr::BitXorInt { dst, lhs, rhs } => {
4908+
let l = expect_int_ref(self.reg(base + *lhs))?;
4909+
let r = expect_int_ref(self.reg(base + *rhs))?;
4910+
self.set_reg(base + *dst, VmValue::Int(l ^ r));
4911+
}
4912+
RegInstr::ShiftLeftInt { dst, lhs, rhs } => {
4913+
let l = expect_int_ref(self.reg(base + *lhs))?;
4914+
let r = expect_int_ref(self.reg(base + *rhs))?;
4915+
self.set_reg(base + *dst, VmValue::Int(l.wrapping_shl(r.max(0) as u32)));
4916+
}
4917+
RegInstr::ShiftRightInt { dst, lhs, rhs } => {
4918+
let l = expect_int_ref(self.reg(base + *lhs))?;
4919+
let r = expect_int_ref(self.reg(base + *rhs))?;
4920+
self.set_reg(base + *dst, VmValue::Int(l.wrapping_shr(r.max(0) as u32)));
4921+
}
4922+
RegInstr::LessInt { dst, lhs, rhs } => {
4923+
let value = eval_numeric_compare(
4924+
RegIntCompare::Less,
4925+
self.reg(base + *lhs),
4926+
self.reg(base + *rhs),
4927+
)?;
4928+
self.set_reg(base + *dst, VmValue::Bool(value));
4929+
}
4930+
RegInstr::LessEqualInt { dst, lhs, rhs } => {
4931+
let value = eval_numeric_compare(
4932+
RegIntCompare::LessEqual,
4933+
self.reg(base + *lhs),
4934+
self.reg(base + *rhs),
4935+
)?;
4936+
self.set_reg(base + *dst, VmValue::Bool(value));
4937+
}
4938+
RegInstr::GreaterInt { dst, lhs, rhs } => {
4939+
let value = eval_numeric_compare(
4940+
RegIntCompare::Greater,
4941+
self.reg(base + *lhs),
4942+
self.reg(base + *rhs),
4943+
)?;
4944+
self.set_reg(base + *dst, VmValue::Bool(value));
4945+
}
4946+
RegInstr::GreaterEqualInt { dst, lhs, rhs } => {
4947+
let value = eval_numeric_compare(
4948+
RegIntCompare::GreaterEqual,
4949+
self.reg(base + *lhs),
4950+
self.reg(base + *rhs),
4951+
)?;
4952+
self.set_reg(base + *dst, VmValue::Bool(value));
4953+
}
4954+
RegInstr::Equal { dst, lhs, rhs } => {
4955+
let eq = self.reg(base + *lhs) == self.reg(base + *rhs);
4956+
self.set_reg(base + *dst, VmValue::Bool(eq));
4957+
}
4958+
RegInstr::NotEqual { dst, lhs, rhs } => {
4959+
let ne = self.reg(base + *lhs) != self.reg(base + *rhs);
4960+
self.set_reg(base + *dst, VmValue::Bool(ne));
4961+
}
4962+
RegInstr::Jump { target } => ip = *target,
4963+
RegInstr::JumpIfBool {
4964+
cond,
4965+
expected,
4966+
target,
4967+
} => {
4968+
if expect_bool_ref(self.reg(base + *cond))? == *expected {
4969+
ip = *target;
4970+
}
4971+
}
4972+
RegInstr::JumpIfIntCompare {
4973+
lhs,
4974+
rhs,
4975+
op,
4976+
expected,
4977+
target,
4978+
} => {
4979+
let l = self.reg(base + *lhs);
4980+
let r = self.reg(base + *rhs);
4981+
if eval_numeric_compare(*op, l, r)? == *expected {
4982+
ip = *target;
4983+
}
4984+
}
4985+
RegInstr::Return { src } => return Ok(self.take_reg(base + *src)),
4986+
// Eligibility (`jit_supported_instruction`) guarantees only the
4987+
// instructions above reach here. A mismatch would be an internal
4988+
// bug; surface it instead of silently diverging.
4989+
other => {
4990+
return Err(EvalError::Runtime(format!(
4991+
"reg VM JIT reached non-eligible instruction `{other:?}`."
4992+
)));
4993+
}
4994+
}
47884995
}
4996+
Ok(VmValue::Unit)
47894997
}
47904998

47914999
/// Grow the shared register stack so that `stack[..upto]` is addressable.
@@ -5203,6 +5411,21 @@ impl RegVm {
52035411
let base = self.frames.last().expect("active frame").base;
52045412
let next_base = base + func.regs;
52055413
let mut ip = self.frames.last().expect("active frame").ip;
5414+
5415+
// Tier-0 JIT: a fresh JIT-eligible frame runs via the specializing
5416+
// executor (which reuses the interpreter's semantics), then completes
5417+
// exactly like the `Return` arm. Eligible functions never suspend, so
5418+
// they are always entered at `ip == 0`.
5419+
if self.jit_enabled && ip == 0 && self.is_jit_eligible(&func) {
5420+
let value = self.run_jit(&func, base)?;
5421+
let frame = self.frames.pop().expect("active frame");
5422+
if self.frames.len() == floor {
5423+
return Ok(Outcome::Completed(value));
5424+
}
5425+
self.set_reg(frame.ret_dst, value);
5426+
continue 'frames;
5427+
}
5428+
52065429
while let Some(instr) = func.code.get(ip) {
52075430
ip += 1;
52085431
match instr {

tests/backend_differential.rs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,87 @@ fn render_expr(expr: &Expr) -> String {
139139
}
140140

141141
fn render(program: &Program) -> String {
142-
let mut source = String::from("fn main() -> Unit {\n");
142+
// The arithmetic lives in `compute()`, a pure integer/control function that
143+
// is JIT-eligible, so the JIT executor actually runs it; `main` calls it and
144+
// prints, so it stays on the interpreter. This makes the 3-way differential
145+
// exercise the JIT path, not just interp vs compiled.
146+
let mut source = String::from("fn compute() -> Int {\n");
143147
for (index, binding) in program.bindings.iter().enumerate() {
144148
source.push_str(&format!(" let x{index} = {}\n", render_expr(binding)));
145149
}
146150
source.push_str(&format!(" let result = {}\n", render_expr(&program.result)));
147-
source.push_str(" Log.write(message: read String.from_int(value: result))\n");
151+
source.push_str(" return result\n}\n\n");
152+
source.push_str("fn main() -> Unit {\n");
153+
source.push_str(" Log.write(message: read String.from_int(value: compute()))\n");
148154
source.push_str(" return Unit\n}\n");
149155
source
150156
}
151157

158+
/// Eligible function with parameters (exercises DeepCopy + integer arithmetic in
159+
/// the JIT) — interp == jit == compiled.
160+
#[test]
161+
fn backends_agree_on_parameterized_arithmetic() {
162+
let source = "\
163+
fn combine(a: Int, b: Int, c: Int) -> Int {
164+
let scaled = a * 3 - b
165+
return scaled + c * 2
166+
}
167+
168+
fn main() -> Unit {
169+
Log.write(message: read String.from_int(value: combine(a: read 7, b: read 4, c: read 5)))
170+
return Unit
171+
}
172+
";
173+
common::differential::assert_backends_agree("jit-params.rss", source, &[]);
174+
}
175+
176+
/// Eligible function with comparisons and branches (LessInt / JumpIfBool /
177+
/// JumpIfIntCompare in the JIT).
178+
#[test]
179+
fn backends_agree_on_comparison_and_branches() {
180+
let source = "\
181+
fn classify(n: Int) -> Int {
182+
if n < 10 {
183+
return 0
184+
}
185+
if n <= 100 {
186+
return 1
187+
}
188+
return 2
189+
}
190+
191+
fn main() -> Unit {
192+
Log.write(message: read String.from_int(value: classify(n: read 3)))
193+
Log.write(message: read String.from_int(value: classify(n: read 42)))
194+
Log.write(message: read String.from_int(value: classify(n: read 999)))
195+
return Unit
196+
}
197+
";
198+
common::differential::assert_backends_agree("jit-branches.rss", source, &[]);
199+
}
200+
201+
/// Eligible function with a loop (jumps + reassignment in the JIT).
202+
#[test]
203+
fn backends_agree_on_loop() {
204+
let source = "\
205+
fn sum_to(n: Int) -> Int {
206+
let mut total = 0
207+
let mut i = 1
208+
while i <= n {
209+
total = total + i
210+
i = i + 1
211+
}
212+
return total
213+
}
214+
215+
fn main() -> Unit {
216+
Log.write(message: read String.from_int(value: sum_to(n: read 10)))
217+
return Unit
218+
}
219+
";
220+
common::differential::assert_backends_agree("jit-loop.rss", source, &[]);
221+
}
222+
152223
proptest! {
153224
// Each case compiles a crate, so keep the count modest. Raise for a longer
154225
// differential-fuzz session (e.g. PROPTEST_CASES=200).

0 commit comments

Comments
 (0)