use std::sync::{Arc, Mutex};
use crate::error::ExecError;
use crate::gc::{GcError, ensure_space};
use crate::interpreter::InstructionOutcome;
use crate::interpreter::NativeServices;
use crate::loader::Literal;
use crate::loader::decode::compact::Operand;
use crate::module::{Module, ModuleRegistry, ResolvedImportTarget};
use crate::native::ProcessContext;
use crate::process::{CodePosition, ExitReason, Process};
use crate::term::Term;
use crate::term::boxed::{Tuple, write_cons, write_tuple};
use crate::timer::TimerWheel;
use super::trampoline;
pub struct ExtCallContext<'a> {
pub timers: Option<&'a Arc<Mutex<TimerWheel>>>,
pub services: Option<&'a NativeServices>,
pub registry: Option<&'a ModuleRegistry>,
}
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,
source: &Operand,
destination: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let value = read_term(process, source)?;
write_term(process, destination, value)?;
Ok(InstructionOutcome::Continue)
}
pub fn call(
process: &mut Process,
module: &Module,
arity: &Operand,
label: &Operand,
return_ip: usize,
save_return: bool,
) -> Result<InstructionOutcome, ExecError> {
let _arity = operand_u8(arity, "call 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)?;
}
let target = label_ip(module, operand_label(label)?)?;
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 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)
}
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,
head: &Operand,
tail: &Operand,
destination: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let head = read_term(process, head)?;
let tail = read_term(process, 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,
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, 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,
source: &Operand,
index: &Operand,
destination: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let tuple_term = read_term(process, 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)
}
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)?;
}
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)?;
}
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::Native(entry) => {
let mut args = Vec::with_capacity(usize::from(arity));
for register in 0..arity {
args.push(process.x_reg(register.into()));
}
let mut context = match ctx.timers {
Some(timers) => {
ProcessContext::with_timer_services(process.pid(), Arc::clone(timers))
}
None => {
let mut pctx = ProcessContext::new();
pctx.set_pid(Some(process.pid()));
pctx
}
};
if let Some(svc) = ctx.services {
context.set_spawn_facility(svc.spawn_facility.clone());
context.set_link_facility(svc.link_facility.clone());
context.set_supervision_facility(svc.supervision_facility.clone());
context.set_code_management_facility(svc.code_management_facility.clone());
if let Some(sink) = &svc.io_sink {
context.set_io_sink(Arc::clone(sink));
}
}
let snapshot = trampoline::build_mailbox_snapshot(process);
context.set_select_facility(
snapshot
.clone()
.map(|s| s as Arc<dyn crate::native::SelectFacility>),
);
let result = match (entry.function)(&args, &mut context) {
Ok(value) => value,
Err(reason) => {
let exception = crate::process::Exception {
class: Term::atom(crate::atom::Atom::ERROR),
reason,
stacktrace: Term::NIL,
};
return super::messaging::raise_exception(process, exception);
}
};
let shutdown_requested = context.take_shutdown_request();
if let Some(snapshot) = snapshot {
trampoline::apply_mailbox_removal(process, &snapshot);
}
if let Some(suspend) = context.take_suspend() {
return trampoline::handle_suspend(process, module, suspend);
}
if let Some(trampoline_req) = context.take_trampoline() {
return trampoline::handle_trampoline(
process,
module,
ctx.registry,
trampoline_req,
);
}
process.set_x_reg(0, result);
if shutdown_requested {
return Ok(InstructionOutcome::Exit(crate::process::ExitReason::Normal));
}
charge_reduction(process)?;
Ok(InstructionOutcome::Continue)
}
}
}
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, 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(literal) => literal_term(literal),
Operand::TypedRegister { register, .. } => read_term(process, 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")),
}
}
fn literal_term(literal: &Literal) -> Result<Term, ExecError> {
match literal {
Literal::Integer(value) => Term::try_small_int(*value).ok_or(ExecError::Badarg),
Literal::Float(value) => {
let heap = Box::leak(Box::new([0u64; 2]));
crate::term::boxed::write_float(heap, *value).ok_or(ExecError::Badarg)
}
Literal::BigInteger(limbs) => {
let limbs = limbs_to_u64(limbs)?;
let heap = Box::leak(vec![0u64; 3 + limbs.len()].into_boxed_slice());
crate::term::boxed::write_bigint(heap, false, &limbs).ok_or(ExecError::Badarg)
}
Literal::Atom(atom) => Ok(Term::atom(*atom)),
Literal::Binary(bytes) | Literal::String(bytes) => {
let data_words = crate::term::binary::packed_word_count(bytes.len());
let heap = Box::leak(vec![0u64; 2 + data_words].into_boxed_slice());
crate::term::binary::write_binary(heap, bytes).ok_or(ExecError::Badarg)
}
Literal::Nil => Ok(Term::NIL),
Literal::Tuple(elements) => {
let mut terms = Vec::with_capacity(elements.len());
for element in elements {
terms.push(literal_term(element)?);
}
let heap = Box::leak(vec![0u64; 1 + terms.len()].into_boxed_slice());
crate::term::boxed::write_tuple(heap, &terms).ok_or(ExecError::Badarg)
}
Literal::List(elements, tail) => {
let mut result = literal_term(tail)?;
for element in elements.iter().rev() {
let head = literal_term(element)?;
let heap = Box::leak(Box::new([0u64; 2]));
result =
crate::term::boxed::write_cons(heap, head, result).ok_or(ExecError::Badarg)?;
}
Ok(result)
}
Literal::Map(entries) => {
let mut pairs = Vec::with_capacity(entries.len());
for (key, value) in entries {
pairs.push((literal_term(key)?, literal_term(value)?));
}
pairs.sort_by(|(left, _), (right, _)| left.cmp(right));
let keys: Vec<_> = pairs.iter().map(|(key, _)| *key).collect();
let values: Vec<_> = pairs.iter().map(|(_, value)| *value).collect();
let heap = Box::leak(vec![0u64; 2 + keys.len() + values.len()].into_boxed_slice());
crate::term::boxed::write_map(heap, &keys, &values).ok_or(ExecError::Badarg)
}
}
}
fn limbs_to_u64(bytes: &[u8]) -> Result<Vec<u64>, ExecError> {
if !bytes.len().is_multiple_of(8) {
return Err(ExecError::UnsupportedLiteral);
}
let mut limbs = Vec::with_capacity(bytes.len() / 8);
for chunk in bytes.chunks_exact(8) {
let mut limb = [0u8; 8];
limb.copy_from_slice(chunk);
limbs.push(u64::from_le_bytes(limb));
}
Ok(limbs)
}
fn operand_atom(operand: &Operand) -> Result<crate::atom::Atom, ExecError> {
match operand {
Operand::Atom(Some(atom)) => Ok(*atom),
Operand::Literal(Literal::Atom(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) * 6,
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) }
}