Skip to content

Commit 7f8c942

Browse files
committed
pd-vm: JITt'ed fuel budgeting
1 parent b1cab1b commit 7f8c942

8 files changed

Lines changed: 648 additions & 75 deletions

File tree

‎pd-vm/README.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ source syntaxes (`.rss`, `.js`, `.lua`, `.scm`).
1717
Executes compiled compact bytecode rather than interpreting source.
1818
Offers consistent runtime semantics for both synchronous and asynchronous execution.
1919
Includes rich debugging and profiling tools: interactive debugger, recording and replay, and JIT trace insights.
20-
2120
## TODO
2221

23-
- [ ] JIT native path and host call fuel budgeting.
22+
- [ ] host call fuel budgeting.
2423
- [ ] Epoch-based interruption API.
2524
- [ ] Callable-as-value support.
2625

@@ -199,8 +198,8 @@ Fuel charging semantics:
199198
Default interval is `1` (exact mode).
200199
- The interpreter applies fuel checks in the VM loop before opcode fetch/execute.
201200
- Trace-JIT execution applies the same cadence before each `TraceStep`.
202-
- When fuel metering is enabled, native JIT entry falls back to trace-step execution so accounting
203-
stays per-step accurate.
201+
- When fuel metering is enabled, native JIT execution injects fuel checks in generated machine
202+
code at the configured check cadence.
204203
- With interval `> 1`, out-of-fuel detection is coarse-grained: execution may run up to
205204
`interval - 1` extra instructions before the next fuel check.
206205
- If there is not enough fuel, execution returns `VmError::OutOfFuel { needed, remaining }`

‎pd-vm/src/vm/jit/native/aarch64.rs‎

