pub mod gc;
pub mod heap;
pub mod registry;
pub mod stack;
use std::collections::HashSet;
use std::fmt;
use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::Arc;
use crate::atom::Atom;
use crate::mailbox::Mailbox;
use crate::module::Module;
use crate::native::NativeContinuation;
use crate::process::heap::Heap;
use crate::process::stack::Stack;
use crate::term::Term;
pub const DEFAULT_REDUCTION_BUDGET: u32 = 4000;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Monitor {
reference: u64,
watcher: u64,
target: u64,
}
impl Monitor {
#[must_use]
pub const fn new(reference: u64, watcher: u64, target: u64) -> Self {
Self {
reference,
watcher,
target,
}
}
#[must_use]
pub const fn reference(self) -> u64 {
self.reference
}
#[must_use]
pub const fn watcher(self) -> u64 {
self.watcher
}
#[must_use]
pub const fn target(self) -> u64 {
self.target
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct CodePosition {
pub module: Atom,
pub instruction_pointer: usize,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Register {
X(u16),
Y(u16),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ExceptionHandler {
pub catch_position: CodePosition,
pub destination: Register,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Exception {
pub class: Term,
pub reason: Term,
pub stacktrace: Term,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ReceiveTimeout {
pub timeout_position: CodePosition,
pub milliseconds: u64,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ExitReason {
Normal,
Kill,
Killed,
Error,
}
impl ExitReason {
#[must_use]
pub const fn as_atom(self) -> Atom {
match self {
Self::Normal => Atom::NORMAL,
Self::Kill => Atom::KILL,
Self::Killed => Atom::KILLED,
Self::Error => Atom::ERROR,
}
}
#[must_use]
pub const fn as_term(self) -> Term {
Term::atom(self.as_atom())
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ProcessStatus {
New,
Running,
Yielded,
Waiting,
Suspended,
Exited(ExitReason),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ProcessError {
InvalidStatusTransition {
from: ProcessStatus,
to: ProcessStatus,
},
}
impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidStatusTransition { from, to } => {
write!(
f,
"invalid process status transition from {from:?} to {to:?}"
)
}
}
}
}
impl std::error::Error for ProcessError {}
#[derive(Debug)]
pub struct Process {
pid: u64,
status: ProcessStatus,
heap: Heap,
stack: Stack,
mailbox: Mailbox,
handlers: Vec<ExceptionHandler>,
current_exception: Option<Exception>,
receive_timeout: Option<ReceiveTimeout>,
receive_timer_ref: Option<u64>,
x_regs: [Term; 1024],
native_continuation: Option<NativeContinuation>,
reduction_counter: u32,
code_position: Option<CodePosition>,
current_module: Option<Arc<Module>>,
current_mfa: Option<(Atom, Atom, u8)>,
links: HashSet<u64>,
monitors: Vec<Monitor>,
trap_exit: bool,
group_leader: Option<u64>,
not_send_sync: PhantomData<Rc<()>>,
}
impl Process {
#[must_use]
pub fn new(pid: u64, heap_size: usize) -> Self {
Self {
pid,
status: ProcessStatus::New,
heap: Heap::new(heap_size),
stack: Stack::new(),
mailbox: Mailbox::new(),
handlers: Vec::new(),
current_exception: None,
receive_timeout: None,
receive_timer_ref: None,
x_regs: [Term::NIL; 1024],
native_continuation: None,
reduction_counter: DEFAULT_REDUCTION_BUDGET,
code_position: None,
current_module: None,
current_mfa: None,
links: HashSet::new(),
monitors: Vec::new(),
trap_exit: false,
group_leader: None,
not_send_sync: PhantomData,
}
}
#[must_use]
pub const fn pid(&self) -> u64 {
self.pid
}
#[must_use]
pub const fn status(&self) -> ProcessStatus {
self.status
}
pub fn transition_to(&mut self, next: ProcessStatus) -> Result<(), ProcessError> {
if Self::can_transition(self.status, next) {
self.status = next;
Ok(())
} else {
Err(ProcessError::InvalidStatusTransition {
from: self.status,
to: next,
})
}
}
const fn can_transition(from: ProcessStatus, to: ProcessStatus) -> bool {
matches!(
(from, to),
(ProcessStatus::New, ProcessStatus::Running)
| (ProcessStatus::Running, ProcessStatus::Yielded)
| (ProcessStatus::Running, ProcessStatus::Waiting)
| (ProcessStatus::Running, ProcessStatus::Suspended)
| (ProcessStatus::Running, ProcessStatus::Exited(_))
| (ProcessStatus::Yielded, ProcessStatus::Running)
| (ProcessStatus::Yielded, ProcessStatus::Suspended)
| (ProcessStatus::Waiting, ProcessStatus::Running)
| (ProcessStatus::Waiting, ProcessStatus::Suspended)
| (ProcessStatus::Suspended, ProcessStatus::Yielded)
| (ProcessStatus::Suspended, ProcessStatus::Waiting)
)
}
#[must_use]
pub const fn heap(&self) -> &Heap {
&self.heap
}
pub fn heap_mut(&mut self) -> &mut Heap {
&mut self.heap
}
#[must_use]
pub const fn stack(&self) -> &Stack {
&self.stack
}
pub fn stack_mut(&mut self) -> &mut Stack {
&mut self.stack
}
#[must_use]
pub const fn mailbox(&self) -> &Mailbox {
&self.mailbox
}
pub fn mailbox_mut(&mut self) -> &mut Mailbox {
&mut self.mailbox
}
pub(crate) fn roots(&mut self) -> Vec<Term> {
self.roots_with_live_x(256)
}
pub(crate) fn roots_with_live_x(&mut self, live_x: usize) -> Vec<Term> {
self.mailbox.drain_arrival();
let live_x = live_x.min(self.x_regs.len());
let exception_roots = self
.current_exception
.into_iter()
.flat_map(|exception| [exception.reason, exception.stacktrace]);
self.x_regs
.iter()
.take(live_x)
.chain(self.stack.y_regs())
.chain(self.mailbox.scan_iter())
.copied()
.chain(exception_roots)
.collect()
}
pub(crate) fn replace_roots(&mut self, roots: &[Term]) {
self.replace_roots_with_live_x(256, roots);
}
pub(crate) fn replace_roots_with_live_x(&mut self, live_x: usize, roots: &[Term]) {
let mut index = 0;
let live_x = live_x.min(self.x_regs.len());
for root in self.x_regs.iter_mut().take(live_x) {
if let Some(value) = roots.get(index).copied() {
*root = value;
}
index += 1;
}
for root in self.stack.y_regs_mut() {
if let Some(value) = roots.get(index).copied() {
*root = value;
}
index += 1;
}
for root in self.mailbox.scan_iter_mut() {
if let Some(value) = roots.get(index).copied() {
*root = value;
}
index += 1;
}
if let Some(exception) = &mut self.current_exception {
if let Some(value) = roots.get(index).copied() {
exception.reason = value;
}
index += 1;
if let Some(value) = roots.get(index).copied() {
exception.stacktrace = value;
}
}
}
pub fn push_exception_handler(&mut self, handler: ExceptionHandler) {
self.handlers.push(handler);
}
pub fn pop_exception_handler(&mut self) -> Option<ExceptionHandler> {
self.handlers.pop()
}
#[must_use]
pub fn exception_handler_count(&self) -> usize {
self.handlers.len()
}
pub const fn set_current_exception(&mut self, exception: Option<Exception>) {
self.current_exception = exception;
}
#[must_use]
pub const fn current_exception(&self) -> Option<Exception> {
self.current_exception
}
pub const fn set_receive_timeout(&mut self, timeout: Option<ReceiveTimeout>) {
self.receive_timeout = timeout;
}
#[must_use]
pub const fn receive_timeout(&self) -> Option<ReceiveTimeout> {
self.receive_timeout
}
pub const fn set_receive_timer_ref(&mut self, timer_ref: Option<u64>) {
self.receive_timer_ref = timer_ref;
}
#[must_use]
pub const fn receive_timer_ref(&self) -> Option<u64> {
self.receive_timer_ref
}
#[must_use]
pub fn x_reg(&self, n: u16) -> Term {
self.x_regs[usize::from(n)]
}
pub fn set_x_reg(&mut self, n: u16, value: Term) {
self.x_regs[usize::from(n)] = value;
}
#[must_use]
pub const fn x_regs(&self) -> &[Term; 1024] {
&self.x_regs
}
pub fn x_regs_mut(&mut self) -> &mut [Term; 1024] {
&mut self.x_regs
}
pub fn set_native_continuation(&mut self, continuation: Option<NativeContinuation>) {
self.native_continuation = continuation;
}
pub fn take_native_continuation(&mut self) -> Option<NativeContinuation> {
self.native_continuation.take()
}
#[must_use]
pub fn has_native_continuation(&self) -> bool {
self.native_continuation.is_some()
}
#[must_use]
pub const fn reduction_counter(&self) -> u32 {
self.reduction_counter
}
pub fn decrement_reductions(&mut self, n: u32) {
self.reduction_counter = self.reduction_counter.saturating_sub(n);
}
#[must_use]
pub const fn reductions_exhausted(&self) -> bool {
self.reduction_counter == 0
}
pub const fn reset_reductions(&mut self, budget: u32) {
self.reduction_counter = budget;
}
#[must_use]
pub const fn code_position(&self) -> Option<CodePosition> {
self.code_position
}
pub const fn set_code_position(&mut self, code_position: Option<CodePosition>) {
self.code_position = code_position;
}
#[must_use]
pub fn current_module(&self) -> Option<&Arc<Module>> {
self.current_module.as_ref()
}
#[must_use]
pub fn references_module(&self, module: &Arc<Module>) -> bool {
self.current_module
.as_ref()
.is_some_and(|current| Arc::ptr_eq(current, module))
|| self
.stack
.pinned_modules()
.any(|pinned| Arc::ptr_eq(pinned, module))
}
pub fn set_current_module(&mut self, module: Arc<Module>) {
self.current_module = Some(module);
}
pub fn clear_current_module(&mut self) {
self.current_module = None;
}
#[must_use]
pub const fn current_mfa(&self) -> Option<(Atom, Atom, u8)> {
self.current_mfa
}
pub const fn set_current_mfa(&mut self, current_mfa: Option<(Atom, Atom, u8)>) {
self.current_mfa = current_mfa;
}
#[must_use]
pub const fn links(&self) -> &HashSet<u64> {
&self.links
}
pub fn add_link(&mut self, pid: u64) -> bool {
pid != self.pid && self.links.insert(pid)
}
pub fn remove_link(&mut self, pid: u64) -> bool {
self.links.remove(&pid)
}
pub fn take_links(&mut self) -> HashSet<u64> {
std::mem::take(&mut self.links)
}
#[must_use]
pub const fn monitors(&self) -> &Vec<Monitor> {
&self.monitors
}
pub fn add_monitor(&mut self, monitor: Monitor) {
self.monitors.push(monitor);
}
pub fn remove_monitor(&mut self, reference: u64) -> Option<Monitor> {
let index = self
.monitors
.iter()
.position(|monitor| monitor.reference() == reference)?;
Some(self.monitors.remove(index))
}
#[must_use]
pub const fn trap_exit(&self) -> bool {
self.trap_exit
}
pub const fn set_trap_exit(&mut self, trap_exit: bool) {
self.trap_exit = trap_exit;
}
#[must_use]
pub const fn group_leader(&self) -> Option<u64> {
self.group_leader
}
pub const fn set_group_leader(&mut self, group_leader: Option<u64>) {
self.group_leader = group_leader;
}
pub fn terminate(&mut self, reason: ExitReason) {
self.status = ProcessStatus::Exited(reason);
self.heap = Heap::new(1);
self.stack = Stack::new();
self.mailbox = Mailbox::new();
self.handlers.clear();
self.current_exception = None;
self.receive_timeout = None;
self.receive_timer_ref = None;
self.x_regs = [Term::NIL; 1024];
self.reduction_counter = 0;
self.code_position = None;
self.current_module = None;
self.current_mfa = None;
}
}
#[cfg(test)]
mod tests {
use super::{
CodePosition, DEFAULT_REDUCTION_BUDGET, ExitReason, Process, ProcessError, ProcessStatus,
};
use crate::atom::Atom;
use crate::gc::tests::module_pin;
use crate::term::Term;
#[test]
fn fresh_process_has_expected_state() {
let process = Process::new(7, 233);
assert_eq!(process.pid(), 7);
assert_eq!(process.status(), ProcessStatus::New);
assert_eq!(process.heap().capacity(), 233);
assert!(process.stack().is_empty());
assert!(process.mailbox().is_empty());
assert_eq!(process.reduction_counter(), DEFAULT_REDUCTION_BUDGET);
assert_eq!(process.code_position(), None);
assert!(process.current_module().is_none());
assert!(process.links().is_empty());
assert!(process.monitors().is_empty());
assert!(!process.trap_exit());
assert_eq!(process.group_leader(), None);
}
#[test]
fn terminate_clears_current_module_pin() {
let mut process = Process::new(0, 233);
process.set_code_position(Some(CodePosition {
module: Atom::OK,
instruction_pointer: 0,
}));
process.set_current_module(module_pin(Atom::OK));
process.terminate(ExitReason::Normal);
assert!(process.current_module().is_none());
assert_eq!(process.code_position(), None);
}
#[test]
fn all_x_registers_start_as_nil() {
let process = Process::new(0, 233);
for register in u16::MIN..=u8::MAX as u16 {
assert_eq!(process.x_reg(register), Term::NIL);
}
}
#[test]
fn x_registers_are_independently_addressable() {
let mut process = Process::new(0, 233);
process.set_x_reg(0, Term::small_int(10));
process.set_x_reg(255, Term::small_int(20));
assert_eq!(process.x_reg(0), Term::small_int(10));
assert_eq!(process.x_reg(255), Term::small_int(20));
assert_eq!(process.x_reg(1), Term::NIL);
}
#[test]
fn valid_status_transitions_succeed() {
let mut process = Process::new(0, 233);
assert_eq!(process.transition_to(ProcessStatus::Running), Ok(()));
assert_eq!(process.transition_to(ProcessStatus::Yielded), Ok(()));
assert_eq!(process.transition_to(ProcessStatus::Running), Ok(()));
assert_eq!(process.transition_to(ProcessStatus::Waiting), Ok(()));
assert_eq!(process.transition_to(ProcessStatus::Running), Ok(()));
assert_eq!(
process.transition_to(ProcessStatus::Exited(ExitReason::Normal)),
Ok(())
);
}
#[test]
fn new_to_exited_transition_fails() {
let mut process = Process::new(0, 233);
assert_eq!(
process.transition_to(ProcessStatus::Exited(ExitReason::Error)),
Err(ProcessError::InvalidStatusTransition {
from: ProcessStatus::New,
to: ProcessStatus::Exited(ExitReason::Error),
})
);
assert_eq!(process.status(), ProcessStatus::New);
}
#[test]
fn exited_state_is_terminal() {
let mut process = Process::new(0, 233);
process
.transition_to(ProcessStatus::Running)
.expect("new process can start running");
process
.transition_to(ProcessStatus::Exited(ExitReason::Kill))
.expect("running process can exit");
assert_eq!(
process.transition_to(ProcessStatus::Running),
Err(ProcessError::InvalidStatusTransition {
from: ProcessStatus::Exited(ExitReason::Kill),
to: ProcessStatus::Running,
})
);
}
#[test]
fn reductions_decrement_saturate_and_reset() {
let mut process = Process::new(0, 233);
assert_eq!(process.reduction_counter(), DEFAULT_REDUCTION_BUDGET);
process.decrement_reductions(1);
assert_eq!(process.reduction_counter(), DEFAULT_REDUCTION_BUDGET - 1);
process.decrement_reductions(DEFAULT_REDUCTION_BUDGET);
assert_eq!(process.reduction_counter(), 0);
assert!(process.reductions_exhausted());
process.reset_reductions(DEFAULT_REDUCTION_BUDGET);
assert_eq!(process.reduction_counter(), DEFAULT_REDUCTION_BUDGET);
assert!(!process.reductions_exhausted());
}
}