1#![allow(unsafe_code)]
3
4use std::sync::Arc;
5
6use crate::{Chunk, Fault, Value, Vm, VmResult};
7
8use super::compiler::TraceCompiler;
9use super::exit::{ExitReason, JitReturn};
10use super::frame::{JitCallRecord, JitFrame, MAX_JIT_CALL_DEPTH};
11use super::module_local::with_jit_module;
12use super::runtime::JitRuntime;
13use super::trace::{CompiledTrace, JitContext, TraceKey, HOT_THRESHOLD};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum SyncSlotsResult {
18 Ok,
19 Deopt,
21 Unavailable,
23}
24
25fn try_jit_step(vm: &mut Vm, ctx: &mut JitContext, budget: u32) -> Option<VmResult> {
27 if !Arc::ptr_eq(&ctx.chunk, &vm.chunk_arc()) {
28 return None;
29 }
30 let pc = vm.current_pc()?;
31 let function = vm.current_function();
32 let register_count = vm.current_num_registers()?;
33
34 let mut slots = vec![0i64; slot_capacity(vm)];
35 match sync_slots_from_vm(vm, &mut slots) {
36 SyncSlotsResult::Ok => {}
37 SyncSlotsResult::Deopt | SyncSlotsResult::Unavailable => return None,
38 }
39
40 let key = TraceKey {
41 function,
42 entry_pc: pc as u32,
43 };
44 let exit = try_run_hot(
45 ctx,
46 vm.chunk_arc().as_ref(),
47 key,
48 &mut slots,
49 budget,
50 register_count,
51 None,
52 )?;
53 match apply_exit_to_vm(vm, &slots, exit) {
54 Ok(Some(result)) => Some(result),
55 Ok(None) => None,
56 Err(fault) => Some(VmResult::Trap(fault)),
57 }
58}
59
60fn try_jit_step_runtime(
61 vm: &mut Vm,
62 runtime: &JitRuntime,
63 budget: u32,
64 metrics: Option<&crate::scheduler::RuntimeMetrics>,
65) -> Option<VmResult> {
66 if !runtime.matches_chunk(&vm.chunk_arc()) {
67 return None;
68 }
69 let pc = vm.current_pc()?;
70 let function = vm.current_function();
71 let register_count = vm.current_num_registers()?;
72
73 let mut slots = vec![0i64; slot_capacity(vm)];
74 match sync_slots_from_vm(vm, &mut slots) {
75 SyncSlotsResult::Ok => {}
76 SyncSlotsResult::Deopt => {
77 if let Some(m) = metrics {
78 crate::scheduler::RuntimeMetrics::inc(&m.jit_deopts);
79 }
80 return None;
81 }
82 SyncSlotsResult::Unavailable => return None,
83 }
84
85 let key = TraceKey {
86 function,
87 entry_pc: pc as u32,
88 };
89 let exit = try_run_hot_runtime(
90 runtime,
91 vm.chunk_arc().as_ref(),
92 key,
93 &mut slots,
94 budget,
95 register_count,
96 metrics,
97 )?;
98 match apply_exit_to_vm(vm, &slots, exit) {
99 Ok(Some(result)) => Some(result),
100 Ok(None) => None,
101 Err(fault) => Some(VmResult::Trap(fault)),
102 }
103}
104
105pub fn run_vm_with_jit(vm: &mut Vm, budget: u32, ctx: &mut JitContext) -> VmResult {
108 run_vm_with_jit_loop(vm, budget, |vm, remaining| try_jit_step(vm, ctx, remaining))
109}
110
111pub fn run_vm_with_jit_runtime(
113 vm: &mut Vm,
114 budget: u32,
115 runtime: &JitRuntime,
116 metrics: Option<&crate::scheduler::RuntimeMetrics>,
117) -> VmResult {
118 run_vm_with_jit_loop(vm, budget, |vm, remaining| {
119 try_jit_step_runtime(vm, runtime, remaining, metrics)
120 })
121}
122
123fn run_vm_with_jit_loop(
124 vm: &mut Vm,
125 budget: u32,
126 mut try_step: impl FnMut(&mut Vm, u32) -> Option<VmResult>,
127) -> VmResult {
128 let mut remaining = budget;
129 while remaining > 0 {
130 if let Some(result) = try_step(vm, remaining) {
131 return result;
132 }
133 match vm.run(1) {
134 VmResult::Yield => {
135 remaining -= 1;
136 }
137 other => return other,
138 }
139 }
140 VmResult::Yield
141}
142
143fn slot_capacity(vm: &Vm) -> usize {
144 let chunk = vm.chunk_arc();
145 let max_in_chunk = chunk
146 .functions
147 .iter()
148 .map(|f| f.num_registers as usize)
149 .max();
150 match (max_in_chunk, vm.current_num_registers()) {
151 (Some(max_regs), Some(current)) => max_regs.max(current as usize),
152 (Some(max_regs), None) => max_regs,
153 (None, Some(current)) => current as usize,
154 (None, None) => 0,
155 }
156}
157
158pub fn run_compiled_trace(
160 entry: super::frame::JitEntry,
161 slots: &mut [i64],
162 pc: u32,
163 budget: u32,
164 function: u32,
165 register_count: u32,
166) -> JitReturn {
167 let mut call_stack = [JitCallRecord {
168 return_pc: 0,
169 return_function: 0,
170 dest_reg: 0,
171 caller_register_count: 0,
172 }; MAX_JIT_CALL_DEPTH];
173 let mut frame = JitFrame {
174 slots: slots.as_mut_ptr(),
175 register_count,
176 pc,
177 budget,
178 function,
179 exit_kind: 0,
180 return_reg: 0,
181 call_depth: 0,
182 call_stack: call_stack.as_mut_ptr(),
183 };
184 unsafe { (entry)(&mut frame) };
185 JitReturn {
186 kind: frame.exit_kind,
187 pc: frame.pc,
188 return_reg: frame.return_reg,
189 }
190}
191
192pub fn run_compiled_trace_ref(
194 trace: &CompiledTrace,
195 slots: &mut [i64],
196 pc: u32,
197 budget: u32,
198 function: u32,
199 register_count: u32,
200) -> JitReturn {
201 run_compiled_trace(trace.entry, slots, pc, budget, function, register_count)
202}
203
204pub fn try_run_hot(
207 ctx: &mut JitContext,
208 chunk: &Chunk,
209 key: TraceKey,
210 slots: &mut [i64],
211 budget: u32,
212 register_count: u8,
213 metrics: Option<&crate::scheduler::RuntimeMetrics>,
214) -> Option<ExitReason> {
215 if ctx.cache.get(&key).is_none() {
216 if !ctx.hot.hit(key, ctx.hot_threshold) {
217 if let Some(m) = metrics {
218 crate::scheduler::RuntimeMetrics::inc(&m.jit_misses);
219 }
220 return None;
221 }
222 let mut compiler = TraceCompiler::new(ctx.module_mut());
223 match compiler.compile_trace(chunk, key) {
224 Ok(compiled) => {
225 ctx.cache.insert(key, compiled);
226 if let Some(m) = metrics {
227 crate::scheduler::RuntimeMetrics::inc(&m.jit_compiles);
228 }
229 }
230 Err(_) => {
231 if let Some(m) = metrics {
232 crate::scheduler::RuntimeMetrics::inc(&m.jit_compile_failures);
233 }
234 return None;
235 }
236 }
237 }
238 let trace = ctx.cache.get(&key)?;
239 if let Some(m) = metrics {
240 crate::scheduler::RuntimeMetrics::inc(&m.jit_executions);
241 }
242 let ret = run_compiled_trace_ref(
243 trace,
244 slots,
245 key.entry_pc,
246 budget,
247 key.function,
248 u32::from(register_count),
249 );
250 Some(ret.into_reason())
251}
252
253pub fn try_run_hot_runtime(
255 runtime: &JitRuntime,
256 chunk: &Chunk,
257 key: TraceKey,
258 slots: &mut [i64],
259 budget: u32,
260 register_count: u8,
261 metrics: Option<&crate::scheduler::RuntimeMetrics>,
262) -> Option<ExitReason> {
263 if let Some(copy) = runtime.get_trace(&key) {
264 if let Some(m) = metrics {
265 crate::scheduler::RuntimeMetrics::inc(&m.jit_executions);
266 }
267 let ret = run_compiled_trace(copy.entry, slots, key.entry_pc, budget, key.function, u32::from(register_count));
268 return Some(ret.into_reason());
269 }
270
271 if !runtime.record_hot_hit(key) {
272 if let Some(m) = metrics {
273 crate::scheduler::RuntimeMetrics::inc(&m.jit_misses);
274 }
275 return None;
276 }
277
278 if runtime.get_trace(&key).is_some() {
280 return try_run_hot_runtime(runtime, chunk, key, slots, budget, register_count, metrics);
281 }
282
283 let compiled = match with_jit_module(|module| TraceCompiler::new(module).compile_trace(chunk, key))
284 {
285 Ok(trace) => trace,
286 Err(_) => {
287 if let Some(m) = metrics {
288 crate::scheduler::RuntimeMetrics::inc(&m.jit_compile_failures);
289 }
290 return None;
291 }
292 };
293
294 runtime.insert_trace(key, compiled);
295 if let Some(m) = metrics {
296 crate::scheduler::RuntimeMetrics::inc(&m.jit_compiles);
297 }
298
299 try_run_hot_runtime(runtime, chunk, key, slots, budget, register_count, metrics)
300}
301
302pub fn apply_exit_to_vm(
305 vm: &mut Vm,
306 slots: &[i64],
307 exit: ExitReason,
308) -> Result<Option<VmResult>, Fault> {
309 match exit {
310 ExitReason::Return { return_reg } => {
311 let value = Value::Int(slots[return_reg as usize]);
312 vm.set_register(return_reg, value.clone())?;
313 let pc = vm
314 .current_pc()
315 .ok_or(Fault::Invariant("empty frame stack while running"))?;
316 vm.set_pc(pc.saturating_add(1));
317 vm.return_value(value)
318 }
319 ExitReason::Trap { pc } => {
320 vm.set_pc(pc as usize);
321 Ok(Some(VmResult::Trap(Fault::DivideByZero)))
322 }
323 ExitReason::Budget { pc } => {
324 vm.set_pc(pc as usize);
325 sync_slots_to_vm(vm, slots)?;
326 Ok(None)
327 }
328 ExitReason::Continue { pc } | ExitReason::Effect { pc } | ExitReason::Deopt { pc } => {
329 vm.set_pc(pc as usize);
330 if matches!(exit, ExitReason::Continue { .. } | ExitReason::Effect { .. }) {
331 sync_slots_to_vm(vm, slots)?;
332 }
333 Ok(None)
334 }
335 }
336}
337
338pub fn sync_slots_from_vm(vm: &Vm, slots: &mut [i64]) -> SyncSlotsResult {
340 let Some(regs) = vm.top_registers() else {
341 return SyncSlotsResult::Unavailable;
342 };
343 for (slot, value) in slots.iter_mut().zip(regs.iter()) {
344 match value {
345 Value::Int(i) => *slot = *i,
346 Value::Bool(b) => *slot = i64::from(*b),
347 Value::Unit => *slot = 0,
348 _ => return SyncSlotsResult::Deopt,
349 }
350 }
351 SyncSlotsResult::Ok
352}
353
354pub fn sync_slots_to_vm(vm: &mut Vm, slots: &[i64]) -> Result<(), Fault> {
356 let Some(regs) = vm.top_registers_mut() else {
357 return Err(Fault::Invariant("empty frame stack while syncing slots"));
358 };
359 for (reg, slot) in regs.iter_mut().zip(slots.iter()) {
360 *reg = Value::Int(*slot);
361 }
362 Ok(())
363}
364
365pub const fn hot_threshold() -> u32 {
367 HOT_THRESHOLD
368}
369
370pub fn force_compile(
372 ctx: &mut JitContext,
373 chunk: &Chunk,
374 key: TraceKey,
375) -> Result<(), super::error::CompileError> {
376 if ctx.cache.get(&key).is_none() {
377 let mut compiler = TraceCompiler::new(ctx.module_mut());
378 let compiled = compiler.compile_trace(chunk, key)?;
379 ctx.cache.insert(key, compiled);
380 }
381 Ok(())
382}
383
384#[cfg(test)]
385mod tests {
386 use std::sync::Arc;
387
388 use crate::{bytecode::builder::ChunkBuilder, FlowOutcome, NativeTable, Opcode, Runtime, Value, Vm, VmResult};
389
390 use super::*;
391 use crate::jit::trace::{JitContext, TraceKey};
392
393 type TestResult = Result<(), Box<dyn std::error::Error>>;
394
395 fn add_chunk() -> std::sync::Arc<crate::Chunk> {
396 let mut b = ChunkBuilder::new("jit-add");
397 b.begin_function("main", 0, 3);
398 b.emit_load_imm(0, 41);
399 b.emit_load_imm(1, 1);
400 b.emit_binop(Opcode::Add, 2, 0, 1);
401 b.emit_return(2);
402 Arc::new(b.finish())
403 }
404
405 #[test]
406 fn run_vm_with_jit_completes_scalar_add() -> TestResult {
407 let chunk = add_chunk();
408 let mut ctx = JitContext::new(chunk.clone())?;
409 let key = TraceKey {
410 function: 0,
411 entry_pc: 0,
412 };
413 force_compile(&mut ctx, &chunk, key)?;
414
415 let mut vm = Vm::new(chunk, NativeTable::empty(), 0, &[])?;
416 let result = run_vm_with_jit(&mut vm, 10_000, &mut ctx);
417 assert!(matches!(result, VmResult::Complete(Value::Int(42))));
418 Ok(())
419 }
420
421 #[test]
422 fn native_trace_computes_42() -> TestResult {
423 let chunk = add_chunk();
424 let mut ctx = JitContext::new(chunk.clone())?;
425 let key = TraceKey {
426 function: 0,
427 entry_pc: 0,
428 };
429 force_compile(&mut ctx, &chunk, key)?;
430 let trace = ctx
431 .cache
432 .get(&key)
433 .ok_or("trace missing after force_compile")?;
434 let mut slots = vec![0i64; 3];
435 let ret = run_compiled_trace_ref(trace, &mut slots, 0, 10_000, 0, 3);
436 let exit = ret.into_reason();
437 assert!(matches!(exit, ExitReason::Return { return_reg: 2 }));
438 assert_eq!(slots[2], 42);
439 Ok(())
440 }
441
442 #[test]
443 fn jit_matches_interpreter_on_scalar_add() -> TestResult {
444 let chunk = add_chunk();
445 let rt = Runtime::new((*chunk).clone())?;
446 let interp = rt.spawn(0, &[])?.join();
447 rt.shutdown();
448 assert!(matches!(interp, FlowOutcome::Completed(Value::Int(42))));
449
450 let mut ctx = JitContext::new(chunk.clone())?;
451 let key = TraceKey {
452 function: 0,
453 entry_pc: 0,
454 };
455 let mut slots = vec![0i64; 3];
456 force_compile(&mut ctx, &chunk, key)?;
457 let trace = ctx
458 .cache
459 .get(&key)
460 .ok_or("trace missing after force_compile")?;
461 let exit = run_compiled_trace_ref(trace, &mut slots, 0, 10_000, 0, 3).into_reason();
462 assert!(matches!(exit, ExitReason::Return { return_reg: 2 }));
463 assert_eq!(slots[2], 42);
464
465 let mut vm = Vm::new(chunk, NativeTable::empty(), 0, &[])?;
466 if let Some(result) = apply_exit_to_vm(&mut vm, &slots, exit)? {
467 assert!(matches!(result, VmResult::Complete(Value::Int(42))));
468 } else {
469 return Err("expected Completed(42)".into());
470 }
471 Ok(())
472 }
473
474 #[test]
475 fn div_by_zero_returns_trap_exit() -> TestResult {
476 let mut b = ChunkBuilder::new("jit-div0");
477 b.begin_function("main", 0, 3);
478 b.emit_load_imm(0, 1);
479 b.emit_load_imm(1, 0);
480 b.emit_binop(Opcode::Div, 2, 0, 1);
481 b.emit_return(2);
482 let chunk = Arc::new(b.finish());
483
484 let mut ctx = JitContext::new(chunk.clone())?;
485 let key = TraceKey {
486 function: 0,
487 entry_pc: 0,
488 };
489 force_compile(&mut ctx, &chunk, key)?;
490 let trace = ctx
491 .cache
492 .get(&key)
493 .ok_or("trace missing after force_compile")?;
494 let mut slots = vec![0i64; 3];
495 let ret = run_compiled_trace_ref(trace, &mut slots, 0, 10_000, 0, 3);
496 assert!(matches!(ret.into_reason(), ExitReason::Trap { .. }));
497 Ok(())
498 }
499
500 #[test]
501 fn sync_slots_deopts_on_float() -> TestResult {
502 let chunk = add_chunk();
503 let mut vm = Vm::new(chunk, NativeTable::empty(), 0, &[])?;
504 vm.set_register(0, Value::Float(1.0))?;
505 let mut slots = vec![0i64; 3];
506 assert_eq!(sync_slots_from_vm(&vm, &mut slots), SyncSlotsResult::Deopt);
507 Ok(())
508 }
509
510 #[test]
511 fn runtime_path_compiles_and_runs() -> TestResult {
512 let chunk = add_chunk();
513 let runtime = JitRuntime::new(chunk.clone(), 1);
514 let key = TraceKey {
515 function: 0,
516 entry_pc: 0,
517 };
518 let mut slots = vec![0i64; 3];
519 let exit = try_run_hot_runtime(&runtime, &chunk, key, &mut slots, 10_000, 3, None)
520 .ok_or("expected jit execution")?;
521 assert!(matches!(exit, ExitReason::Return { return_reg: 2 }));
522 assert_eq!(slots[2], 42);
523 Ok(())
524 }
525
526 #[test]
527 fn budget_exit_when_exhausted_mid_trace() -> TestResult {
528 let mut b = ChunkBuilder::new("jit-budget");
529 b.begin_function("main", 0, 1);
530 b.emit_load_imm(0, 1);
531 b.emit_load_imm(0, 2);
532 b.emit_load_imm(0, 3);
533 b.emit_return(0);
534 let chunk = Arc::new(b.finish());
535
536 let mut ctx = JitContext::new(chunk.clone())?;
537 let key = TraceKey {
538 function: 0,
539 entry_pc: 0,
540 };
541 force_compile(&mut ctx, &chunk, key)?;
542 let trace = ctx
543 .cache
544 .get(&key)
545 .ok_or("trace missing after force_compile")?;
546 let mut slots = vec![0i64; 1];
547 let ret = run_compiled_trace_ref(trace, &mut slots, 0, 2, 0, 1);
548 assert!(matches!(ret.into_reason(), ExitReason::Budget { pc: 2 }));
549 Ok(())
550 }
551
552 #[test]
553 fn interleaved_jit_compiles_mid_quantum() -> TestResult {
554 let mut b = ChunkBuilder::new("jit-interleave");
555 b.begin_function("main", 0, 2);
556 b.emit_load_imm(0, 0);
557 let head = b.new_label();
558 b.bind_label(head);
559 b.emit_load_imm(1, 1);
560 b.emit_binop(Opcode::Add, 0, 0, 1);
561 b.emit_load_imm(1, 100);
562 b.emit_binop(Opcode::Lt, 1, 0, 1);
563 let done = b.new_label();
564 b.emit_branch(1, done);
565 b.emit_jump(head);
566 b.bind_label(done);
567 b.emit_return(0);
568 let chunk = Arc::new(b.finish());
569 let head_pc = chunk.functions[0].entry + 1;
570
571 let mut ctx = JitContext::new(chunk.clone())?;
572 ctx.hot_threshold = 2;
573 let mut vm = Vm::new(chunk, NativeTable::empty(), 0, &[])?;
574 let result = run_vm_with_jit(&mut vm, 10_000, &mut ctx);
575 assert!(matches!(result, VmResult::Complete(Value::Int(100))));
576 assert!(ctx
577 .cache
578 .get(&TraceKey {
579 function: 0,
580 entry_pc: head_pc,
581 })
582 .is_some());
583 Ok(())
584 }
585
586 #[test]
587 fn reload_chunk_invalidates_jit_cache() -> TestResult {
588 let chunk = add_chunk();
589 let runtime = JitRuntime::new(chunk.clone(), 1);
590 let key = TraceKey {
591 function: 0,
592 entry_pc: 0,
593 };
594 let mut slots = vec![0i64; 3];
595 let exit = try_run_hot_runtime(&runtime, &chunk, key, &mut slots, 10_000, 3, None)
596 .ok_or("expected jit execution")?;
597 assert!(matches!(exit, ExitReason::Return { .. }));
598
599 let mut b = ChunkBuilder::new("other");
600 b.begin_function("main", 0, 1);
601 b.emit_load_imm(0, 1);
602 b.emit_return(0);
603 runtime.reload(Arc::new(b.finish()));
604 assert!(runtime.get_trace(&key).is_none());
605 Ok(())
606 }
607}