Lines changed: 155 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{
2-
NativeBackend, STATUS_CONTINUE, STATUS_ERROR, STATUS_HALTED, STATUS_TRACE_EXIT, STATUS_WAITING,
3-
STATUS_YIELDED,
2+
NativeBackend, STATUS_CONTINUE, STATUS_ERROR, STATUS_HALTED, STATUS_OUT_OF_FUEL,
3+
STATUS_TRACE_EXIT, STATUS_WAITING, STATUS_YIELDED,
44
};
55
use crate::vm::jit::TraceStep;
66
use crate::vm::{HostCallExecOutcome, Value, Vm, VmError, VmResult};
@@ -11,8 +11,11 @@ pub(super) struct AArch64Backend;
1111
impl NativeBackend for AArch64Backend {
1212
type ExecutableMemory = BackendExecutableMemory;
1313

14-
fn emit_trace_bytes(trace: &crate::vm::jit::JitTrace) -> VmResult<Vec<u8>> {
15-
emit_native_trace_bytes(trace)
14+
fn emit_trace_bytes(
15+
trace: &crate::vm::jit::JitTrace,
16+
fuel_check_interval: Option<u32>,
17+
) -> VmResult<Vec<u8>> {
18+
emit_native_trace_bytes(trace, fuel_check_interval)
1619
}
1720

1821
fn executable_memory_from_code(code: &[u8]) -> VmResult<Self::ExecutableMemory> {
@@ -86,6 +89,9 @@ struct NativeStackLayout {
8689
vm_program_constants_ptr_offset: i32,
8790
vm_program_constants_len_offset: i32,
8891
vm_ip_offset: i32,
92+
vm_fuel_enabled_offset: i32,
93+
vm_fuel_remaining_offset: i32,
94+
vm_fuel_ops_until_check_offset: i32,
8995
stack_vec: VecLayout,
9096
value: ValueLayout,
9197
}
@@ -126,15 +132,38 @@ fn trace_ip_to_u64(ip: usize, context: &str) -> VmResult<u64> {
126132
u64::try_from(ip).map_err(|_| VmError::JitNative(format!("{context} exceeds 64-bit range")))
127133
}
128134

129-
fn emit_native_trace_bytes(trace: &crate::vm::jit::JitTrace) -> VmResult<Vec<u8>> {
135+
fn emit_native_trace_bytes(
136+
trace: &crate::vm::jit::JitTrace,
137+
fuel_check_interval: Option<u32>,
138+
) -> VmResult<Vec<u8>> {
139+
if matches!(fuel_check_interval, Some(0)) {
140+
return Err(VmError::InvalidFuelCheckInterval(0));
141+
}
142+
130143
let layout = detect_native_stack_layout()?;
131144
let mut code = Vec::with_capacity(1024);
132145
let mut status_checks = Vec::new();
133146

134147
emit_native_prologue(&mut code);
135148
emit_status_continue(&mut code);
136149

137-
for step in &trace.steps {
150+
for (step_index, step) in trace.steps.iter().enumerate() {
151+
if let Some(interval) = fuel_check_interval {
152+
let stride = interval as usize;
153+
if step_index % stride == 0 {
154+
let remaining = trace.steps.len().saturating_sub(step_index);
155+
let chunk_len = remaining.min(stride) as u32;
156+
emit_step_with_status!(
157+
code,
158+
status_checks => emit_native_fuel_tick_inline(
159+
&mut code,
160+
layout,
161+
chunk_len,
162+
interval,
163+
)
164+
);
165+
}
166+
}
138167
match step {
139168
TraceStep::Nop => {}
140169
TraceStep::Ldc(index) => {
@@ -359,6 +388,64 @@ fn emit_status_halted(code: &mut Vec<u8>) {
359388
emit_mov_imm64(code, 0, STATUS_HALTED as i64 as u64);
360389
}
361390

391+
fn emit_native_fuel_tick_inline(
392+
code: &mut Vec<u8>,
393+
layout: NativeStackLayout,
394+
steps_to_advance: u32,
395+
fuel_check_interval: u32,
396+
) -> VmResult<()> {
397+
if steps_to_advance == 0 {
398+
return Ok(());
399+
}
400+
401+
emit_ldr_b_disp(code, 14, VM_REG, layout.vm_fuel_enabled_offset)?;
402+
emit_cmp_imm(code, 14, 0)?;
403+
let metering_disabled = emit_b_cond_placeholder(code, Cond::Eq);
404+
405+
emit_ldr_w_disp(code, 15, VM_REG, layout.vm_fuel_ops_until_check_offset)?;
406+
emit_mov_imm64(code, 16, u64::from(steps_to_advance));
407+
emit_cmp_reg(code, 15, 16);
408+
let countdown = emit_b_cond_placeholder(code, Cond::Hi);
409+
410+
emit_ldr_x_disp(code, 17, VM_REG, layout.vm_fuel_remaining_offset)?;
411+
emit_mov_imm64(code, 16, u64::from(fuel_check_interval));
412+
emit_mov_reg(code, 18, 16);
413+
emit_cmp_reg(code, 17, 18);
414+
let out_of_fuel = emit_b_cond_placeholder(code, Cond::Lo);
415+
emit_sub_reg(code, 17, 17, 18);
416+
emit_str_x_disp(code, 17, VM_REG, layout.vm_fuel_remaining_offset)?;
417+
emit_mov_imm64(code, 18, u64::from(steps_to_advance));
418+
emit_sub_reg(code, 18, 18, 15);
419+
emit_mov_imm64(code, 16, u64::from(fuel_check_interval));
420+
emit_sub_reg(code, 16, 16, 18);
421+
emit_str_w_disp(code, 16, VM_REG, layout.vm_fuel_ops_until_check_offset)?;
422+
emit_status_continue(code);
423+
let charge_done = emit_b_placeholder(code);
424+
425+
let countdown_label = code.len();
426+
emit_mov_imm64(code, 16, u64::from(steps_to_advance));
427+
emit_sub_reg(code, 15, 15, 16);
428+
emit_str_w_disp(code, 15, VM_REG, layout.vm_fuel_ops_until_check_offset)?;
429+
emit_status_continue(code);
430+
let countdown_done = emit_b_placeholder(code);
431+
432+
let disabled_label = code.len();
433+
emit_status_continue(code);
434+
let disabled_done = emit_b_placeholder(code);
435+
436+
let out_of_fuel_label = code.len();
437+
emit_mov_imm64(code, 0, STATUS_OUT_OF_FUEL as i64 as u64);
438+
439+
let done_label = code.len();
440+
patch_b_cond_rel19(code, metering_disabled, disabled_label)?;
441+
patch_b_cond_rel19(code, countdown, countdown_label)?;
442+
patch_b_cond_rel19(code, out_of_fuel, out_of_fuel_label)?;
443+
patch_b_rel26(code, charge_done, done_label)?;
444+
patch_b_rel26(code, countdown_done, done_label)?;
445+
patch_b_rel26(code, disabled_done, done_label)?;
446+
Ok(())
447+
}
448+
362449
#[derive(Clone, Copy)]
363450
enum NativeBinaryNumericOp {
364451
Add,
@@ -1489,6 +1576,18 @@ fn detect_native_stack_layout_uncached() -> VmResult<NativeStackLayout> {
14891576
"Vm::program_constants_len offset",
14901577
)?;
14911578
let vm_ip_offset = usize_to_i32(std::mem::offset_of!(Vm, ip), "Vm::ip offset")?;
1579+
let vm_fuel_enabled_offset = usize_to_i32(
1580+
std::mem::offset_of!(Vm, fuel_enabled),
1581+
"Vm::fuel_enabled offset",
1582+
)?;
1583+
let vm_fuel_remaining_offset = usize_to_i32(
1584+
std::mem::offset_of!(Vm, fuel_remaining),
1585+
"Vm::fuel_remaining offset",
1586+
)?;
1587+
let vm_fuel_ops_until_check_offset = usize_to_i32(
1588+
std::mem::offset_of!(Vm, fuel_ops_until_check),
1589+
"Vm::fuel_ops_until_check offset",
1590+
)?;
14921591
let stack_vec = detect_vec_layout()?;
14931592
let value = detect_value_layout()?;
14941593
Ok(NativeStackLayout {
@@ -1497,6 +1596,9 @@ fn detect_native_stack_layout_uncached() -> VmResult<NativeStackLayout> {
14971596
vm_program_constants_ptr_offset,
14981597
vm_program_constants_len_offset,
14991598
vm_ip_offset,
1599+
vm_fuel_enabled_offset,
1600+
vm_fuel_remaining_offset,
1601+
vm_fuel_ops_until_check_offset,
15001602
stack_vec,
15011603
value,
15021604
})
@@ -2495,9 +2597,22 @@ mod tests {
24952597
}
24962598
}
24972599

2600+
fn build_trace(steps: Vec<TraceStep>) -> JitTrace {
2601+
JitTrace {
2602+
id: 0,
2603+
root_ip: 0,
2604+
start_line: None,
2605+
has_call: false,
2606+
has_yielding_call: false,
2607+
steps,
2608+
terminal: JitTraceTerminal::LoopBack,
2609+
executions: 0,
2610+
}
2611+
}
2612+
24982613
fn execute_single_step(vm: &mut Vm, step: TraceStep) -> VmResult<i32> {
24992614
let trace = build_single_step_trace(step);
2500-
let code = emit_native_trace_bytes(&trace)?;
2615+
let code = emit_native_trace_bytes(&trace, None)?;
25012616
let memory = BackendExecutableMemory::from_code(&code)?;
25022617
let entry = unsafe { std::mem::transmute::<*mut u8, NativeEntry>(memory.ptr) };
25032618
clear_bridge_error();
@@ -2507,7 +2622,7 @@ mod tests {
25072622
}
25082623

25092624
fn execute_trace(vm: &mut Vm, trace: JitTrace) -> VmResult<i32> {
2510-
let code = emit_native_trace_bytes(&trace)?;
2625+
let code = emit_native_trace_bytes(&trace, None)?;
25112626
let memory = BackendExecutableMemory::from_code(&code)?;
25122627
let entry = unsafe { std::mem::transmute::<*mut u8, NativeEntry>(memory.ptr) };
25132628
clear_bridge_error();
@@ -2520,7 +2635,7 @@ mod tests {
25202635
vm: &mut Vm,
25212636
trace: JitTrace,
25222637
) -> VmResult<(i32, BackendExecutableMemory)> {
2523-
let code = emit_native_trace_bytes(&trace)?;
2638+
let code = emit_native_trace_bytes(&trace, None)?;
25242639
let memory = BackendExecutableMemory::from_code(&code)?;
25252640
let entry = unsafe { std::mem::transmute::<*mut u8, NativeEntry>(memory.ptr) };
25262641
clear_bridge_error();
@@ -2683,7 +2798,7 @@ mod tests {
26832798
#[test]
26842799
fn add_step_emits_no_helper_call() {
26852800
let trace = build_single_step_trace(TraceStep::Add);
2686-
let code = emit_native_trace_bytes(&trace).expect("trace should compile");
2801+
let code = emit_native_trace_bytes(&trace, None).expect("trace should compile");
26872802
let call_count = code
26882803
.chunks_exact(4)
26892804
.filter(|chunk| {
@@ -2693,14 +2808,41 @@ mod tests {
26932808
assert_eq!(call_count, 0, "add should not emit helper calls");
26942809
}
26952810

2811+
#[test]
2812+
fn fuel_tick_emission_is_optional() {
2813+
let trace = build_single_step_trace(TraceStep::Add);
2814+
let without_fuel_checks = emit_native_trace_bytes(&trace, None).expect("trace should compile");
2815+
let with_fuel_checks =
2816+
emit_native_trace_bytes(&trace, Some(1)).expect("trace should compile");
2817+
assert!(
2818+
with_fuel_checks.len() > without_fuel_checks.len(),
2819+
"fuel checks should add machine code; without={} with={}",
2820+
without_fuel_checks.len(),
2821+
with_fuel_checks.len()
2822+
);
2823+
}
2824+
2825+
#[test]
2826+
fn larger_fuel_intervals_emit_fewer_tick_blocks() {
2827+
let trace = build_trace(vec![TraceStep::Nop; 32]);
2828+
let every_step = emit_native_trace_bytes(&trace, Some(1)).expect("trace should compile");
2829+
let every_eight = emit_native_trace_bytes(&trace, Some(8)).expect("trace should compile");
2830+
assert!(
2831+
every_eight.len() < every_step.len(),
2832+
"expected fewer fuel tick blocks for interval=8; interval=1={} interval=8={}",
2833+
every_step.len(),
2834+
every_eight.len()
2835+
);
2836+
}
2837+
26962838
#[test]
26972839
fn call_step_emits_helper_call() {
26982840
let trace = build_single_step_trace(TraceStep::Call {
26992841
index: 0,
27002842
argc: 1,
27012843
call_ip: 0,
27022844
});
2703-
let code = emit_native_trace_bytes(&trace).expect("trace should compile");
2845+
let code = emit_native_trace_bytes(&trace, None).expect("trace should compile");
27042846
let call_count = code
27052847
.chunks_exact(4)
27062848
.filter(|chunk| {
@@ -2846,7 +2988,7 @@ mod tests {
28462988
#[test]
28472989
fn jump_to_root_step_returns_trace_exit() {
28482990
let trace = build_single_step_trace(TraceStep::JumpToRoot);
2849-
let code = emit_native_trace_bytes(&trace).expect("jump trace should compile");
2991+
let code = emit_native_trace_bytes(&trace, None).expect("jump trace should compile");
28502992
let mut found_cbnz = false;
28512993
for chunk in code.chunks_exact(4) {
28522994
let insn = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
@@ -2905,7 +3047,7 @@ mod tests {
29053047
executions: 0,
29063048
};
29073049

2908-
let code = emit_native_trace_bytes(&trace).expect("all steps should emit");
3050+
let code = emit_native_trace_bytes(&trace, None).expect("all steps should emit");
29093051
assert!(!code.is_empty());
29103052
}
29113053

0 commit comments

Comments
 (0)