use std::sync::{Arc, Mutex};
use crate::atom::{Atom, AtomTable};
use crate::error::ExecError;
use crate::gc::{GcError, ensure_space};
use crate::interpreter::InstructionOutcome;
use crate::interpreter::NativeServices;
use crate::jit::JitCache;
use crate::jit::ir_common::JIT_DEOPT_SENTINEL;
use crate::jit::ir_exceptions::{
JIT_STATUS_DEOPT, JIT_STATUS_EXCEPTION, JIT_STATUS_YIELD, JitReturn,
};
use crate::jit::runtime::JIT_YIELD_SENTINEL;
use crate::loader::decode::compact::Operand;
use crate::module::{Module, ModuleRegistry, ResolvedImportTarget};
use crate::native::ExceptionClass;
use crate::process::{CodePosition, ExitReason, JitRuntimeContext, JitStatus, Process};
use crate::term::Term;
use crate::term::boxed::{Tuple, write_cons, write_tuple};
use crate::timer::TimerWheel;
#[derive(Copy, Clone)]
pub struct JitDispatchContext<'a> {
pub jit_cache: Option<&'a JitCache>,
pub registry: Option<&'a ModuleRegistry>,
}
pub struct ExtCallContext<'a> {
pub timers: Option<&'a Arc<Mutex<TimerWheel>>>,
pub services: Option<&'a NativeServices>,
pub registry: Option<&'a ModuleRegistry>,
pub atom_table: Option<&'a AtomTable>,
pub jit_cache: Option<&'a JitCache>,
}
pub(super) fn exception_class_atom(class: ExceptionClass) -> Atom {
match class {
ExceptionClass::Error => Atom::ERROR,
ExceptionClass::Throw => Atom::THROW,
ExceptionClass::Exit => Atom::EXIT_CLASS,
}
}
pub fn label(_label: u32) -> Result<InstructionOutcome, ExecError> {
Ok(InstructionOutcome::Continue)
}
pub fn func_info(
process: &mut Process,
module: &Operand,
function: &Operand,
arity: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let metadata = (
operand_atom(module)?,
operand_atom(function)?,
operand_u8(arity, "func_info arity")?,
);
process.set_current_mfa(Some(metadata));
Ok(InstructionOutcome::Continue)
}
pub fn move_(
process: &mut Process,
module: &Module,
source: &Operand,
destination: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let value = read_term(process, module, source)?;
write_term(process, destination, value)?;
Ok(InstructionOutcome::Continue)
}
pub fn swap(
process: &mut Process,
module: &Module,
left: &Operand,
right: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let left_value = read_term(process, module, left)?;
let right_value = read_term(process, module, right)?;
write_term(process, left, right_value)?;
write_term(process, right, left_value)?;
Ok(InstructionOutcome::Continue)
}
pub fn call(
process: &mut Process,
module: &Module,
arity: &Operand,
label: &Operand,
return_ip: usize,
save_return: bool,
jit_ctx: JitDispatchContext<'_>,
) -> Result<InstructionOutcome, ExecError> {
let arity = operand_u8(arity, "call arity")?;
let target = label_ip(module, operand_label(label)?)?;
if save_return {
let caller_module = current_module_pin(process, module);
process
.stack_mut()
.push_frame(module.name, return_ip, caller_module, 0)
.map_err(ExecError::from)?;
}
if let Some(outcome) = dispatch_local_jit(
process,
module,
target,
arity,
save_return,
jit_ctx.jit_cache,
jit_ctx.registry,
)? {
return Ok(outcome);
}
jump_with_reduction(process, module.name, target)
}
pub fn call_ext(
process: &mut Process,
module: &Module,
arity: &Operand,
import: &Operand,
return_ip: usize,
save_return: bool,
ctx: &ExtCallContext<'_>,
) -> Result<InstructionOutcome, ExecError> {
call_external_target(process, module, arity, import, save_return, return_ip, ctx)
}
pub fn call_last(
process: &mut Process,
module: &Module,
arity: &Operand,
label: &Operand,
deallocate: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let _arity = operand_u8(arity, "call_last arity")?;
deallocate_frame(process, deallocate)?;
let target = label_ip(module, operand_label(label)?)?;
jump_with_reduction(process, module.name, target)
}
pub fn call_ext_last(
process: &mut Process,
module: &Module,
arity: &Operand,
import: &Operand,
deallocate: &Operand,
ctx: &ExtCallContext<'_>,
) -> Result<InstructionOutcome, ExecError> {
deallocate_frame(process, deallocate)?;
call_external_target(process, module, arity, import, false, 0, ctx)
}
pub fn return_(process: &mut Process) -> Result<InstructionOutcome, ExecError> {
if process.stack().is_empty() {
return Ok(InstructionOutcome::Exit(ExitReason::Normal));
}
let return_point = process.stack_mut().pop_frame().map_err(ExecError::from)?;
process.set_current_module(Arc::clone(&return_point.module_version));
Ok(InstructionOutcome::Jump(CodePosition {
module: return_point.module,
instruction_pointer: return_point.ip,
}))
}
pub fn allocate(
process: &mut Process,
module: &Module,
stack_need: &Operand,
) -> Result<InstructionOutcome, ExecError> {
push_y_frame(process, module, stack_need)
}
pub fn allocate_heap(
process: &mut Process,
module: &Module,
stack_need: &Operand,
heap_need: &Operand,
live: &Operand,
) -> Result<InstructionOutcome, ExecError> {
test_heap(process, heap_need, live)?;
push_y_frame(process, module, stack_need)
}
pub fn allocate_zero(
process: &mut Process,
module: &Module,
stack_need: &Operand,
) -> Result<InstructionOutcome, ExecError> {
push_y_frame(process, module, stack_need)
}
pub fn deallocate(process: &mut Process, words: &Operand) -> Result<InstructionOutcome, ExecError> {
deallocate_frame(process, words)?;
Ok(InstructionOutcome::Continue)
}
pub fn trim(
process: &mut Process,
words: &Operand,
remaining: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let words = operand_u16(words, "trim words")?;
let remaining = operand_u16(remaining, "trim remaining")?;
let expected_slots = words.checked_add(remaining).ok_or(ExecError::Badarg)?;
let current_slots = process
.stack()
.current_frame()
.map_err(ExecError::from)?
.y_slots();
if current_slots != expected_slots {
return Err(ExecError::Badarg);
}
process
.stack_mut()
.trim_y_regs(remaining)
.map_err(ExecError::from)?;
Ok(InstructionOutcome::Continue)
}
pub fn test_heap(
process: &mut Process,
heap_need: &Operand,
live: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let needed = operand_usize(heap_need, "heap words")?;
let live = operand_usize(live, "live x registers")?;
ensure_space(process, needed, live).map_err(gc_error_to_exec)?;
Ok(InstructionOutcome::Continue)
}
pub(super) fn gc_error_to_exec(error: GcError) -> ExecError {
match error {
GcError::HeapFull(error) => ExecError::from(error),
GcError::InvalidObjectHeader(_) => ExecError::Badarg,
}
}
pub fn put_list(
process: &mut Process,
module: &Module,
head: &Operand,
tail: &Operand,
destination: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let head = read_term(process, module, head)?;
let tail = read_term(process, module, tail)?;
let ptr = process.heap_mut().alloc(2).map_err(ExecError::from)?;
let heap = heap_slice(ptr, 2);
let term = write_cons(heap, head, tail).ok_or(ExecError::Badarg)?;
write_term(process, destination, term)?;
Ok(InstructionOutcome::Continue)
}
pub fn put_tuple2(
process: &mut Process,
module: &Module,
destination: &Operand,
elements: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let Operand::List(element_operands) = elements else {
return Err(ExecError::InvalidOperand("put_tuple2 elements"));
};
let mut values = Vec::with_capacity(element_operands.len());
for operand in element_operands {
values.push(read_term(process, module, operand)?);
}
let words = values
.len()
.checked_add(1)
.ok_or(ExecError::InvalidOperand("tuple size"))?;
let ptr = process.heap_mut().alloc(words).map_err(ExecError::from)?;
let heap = heap_slice(ptr, words);
let term = write_tuple(heap, &values).ok_or(ExecError::Badarg)?;
write_term(process, destination, term)?;
Ok(InstructionOutcome::Continue)
}
pub fn get_tuple_element(
process: &mut Process,
module: &Module,
source: &Operand,
index: &Operand,
destination: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let tuple_term = read_term(process, module, source)?;
let tuple = Tuple::new(tuple_term).ok_or(ExecError::Badarg)?;
let index = operand_usize(index, "tuple index")?;
let value = tuple.get(index).ok_or(ExecError::Badarg)?;
write_term(process, destination, value)?;
Ok(InstructionOutcome::Continue)
}
pub fn update_record(
process: &mut Process,
module: &Module,
operands: &[Operand],
) -> Result<InstructionOutcome, ExecError> {
let parsed = parse_update_record_operands(operands)?;
let words = parsed
.arity
.checked_add(1)
.ok_or(ExecError::InvalidOperand("update_record tuple size"))?;
ensure_space(process, words, 256).map_err(gc_error_to_exec)?;
let source_term = read_term(process, module, parsed.source)?;
let tuple = Tuple::new(source_term).ok_or(ExecError::Badarg)?;
if tuple.arity() != parsed.arity {
return Err(ExecError::Badarg);
}
let mut values = Vec::with_capacity(parsed.arity);
for index in 0..parsed.arity {
values.push(tuple.get(index).ok_or(ExecError::Badarg)?);
}
for update in parsed.updates {
let slot = update.index.checked_sub(1).ok_or(ExecError::Badarg)?;
let value = values.get_mut(slot).ok_or(ExecError::Badarg)?;
*value = read_term(process, module, update.value)?;
}
let ptr = process.heap_mut().alloc(words).map_err(ExecError::from)?;
let heap = heap_slice(ptr, words);
let term = write_tuple(heap, &values).ok_or(ExecError::Badarg)?;
write_term(process, parsed.destination, term)?;
Ok(InstructionOutcome::Continue)
}
struct UpdateRecordOperands<'a> {
arity: usize,
source: &'a Operand,
destination: &'a Operand,
updates: Vec<UpdateRecordPair<'a>>,
}
struct UpdateRecordPair<'a> {
index: usize,
value: &'a Operand,
}
fn parse_update_record_operands(
operands: &[Operand],
) -> Result<UpdateRecordOperands<'_>, ExecError> {
if operands.len() < 4 {
return Err(ExecError::InvalidOperand("update_record operands"));
}
let _hint = operand_atom(&operands[0])?;
let arity = operand_usize(&operands[1], "update_record arity")?;
let pair_operands = update_record_pair_operands(operands)?;
let update_count = pair_operands.len() / 2;
let mut updates = Vec::with_capacity(update_count);
for pair in pair_operands.chunks_exact(2) {
updates.push(UpdateRecordPair {
index: operand_usize(&pair[0], "update_record index")?,
value: &pair[1],
});
}
Ok(UpdateRecordOperands {
arity,
source: &operands[2],
destination: &operands[3],
updates,
})
}
fn update_record_pair_operands(operands: &[Operand]) -> Result<&[Operand], ExecError> {
if operands.len() == 5
&& let Operand::List(pairs) = &operands[4]
{
if pairs.len() % 2 == 0 {
return Ok(pairs);
}
return Err(ExecError::InvalidOperand("update_record pairs"));
}
if !(operands.len() - 4).is_multiple_of(2) {
return Err(ExecError::InvalidOperand("update_record pairs"));
}
Ok(&operands[4..])
}
fn call_external_target(
process: &mut Process,
module: &Module,
arity: &Operand,
import: &Operand,
save_return: bool,
return_ip: usize,
ctx: &ExtCallContext<'_>,
) -> Result<InstructionOutcome, ExecError> {
let arity = operand_u8(arity, "external call arity")?;
let import_index = operand_usize(import, "import index")?;
let resolved = module
.resolved_imports
.get(import_index)
.ok_or(ExecError::InvalidImport {
index: import_index,
})?;
if resolved.arity != arity {
return Err(ExecError::InvalidOperand("external call arity mismatch"));
}
match resolved.target {
ResolvedImportTarget::Code {
module: _,
label: _,
} => {
let target_module = resolved.module;
let target_mod =
ctx.registry
.and_then(|r| r.lookup(target_module))
.ok_or(ExecError::Undef {
module: target_module,
function: resolved.function,
arity,
})?;
let instruction_pointer = target_mod.export_ip(resolved.function, resolved.arity)?;
if save_return {
let caller_module = current_module_pin(process, module);
process
.stack_mut()
.push_frame(module.name, return_ip, caller_module, 0)
.map_err(ExecError::from)?;
}
if let Some(outcome) = dispatch_external_jit(
process,
target_mod.as_ref(),
instruction_pointer,
resolved.function,
arity,
save_return,
ctx,
)? {
return Ok(outcome);
}
let target = CodePosition {
module: target_module,
instruction_pointer,
};
process.set_current_module(Arc::clone(&target_mod));
jump_position_with_reduction(process, target)
}
ResolvedImportTarget::Deferred {
module: target_module,
function,
arity: target_arity,
} => {
let target_mod =
ctx.registry
.and_then(|r| r.lookup(target_module))
.ok_or(ExecError::Undef {
module: target_module,
function,
arity: target_arity,
})?;
let instruction_pointer = target_mod.export_ip(function, target_arity)?;
if save_return {
let caller_module = current_module_pin(process, module);
process
.stack_mut()
.push_frame(module.name, return_ip, caller_module, 0)
.map_err(ExecError::from)?;
}
if let Some(outcome) = dispatch_external_jit(
process,
target_mod.as_ref(),
instruction_pointer,
function,
target_arity,
save_return,
ctx,
)? {
return Ok(outcome);
}
let target = CodePosition {
module: target_module,
instruction_pointer,
};
process.set_current_module(Arc::clone(&target_mod));
jump_position_with_reduction(process, target)
}
ResolvedImportTarget::Unresolved {
module,
function,
arity,
} => Err(ExecError::Undef {
module,
function,
arity,
}),
ResolvedImportTarget::Denied { .. } => Err(ExecError::Undef {
module: resolved.module,
function: resolved.function,
arity: resolved.arity,
}),
ResolvedImportTarget::Native(entry) => super::native_call::call_native_entry(
process,
module,
entry,
(resolved.module, resolved.function, resolved.arity),
save_return,
ctx,
),
}
}
type RawJitFn = extern "C" fn(*mut u64, *mut Process) -> JitReturn;
fn dispatch_local_jit(
process: &mut Process,
module: &Module,
target_ip: usize,
arity: u8,
_save_return: bool,
jit_cache: Option<&JitCache>,
registry: Option<&ModuleRegistry>,
) -> Result<Option<InstructionOutcome>, ExecError> {
let Some(cache) = jit_cache else {
return Ok(None);
};
let Some((function, function_arity)) = module.function_at_ip(target_ip) else {
return Ok(None);
};
if function_arity != arity {
return Ok(None);
}
let Some(native) = cache.lookup(module.name, function, arity, module.generation()) else {
return Ok(None);
};
process.set_code_position(Some(CodePosition {
module: module.name,
instruction_pointer: target_ip,
}));
invoke_jit(process, module, native, registry, jit_cache)
}
fn dispatch_external_jit(
process: &mut Process,
target_module: &Module,
target_ip: usize,
function: Atom,
arity: u8,
_save_return: bool,
ctx: &ExtCallContext<'_>,
) -> Result<Option<InstructionOutcome>, ExecError> {
let Some(cache) = ctx.jit_cache else {
return Ok(None);
};
let Some(native) = cache.lookup(
target_module.name,
function,
arity,
target_module.generation(),
) else {
return Ok(None);
};
process.set_code_position(Some(CodePosition {
module: target_module.name,
instruction_pointer: target_ip,
}));
invoke_jit(process, target_module, native, ctx.registry, ctx.jit_cache)
}
fn invoke_jit(
process: &mut Process,
module: &Module,
native: crate::jit::NativeCode,
registry: Option<&ModuleRegistry>,
jit_cache: Option<&JitCache>,
) -> Result<Option<InstructionOutcome>, ExecError> {
let Some(registry) = registry else {
return Ok(None);
};
let previous_context = process.jit_runtime_context();
process.set_jit_runtime_context(Some(JitRuntimeContext::new(
module as *const Module,
registry as *const ModuleRegistry,
jit_cache.map_or(std::ptr::null(), |cache| cache as *const JitCache),
)));
process.set_jit_status(None);
let outcome = call_native(process, native);
process.set_jit_runtime_context(previous_context);
outcome
}
fn call_native(
process: &mut Process,
native: crate::jit::NativeCode,
) -> Result<Option<InstructionOutcome>, ExecError> {
let register_file = process.x_regs_mut().as_mut_ptr().cast::<u64>();
let raw_fn: RawJitFn = unsafe { std::mem::transmute(native.call_ptr()) };
let returned = raw_fn(register_file, process);
match process.take_jit_status() {
Some(JitStatus::Yield) => return Ok(Some(InstructionOutcome::Yield)),
None => {}
}
match returned.status {
JIT_STATUS_EXCEPTION => {
return Ok(Some(InstructionOutcome::Exit(ExitReason::Error)));
}
JIT_STATUS_DEOPT => return Ok(None),
JIT_STATUS_YIELD => return Ok(Some(InstructionOutcome::Yield)),
_ => {}
}
let raw = returned.value;
if raw == JIT_YIELD_SENTINEL as u64 {
return Ok(Some(InstructionOutcome::Yield));
}
if raw == JIT_DEOPT_SENTINEL as u64 {
return Ok(None);
}
process.set_x_reg(0, Term::from_raw(raw));
return_(process).map(Some)
}
fn push_y_frame(
process: &mut Process,
module: &Module,
stack_need: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let slots = operand_u16(stack_need, "stack slots")?;
let return_ip = process
.code_position()
.map_or(0, |position| position.instruction_pointer);
let caller_module = current_module_pin(process, module);
process
.stack_mut()
.push_frame(module.name, return_ip, caller_module, slots)
.map_err(ExecError::from)?;
Ok(InstructionOutcome::Continue)
}
pub(crate) fn deallocate_frame(process: &mut Process, words: &Operand) -> Result<(), ExecError> {
let _words = operand_u16(words, "deallocate words")?;
let _ = process.stack_mut().pop_frame().map_err(ExecError::from)?;
Ok(())
}
pub(crate) fn current_module_pin(process: &Process, module: &Module) -> Arc<Module> {
process
.current_module()
.filter(|current| current.name == module.name)
.map_or_else(|| Arc::new(module.clone()), Arc::clone)
}
fn jump_with_reduction(
process: &mut Process,
module: crate::atom::Atom,
instruction_pointer: usize,
) -> Result<InstructionOutcome, ExecError> {
jump_position_with_reduction(
process,
CodePosition {
module,
instruction_pointer,
},
)
}
pub(crate) fn jump_position_with_reduction(
process: &mut Process,
target: CodePosition,
) -> Result<InstructionOutcome, ExecError> {
charge_reduction(process)?;
Ok(if process.reductions_exhausted() {
process.set_code_position(Some(target));
InstructionOutcome::Yield
} else {
InstructionOutcome::Jump(target)
})
}
pub(crate) fn charge_reduction(process: &mut Process) -> Result<(), ExecError> {
process.decrement_reductions(1);
Ok(())
}
pub(crate) fn label_ip(module: &Module, label: u32) -> Result<usize, ExecError> {
module.label_ip(label)
}
pub(crate) fn read_term(
process: &Process,
module: &Module,
operand: &Operand,
) -> Result<Term, ExecError> {
match operand {
Operand::Integer(value) => Term::try_small_int(*value).ok_or(ExecError::Badarg),
Operand::Unsigned(value) => {
let value = i64::try_from(*value).map_err(|_| ExecError::Badarg)?;
Term::try_small_int(value).ok_or(ExecError::Badarg)
}
Operand::Atom(Some(atom)) => Ok(Term::atom(*atom)),
Operand::Atom(None) => Ok(Term::NIL),
Operand::X(index) => Ok(process.x_reg(u16_from_u32(*index, "X register")?)),
Operand::Y(index) => process
.stack()
.y_reg(u16_from_u32(*index, "Y register")?)
.map_err(ExecError::from),
Operand::Literal(index) => literal_term(module, *index),
Operand::TypedRegister { register, .. } => read_term(process, module, register),
_ => Err(ExecError::InvalidOperand("term source")),
}
}
pub(crate) fn write_term(
process: &mut Process,
destination: &Operand,
value: Term,
) -> Result<(), ExecError> {
match destination {
Operand::X(index) => {
process.set_x_reg(u16_from_u32(*index, "X register")?, value);
Ok(())
}
Operand::Y(index) => process
.stack_mut()
.set_y_reg(u16_from_u32(*index, "Y register")?, value)
.map_err(ExecError::from),
Operand::TypedRegister { register, .. } => write_term(process, register, value),
_ => Err(ExecError::InvalidOperand("term destination")),
}
}
pub(crate) fn literal_term(module: &Module, literal_index: usize) -> Result<Term, ExecError> {
module
.constant_pool
.get(literal_index)
.ok_or(ExecError::InvalidOperand("literal index"))
}
pub(crate) fn operand_atom(operand: &Operand) -> Result<crate::atom::Atom, ExecError> {
match operand {
Operand::Atom(Some(atom)) => Ok(*atom),
_ => Err(ExecError::InvalidOperand("atom")),
}
}
pub(crate) fn operand_label(operand: &Operand) -> Result<u32, ExecError> {
match operand {
Operand::Label(label) => Ok(*label),
Operand::Unsigned(value) => {
u32::try_from(*value).map_err(|_| ExecError::InvalidOperand("label"))
}
Operand::Integer(value) => {
u32::try_from(*value).map_err(|_| ExecError::InvalidOperand("label"))
}
_ => Err(ExecError::InvalidOperand("label")),
}
}
pub(crate) fn operand_usize(operand: &Operand, context: &'static str) -> Result<usize, ExecError> {
match operand {
Operand::Unsigned(value) => {
usize::try_from(*value).map_err(|_| ExecError::InvalidOperand(context))
}
Operand::Integer(value) => {
usize::try_from(*value).map_err(|_| ExecError::InvalidOperand(context))
}
Operand::Allocation(entries) => {
use crate::loader::decode::compact::Allocation;
let mut total: usize = 0;
for entry in entries {
let words = match entry {
Allocation::Words(n) => *n as usize,
Allocation::Floats(n) => (*n as usize) * 2,
Allocation::Funs(n) => (*n as usize) * super::closures::CLOSURE_BASE_WORDS,
Allocation::Unknown { .. } => 0,
};
total = total.saturating_add(words);
}
Ok(total)
}
_ => Err(ExecError::InvalidOperand(context)),
}
}
fn operand_u8(operand: &Operand, context: &'static str) -> Result<u8, ExecError> {
u8::try_from(operand_usize(operand, context)?).map_err(|_| ExecError::InvalidOperand(context))
}
fn operand_u16(operand: &Operand, context: &'static str) -> Result<u16, ExecError> {
u16::try_from(operand_usize(operand, context)?).map_err(|_| ExecError::InvalidOperand(context))
}
fn u16_from_u32(value: u32, context: &'static str) -> Result<u16, ExecError> {
u16::try_from(value).map_err(|_| ExecError::InvalidOperand(context))
}
pub(crate) fn heap_slice<'a>(ptr: *mut u64, words: usize) -> &'a mut [u64] {
unsafe { std::slice::from_raw_parts_mut(ptr, words) }
}