Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 428 additions & 0 deletions plans/2026-08-26_host-adapter-owned-scope-state.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ pub use vm::{
AotArtifactError, CallOutcome, CallReturn, DEFAULT_MAX_SCRIPT_CALL_DEPTH, EpochCheckpoint,
EpochHandle, FuelCheckpoint, HostArgsFunction, HostAsyncBridge, HostBindingPlan, HostFunction,
HostFunctionRegistry, HostOpId, HostStackFunction, IntoScriptValue, QueuedScriptInvocation,
ScriptArgs, ScriptCallback, ScriptResult, StaticHostArgsFunction, StaticHostFunction,
StaticHostStackFunction, Store, Vm, VmError, VmResult, VmStatus, VmYieldReason,
ResourceCloseReason, ScriptArgs, ScriptCallback, ScriptResult, StaticHostArgsFunction,
StaticHostFunction, StaticHostStackFunction, Store, Vm, VmError, VmResult, VmStatus,
VmYieldReason, execution_scope, operation, resource,
};
#[cfg(feature = "runtime")]
pub use vmbc::{
Expand Down
23 changes: 15 additions & 8 deletions src/vm/aot/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,12 @@ impl From<crate::WireError> for AotArtifactError {

impl Vm {
pub fn encode_aot_artifact(&mut self) -> Result<Vec<u8>, AotArtifactError> {
if self.aot_program.is_none() {
if self.engine.aot_program.is_none() {
self.compile_aot()?;
}
let program_hash = self.ensure_program_cache_key();
let aot_program = self
.engine
.aot_program
.as_ref()
.ok_or(AotArtifactError::MissingAotProgram)?;
Expand All @@ -135,8 +136,8 @@ impl Vm {
} else {
CompiledProgram::from_code(decoded.code, decoded.resume_ips)?
};
self.aot_program = Some(compiled);
self.aot_exec_count = 0;
self.engine.aot_program = Some(compiled);
self.engine.aot_exec_count = 0;
Ok(())
}

Expand All @@ -159,8 +160,8 @@ impl Vm {
} else {
CompiledProgram::from_code(decoded.code, decoded.resume_ips)?
};
vm.aot_program = Some(compiled);
vm.aot_exec_count = 0;
vm.engine.aot_program = Some(compiled);
vm.engine.aot_exec_count = 0;
Ok(vm)
}

Expand Down Expand Up @@ -201,7 +202,11 @@ fn encode_artifact(
write_string("os", std::env::consts::OS, &mut out)?;
write_string("backend", selected_codegen_backend(), &mut out)?;

write_u32("vm ip offset", std::mem::offset_of!(Vm, ip), &mut out)?;
write_u32(
"vm ip offset",
std::mem::offset_of!(Vm, instance.ip),
&mut out,
)?;
write_u32(
"native helper offset",
helper_entry_offset() as usize,
Expand Down Expand Up @@ -283,7 +288,7 @@ fn decode_artifact(
)?;
validate_runtime_field(
"vm ip offset",
std::mem::offset_of!(Vm, ip).to_string(),
std::mem::offset_of!(Vm, instance.ip).to_string(),
cursor.read_u32()?.to_string(),
)?;
validate_runtime_field(
Expand Down Expand Up @@ -446,7 +451,8 @@ mod tests {
bc.ret();
let mut vm = Vm::new(Program::new(Vec::new(), bc.finish()));
vm.compile_aot().expect("aot compile should succeed");
vm.aot_program
vm.engine
.aot_program
.as_mut()
.expect("compiled program")
.interpreter_boundary_only = true;
Expand All @@ -468,6 +474,7 @@ mod tests {
.expect("boundary artifact should load");
assert!(
standalone
.engine
.aot_program
.as_ref()
.expect("loaded aot program")
Expand Down
2 changes: 1 addition & 1 deletion src/vm/aot/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ fn compile_ssa(
let ctx_setup_elapsed = ctx_setup_started.elapsed();

let vm_ip_offset =
i32::try_from(std::mem::offset_of!(Vm, ip)).expect("Vm::ip offset must fit i32");
i32::try_from(std::mem::offset_of!(Vm, instance.ip)).expect("Vm::ip offset must fit i32");
let code_len_i64 = i64::try_from(program.code.len())
.map_err(|_| AotCompileError::Codegen("program length does not fit i64".to_string()))?;

Expand Down
61 changes: 37 additions & 24 deletions src/vm/aot/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,33 @@ use crate::vm::{ExecOutcome, Vm, VmError, VmResult};

impl Vm {
pub fn compile_aot(&mut self) -> VmResult<()> {
self.aot_program = Some(compile_program(self.program())?);
self.aot_exec_count = 0;
self.engine.aot_program = Some(compile_program(self.program())?);
self.engine.aot_exec_count = 0;
Ok(())
}

pub fn clear_aot(&mut self) {
self.aot_program = None;
self.aot_exec_count = 0;
self.engine.aot_program = None;
self.engine.aot_exec_count = 0;
}

pub fn has_aot_program(&self) -> bool {
self.aot_program.is_some()
self.engine.aot_program.is_some()
}

pub fn aot_exec_count(&self) -> u64 {
self.aot_exec_count
self.engine.aot_exec_count
}

pub fn aot_resume_ips(&self) -> Option<&[usize]> {
self.aot_program
self.engine
.aot_program
.as_ref()
.map(|program| program.resume_ips.as_ref())
}

pub fn dump_aot_info(&self) -> String {
let Some(program) = self.aot_program.as_ref() else {
let Some(program) = self.engine.aot_program.as_ref() else {
return "whole-program aot: disabled\n".to_string();
};

Expand All @@ -43,7 +44,10 @@ impl Vm {
" native codegen backend: {}\n",
selected_codegen_backend()
));
out.push_str(&format!(" aot executions: {}\n", self.aot_exec_count));
out.push_str(&format!(
" aot executions: {}\n",
self.engine.aot_exec_count
));
out.push_str(&format!(" code_bytes={}\n", program.code.len()));
out.push_str(&format!(
" lowering={}\n",
Expand All @@ -58,38 +62,47 @@ impl Vm {
}

pub(crate) fn execute_aot_entry(&mut self) -> VmResult<ExecOutcome> {
let Some(entry) = self.aot_program.as_ref().map(|program| program.entry) else {
let Some(entry) = self
.engine
.aot_program
.as_ref()
.map(|program| program.entry)
else {
return Ok(ExecOutcome::Continue);
};

clear_bridge_error();
unsafe { crate::vm::native::prepare_for_execution() };
let status = unsafe { entry(self as *mut Vm) };
self.aot_exec_count = self.aot_exec_count.saturating_add(1);
self.engine.aot_exec_count = self.engine.aot_exec_count.saturating_add(1);

match status {
STATUS_CONTINUE | STATUS_LINKED_CONTINUE => Ok(ExecOutcome::Continue),
STATUS_HALTED => Ok(ExecOutcome::Halted),
STATUS_YIELDED => {
self.last_yield_reason = Some(super::super::VmYieldReason::Host);
self.instance.last_yield_reason = Some(super::super::VmYieldReason::Host);
Ok(ExecOutcome::Yielded)
}
STATUS_WAITING => {
let op_id = self.waiting_host_op.map(|op| op.op_id).ok_or_else(|| {
VmError::JitNative(
"aot call bridge reported waiting without a pending op".to_string(),
)
})?;
let op_id = self
.instance
.waiting_host_op
.map(|op| op.op_id)
.ok_or_else(|| {
VmError::JitNative(
"aot call bridge reported waiting without a pending op".to_string(),
)
})?;
Ok(ExecOutcome::Waiting(op_id))
}
STATUS_OUT_OF_FUEL => match self.interrupt_mode {
STATUS_OUT_OF_FUEL => match self.run_ctx.interrupt_mode {
super::super::InterruptMode::Fuel => Err(VmError::OutOfFuel {
needed: 1,
remaining: self.fuel_remaining,
remaining: self.run_ctx.fuel_remaining,
}),
super::super::InterruptMode::Epoch => Err(VmError::EpochDeadlineReached {
current: self.current_epoch(),
deadline: self.epoch_deadline,
deadline: self.run_ctx.epoch_deadline,
}),
super::super::InterruptMode::None => Err(VmError::JitNative(
"aot interruption checkpoint fired while interruption was disabled".to_string(),
Expand All @@ -99,18 +112,18 @@ impl Vm {
if let Some(err) = take_bridge_error() {
return Err(err);
}
if self.ip == self.program.code.len() {
if self.instance.ip == self.program.code.len() {
return Err(VmError::BytecodeBounds);
}
Err(VmError::JitNative(format!(
"aot entry reported failure without VmError (ip={} stack_len={} aot={})",
self.ip,
self.stack.len(),
self.instance.ip,
self.instance.stack.len(),
self.has_aot_program()
)))
}
STATUS_TRACE_EXIT => {
self.aot_interpreter_boundary_hit = true;
self.engine.aot_interpreter_boundary_hit = true;
Ok(ExecOutcome::Continue)
}
other => Err(VmError::JitNative(format!(
Expand Down
141 changes: 141 additions & 0 deletions src/vm/engine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//! Backend engine state.
//!
//! [`Engine`] owns the code-generation backends and their caches: the trace
//! JIT engine, native traces and their counters, the optional AOT program,
//! the regex cache, program-derived decode caches, and code-generation
//! telemetry. It holds no per-run interpreter state and no host bindings, so
//! it can be shared across runs (and, by construction, reused by any number of
//! instances that never share stacks or resources).
//!
//! Native ABI note: the JIT/AOT code generators read a handful of fields by
//! machine offset through `std::mem::offset_of!(Vm, engine.<field>)`. The
//! field set and the offsets are part of the native ABI; see
//! `crate::vm::native::layout`.

use std::collections::HashMap;
use std::sync::Arc;

use crate::builtins::runtime::regex::RegexCache;
use crate::bytecode::{DecodedInstructionData, Program};
use crate::vm::aot;
use crate::vm::jit;
use crate::vm::native;

/// Engine-owned backend configuration, caches, and code-generation telemetry.
///
/// Thread safety: `Engine` is not shared between threads (`TraceJitEngine` is
/// not `Sync`); one VM facade owns one engine. Clone semantics: `Engine` is
/// intentionally not `Clone` — duplicating it would duplicate native traces
/// and JIT bookkeeping that are keyed to one execution identity.
pub(crate) struct Engine {
pub(crate) jit: jit::TraceJitEngine,
pub(crate) native_traces: Vec<Option<jit::NativeTrace>>,
pub(crate) native_trace_exec_count: u64,
pub(crate) aot_program: Option<aot::CompiledProgram>,
pub(crate) aot_exec_count: u64,
pub(crate) aot_interpreter_boundary_hit: bool,
pub(crate) jit_native_region_entry_count: u64,
pub(crate) jit_native_region_edge_count: u64,
pub(crate) jit_native_direct_link_count: u64,
pub(crate) jit_native_direct_links_enabled: bool,
pub(crate) jit_native_direct_cross_frame_enabled: bool,
pub(crate) jit_native_active_direct_trace_id: usize,
pub(crate) jit_native_direct_escape_streak: u16,
pub(crate) jit_native_direct_region_fallback: bool,
pub(crate) jit_native_compile_time_ns: u64,
pub(crate) jit_native_region_compile_time_ns: u64,
pub(crate) jit_trace_exit_count: u64,
pub(crate) jit_native_loop_back_count: u64,
pub(crate) jit_native_link_handoff_count: u64,
pub(crate) jit_native_link_dispatch_depth: u32,
pub(crate) jit_helper_fallback_count: u64,
pub(crate) jit_native_bridge_stats_enabled: bool,
pub(crate) jit_native_bridge_counts: HashMap<&'static str, u64>,
pub(crate) program_cache_key: u64,
pub(crate) program_cache_key_ready: bool,
pub(crate) regex_cache: RegexCache,
pub(crate) decoded_instruction_data: Arc<DecodedInstructionData>,
pub(crate) operand_type_hints: Option<Arc<[u8]>>,
// Native ABI mirrors: the JIT/AOT code generators load these addresses by
// field offset from the `Vm` facade. They are derived from the program and
// from static helper entry points, and are documented as load-bearing for
// `crate::vm::native`.
pub(crate) program_constants_ptr: usize,
#[allow(dead_code)]
pub(crate) program_constants_len: usize,
#[allow(dead_code)]
pub(crate) native_helper_fn: usize,
#[allow(dead_code)]
pub(crate) native_interrupt_helper_fn: usize,
}

impl Engine {
/// Builds an engine for one program and JIT configuration.
pub(crate) fn new(jit_config: jit::JitConfig, program: &Program) -> Self {
Self {
jit: jit::TraceJitEngine::new(jit_config),
native_traces: Vec::new(),
native_trace_exec_count: 0,
aot_program: None,
aot_exec_count: 0,
aot_interpreter_boundary_hit: false,
jit_native_region_entry_count: 0,
jit_native_region_edge_count: 0,
jit_native_direct_link_count: 0,
jit_native_direct_links_enabled: true,
jit_native_direct_cross_frame_enabled: false,
jit_native_active_direct_trace_id: usize::MAX,
jit_native_direct_escape_streak: 0,
jit_native_direct_region_fallback: false,
jit_native_compile_time_ns: 0,
jit_native_region_compile_time_ns: 0,
jit_trace_exit_count: 0,
jit_native_loop_back_count: 0,
jit_native_link_handoff_count: 0,
jit_native_link_dispatch_depth: 0,
jit_helper_fallback_count: 0,
jit_native_bridge_stats_enabled: false,
jit_native_bridge_counts: HashMap::new(),
program_cache_key: 0,
program_cache_key_ready: false,
regex_cache: RegexCache::default(),
decoded_instruction_data: program.shared_decoded_instruction_data(),
operand_type_hints: program.shared_operand_type_hints(),
program_constants_ptr: program.constants.as_ptr() as usize,
program_constants_len: program.constants.len(),
native_helper_fn: native::helper_entry_address(),
native_interrupt_helper_fn: native::interrupt_helper_entry_address(),
}
}

/// Returns the program cache key, computing and caching it on first use.
/// The key identifies the program for backend cache lookups; it is stable
/// for the lifetime of the engine (the program is immutable).
pub(crate) fn ensure_program_cache_key(&mut self, program: &Program) -> u64 {
if !self.program_cache_key_ready {
self.program_cache_key = super::compute_program_cache_key(program);
self.program_cache_key_ready = true;
}
self.program_cache_key
}

/// Rewinds run-scoped backend state between runs while retaining compiled
/// artifacts: hot-entry bookkeeping and call-site profiles are cleared,
/// and the AOT boundary flag is recomputed from the compiled program.
pub(crate) fn reset_runtime_state(&mut self, program: &Program) {
self.aot_interpreter_boundary_hit = self
.aot_program
.as_ref()
.is_some_and(|compiled| compiled.interpreter_boundary_only);
self.jit.reset_runtime_backoff();
self.jit.clear_call_site_profiles();
let _ = program;
}

/// Invalidates code-generation caches that may reference run-scoped
/// behavior (used when drop-contract event accounting is toggled).
pub(crate) fn invalidate_codegen_caches(&mut self) {
self.native_traces.clear();
self.native_trace_exec_count = 0;
}
}
Loading
Loading