use std::cmp::Ordering;
use std::time::Duration;
use crate::atom::{Atom, AtomTable};
use crate::native::{BifRegistryImpl, NativeFn, NativeRegistrationError, ProcessContext};
use crate::term::Term;
use crate::term::boxed::write_tuple;
use crate::term::compare;
use crate::timer::TimerRef;
type Gate1Bif = (&'static str, u8, NativeFn);
const GATE1_BIFS: &[Gate1Bif] = &[
("+", 2, add),
("-", 2, subtract),
("*", 2, multiply),
("div", 2, div),
("rem", 2, rem),
("<", 2, less_than),
(">=", 2, greater_equal),
("=:=", 2, exact_equal),
("=/=", 2, exact_not_equal),
("error", 1, error),
("display", 1, display),
("get_module_info", 1, get_module_info_1),
("get_module_info", 2, get_module_info_2),
("send_after", 3, send_after),
("start_timer", 3, start_timer),
("cancel_timer", 1, cancel_timer),
];
pub fn register_gate1_bifs(
registry: &BifRegistryImpl,
atom_table: &AtomTable,
) -> Result<(), NativeRegistrationError> {
let erlang = atom_table.intern("erlang");
for &(function_name, arity, native_function) in GATE1_BIFS {
let function = atom_table.intern(function_name);
registry.register(erlang, function, arity, native_function)?;
}
crate::native::code_management_bifs::register_code_management_bifs(registry, atom_table)?;
Ok(())
}
pub fn add(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
arithmetic(args, i64::checked_add)
}
pub fn subtract(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
arithmetic(args, i64::checked_sub)
}
pub fn multiply(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
arithmetic(args, i64::checked_mul)
}
pub fn div(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
arithmetic(args, i64::checked_div)
}
pub fn rem(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
arithmetic(args, i64::checked_rem)
}
pub fn less_than(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
let (left, right) = two_terms(args)?;
Ok(bool_term(compare::cmp(left, right) == Ordering::Less))
}
pub fn greater_equal(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
let (left, right) = two_terms(args)?;
Ok(bool_term(compare::cmp(left, right) != Ordering::Less))
}
pub fn exact_equal(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
let [left, right] = args else {
return Err(badarg());
};
Ok(bool_term(left == right))
}
pub fn exact_not_equal(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
let [left, right] = args else {
return Err(badarg());
};
Ok(bool_term(left != right))
}
pub fn error(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
let [reason] = args else {
return Err(badarg());
};
Err(*reason)
}
pub fn display(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
let [term] = args else {
return Err(badarg());
};
println!("{term:?}");
Ok(bool_term(true))
}
pub fn get_module_info_1(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [module] = args else {
return Err(badarg());
};
let _ = (module, context);
Ok(Term::NIL)
}
pub fn get_module_info_2(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [module, key] = args else {
return Err(badarg());
};
let _ = (module, key, context);
Ok(Term::NIL)
}
pub fn send_after(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [delay, pid, message] = args else {
return Err(badarg());
};
let delay = duration_from_term(*delay)?;
let target_pid = pid.as_pid().ok_or_else(badarg)?;
let reference = context
.schedule_timer(delay, target_pid, *message)
.ok_or_else(badarg)?;
timer_ref_term(reference)
}
pub fn start_timer(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [delay, pid, message] = args else {
return Err(badarg());
};
let delay = duration_from_term(*delay)?;
let target_pid = pid.as_pid().ok_or_else(badarg)?;
let payload = *message;
let reference = context
.schedule_timer_with_reference(delay, target_pid, |reference| {
timeout_tuple_term(reference, payload).unwrap_or(payload)
})
.ok_or_else(badarg)?;
timer_ref_term(reference)
}
pub fn cancel_timer(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [reference] = args else {
return Err(badarg());
};
let reference = reference
.as_small_int()
.and_then(|id| u64::try_from(id).ok())
.map(TimerRef::from_id)
.ok_or_else(badarg)?;
match context.cancel_timer(reference) {
Some(remaining) => i64::try_from(remaining.as_millis())
.ok()
.and_then(Term::try_small_int)
.ok_or_else(badarg),
None => Ok(Term::atom(Atom::FALSE)),
}
}
fn arithmetic(args: &[Term], operation: fn(i64, i64) -> Option<i64>) -> Result<Term, Term> {
let (left, right) = two_small_ints(args)?;
let result = operation(left, right).ok_or_else(badarith)?;
Term::try_small_int(result).ok_or_else(badarith)
}
fn two_terms(args: &[Term]) -> Result<(Term, Term), Term> {
let [left, right] = args else {
return Err(badarg());
};
Ok((*left, *right))
}
fn two_small_ints(args: &[Term]) -> Result<(i64, i64), Term> {
let [left, right] = args else {
return Err(badarith());
};
match (left.as_small_int(), right.as_small_int()) {
(Some(left), Some(right)) => Ok((left, right)),
_ => Err(badarith()),
}
}
fn duration_from_term(term: Term) -> Result<Duration, Term> {
let milliseconds = term
.as_small_int()
.and_then(|value| u64::try_from(value).ok())
.ok_or_else(badarg)?;
Ok(Duration::from_millis(milliseconds))
}
fn timer_ref_term(reference: TimerRef) -> Result<Term, Term> {
i64::try_from(reference.id())
.ok()
.and_then(Term::try_small_int)
.ok_or_else(badarg)
}
fn timeout_tuple_term(reference: TimerRef, message: Term) -> Result<Term, Term> {
let words = Box::leak(Box::new([0_u64; 4]));
write_tuple(
words,
&[
Term::atom(Atom::TIMEOUT),
timer_ref_term(reference)?,
message,
],
)
.ok_or_else(badarg)
}
fn bool_term(value: bool) -> Term {
Term::atom(if value { Atom::TRUE } else { Atom::FALSE })
}
fn badarith() -> Term {
Term::atom(Atom::BADARITH)
}
fn badarg() -> Term {
Term::atom(Atom::BADARG)
}
#[cfg(test)]
mod tests {
use super::{
add, cancel_timer, compare, display, div, error, exact_equal, exact_not_equal,
greater_equal, less_than, multiply, register_gate1_bifs, rem, send_after, start_timer,
subtract,
};
use crate::atom::{Atom, AtomTable};
use crate::native::{BifRegistryImpl, ProcessContext};
use crate::term::Term;
use crate::term::boxed::{Tuple, write_cons, write_map, write_tuple};
use crate::timer::TimerWheel;
use std::cmp::Ordering;
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn context() -> ProcessContext {
ProcessContext::new()
}
fn small_int(value: i64) -> Term {
Term::small_int(value)
}
fn badarith() -> Term {
Term::atom(Atom::BADARITH)
}
fn badarg() -> Term {
Term::atom(Atom::BADARG)
}
#[test]
fn arithmetic_bifs_return_small_integer_results() {
let mut context = context();
assert_eq!(
add(&[small_int(3), small_int(4)], &mut context),
Ok(small_int(7))
);
assert_eq!(
subtract(&[small_int(10), small_int(3)], &mut context),
Ok(small_int(7))
);
assert_eq!(
multiply(&[small_int(3), small_int(4)], &mut context),
Ok(small_int(12))
);
assert_eq!(
div(&[small_int(7), small_int(2)], &mut context),
Ok(small_int(3))
);
assert_eq!(
rem(&[small_int(7), small_int(2)], &mut context),
Ok(small_int(1))
);
}
#[test]
fn arithmetic_bifs_return_badarith_for_invalid_inputs() {
let mut context = context();
assert_eq!(
div(&[small_int(7), small_int(0)], &mut context),
Err(badarith())
);
assert_eq!(
add(&[Term::atom(Atom::OK), small_int(1)], &mut context),
Err(badarith())
);
assert_eq!(add(&[small_int(1)], &mut context), Err(badarith()));
assert_eq!(
add(
&[small_int(Term::SMALL_INT_MAX), small_int(1)],
&mut context
),
Err(badarith())
);
assert_eq!(
subtract(
&[small_int(Term::SMALL_INT_MIN), small_int(1)],
&mut context
),
Err(badarith())
);
assert_eq!(
multiply(
&[small_int(Term::SMALL_INT_MAX), small_int(2)],
&mut context
),
Err(badarith())
);
assert_eq!(
rem(&[small_int(7), small_int(0)], &mut context),
Err(badarith())
);
}
#[test]
fn comparison_bifs_return_true_or_false_atoms() {
let mut context = context();
assert_eq!(
less_than(&[small_int(1), small_int(2)], &mut context),
Ok(Term::atom(Atom::TRUE))
);
assert_eq!(
less_than(&[small_int(2), small_int(1)], &mut context),
Ok(Term::atom(Atom::FALSE))
);
assert_eq!(
greater_equal(&[small_int(2), small_int(1)], &mut context),
Ok(Term::atom(Atom::TRUE))
);
assert_eq!(
greater_equal(&[small_int(1), small_int(2)], &mut context),
Ok(Term::atom(Atom::FALSE))
);
assert_eq!(
exact_equal(&[small_int(1), small_int(1)], &mut context),
Ok(Term::atom(Atom::TRUE))
);
assert_eq!(
exact_equal(&[small_int(1), small_int(2)], &mut context),
Ok(Term::atom(Atom::FALSE))
);
assert_eq!(
exact_not_equal(&[small_int(1), small_int(2)], &mut context),
Ok(Term::atom(Atom::TRUE))
);
assert_eq!(
exact_not_equal(&[small_int(1), small_int(1)], &mut context),
Ok(Term::atom(Atom::FALSE))
);
}
#[test]
fn comparison_bifs_return_badarg_only_for_wrong_arity() {
let mut context = context();
assert_eq!(less_than(&[small_int(1)], &mut context), Err(badarg()));
assert_eq!(greater_equal(&[], &mut context), Err(badarg()));
assert_eq!(exact_equal(&[small_int(1)], &mut context), Err(badarg()));
assert_eq!(
exact_not_equal(&[small_int(1)], &mut context),
Err(badarg())
);
}
#[test]
fn comparison_bifs_use_beam_total_term_order_across_types() {
let mut context = context();
assert_eq!(
less_than(&[small_int(1), Term::atom(Atom::OK)], &mut context),
Ok(Term::atom(Atom::TRUE))
);
assert_eq!(
greater_equal(&[Term::atom(Atom::OK), small_int(1)], &mut context),
Ok(Term::atom(Atom::TRUE))
);
assert_eq!(
less_than(&[Term::atom(Atom::OK), small_int(1)], &mut context),
Ok(Term::atom(Atom::FALSE))
);
let mut list_heap = [0_u64; 2];
let list = write_cons(&mut list_heap, small_int(1), Term::NIL).expect("cons");
assert_eq!(
less_than(&[Term::NIL, list], &mut context),
Ok(Term::atom(Atom::TRUE))
);
let mut tuple_heap = [0_u64; 1];
let tuple = write_tuple(&mut tuple_heap, &[]).expect("tuple");
let mut map_heap = [0_u64; 2];
let map = write_map(&mut map_heap, &[], &[]).expect("map");
assert_eq!(
less_than(&[tuple, map], &mut context),
Ok(Term::atom(Atom::TRUE))
);
assert_eq!(
greater_equal(&[map, tuple], &mut context),
Ok(Term::atom(Atom::TRUE))
);
}
#[test]
fn comparison_bifs_agree_with_opcode_compare_cmp() {
let mut context = context();
let mut list_heap = [0_u64; 2];
let list = write_cons(&mut list_heap, small_int(1), Term::NIL).expect("cons");
let mut tuple_heap = [0_u64; 1];
let tuple = write_tuple(&mut tuple_heap, &[]).expect("tuple");
let pairs = [
(small_int(1), Term::atom(Atom::OK)),
(Term::atom(Atom::OK), small_int(1)),
(small_int(1), small_int(2)),
(Term::NIL, list),
(tuple, list),
(Term::atom(Atom::OK), Term::atom(Atom::OK)),
];
for (left, right) in pairs {
let opcode_lt = compare::cmp(left, right) == Ordering::Less;
let opcode_ge = compare::cmp(left, right) != Ordering::Less;
assert_eq!(
less_than(&[left, right], &mut context),
Ok(Term::atom(if opcode_lt { Atom::TRUE } else { Atom::FALSE })),
"less_than disagrees with opcode cmp on {left:?} < {right:?}"
);
assert_eq!(
greater_equal(&[left, right], &mut context),
Ok(Term::atom(if opcode_ge { Atom::TRUE } else { Atom::FALSE })),
"greater_equal disagrees with opcode cmp on {left:?} >= {right:?}"
);
}
}
#[test]
fn utility_bifs_exit_or_return_true() {
let mut context = context();
assert_eq!(
error(&[Term::atom(Atom::BADARG)], &mut context),
Err(Term::atom(Atom::BADARG))
);
assert_eq!(
display(&[small_int(42)], &mut context),
Ok(Term::atom(Atom::TRUE))
);
}
#[test]
fn utility_bifs_return_badarg_for_wrong_arity() {
let mut context = context();
assert_eq!(error(&[], &mut context), Err(badarg()));
assert_eq!(display(&[], &mut context), Err(badarg()));
}
#[test]
fn register_gate1_bifs_registers_all_minimum_mfas() {
let atom_table = AtomTable::new();
let registry = BifRegistryImpl::new();
register_gate1_bifs(®istry, &atom_table).expect("gate 1 BIF registration");
let erlang = atom_table.intern("erlang");
for (name, arity) in [
("+", 2),
("-", 2),
("*", 2),
("div", 2),
("rem", 2),
("<", 2),
(">=", 2),
("=:=", 2),
("=/=", 2),
("error", 1),
("display", 1),
("get_module_info", 1),
("get_module_info", 2),
("send_after", 3),
("start_timer", 3),
("cancel_timer", 1),
("load_module", 2),
("purge_module", 1),
("delete_module", 1),
("check_old_code", 1),
("check_process_code", 2),
] {
let function = atom_table.intern(name);
assert!(
registry.lookup(erlang, function, arity).is_some(),
"missing erlang:{name}/{arity}"
);
}
}
#[test]
fn register_gate1_bifs_fails_when_called_twice() {
let atom_table = AtomTable::new();
let registry = BifRegistryImpl::new();
register_gate1_bifs(®istry, &atom_table).expect("first registration");
assert!(register_gate1_bifs(®istry, &atom_table).is_err());
}
#[test]
fn timer_bifs_schedule_start_and_cancel_round_trips() {
let timers = Arc::new(Mutex::new(TimerWheel::new()));
let mut context = ProcessContext::with_timer_services(7, Arc::clone(&timers));
let send_ref = send_after(
&[small_int(100), Term::pid(9), Term::atom(Atom::OK)],
&mut context,
)
.expect("send_after schedules");
assert!(send_ref.as_small_int().is_some());
let start_ref = start_timer(
&[small_int(100), Term::pid(9), Term::atom(Atom::OK)],
&mut context,
)
.expect("start_timer schedules");
assert!(start_ref.as_small_int().is_some());
let remaining = cancel_timer(&[send_ref], &mut context).expect("cancel pending timer");
assert!(remaining.as_small_int().is_some());
assert_eq!(
cancel_timer(&[send_ref], &mut context),
Ok(Term::atom(Atom::FALSE))
);
let expired = timers
.lock()
.unwrap_or_else(|error| error.into_inner())
.tick_at(std::time::Instant::now() + Duration::from_millis(101));
assert_eq!(expired.len(), 1);
assert_eq!(
expired[0].reference.id(),
start_ref.as_small_int().unwrap_or_default() as u64
);
let tuple = Tuple::new(expired[0].message).expect("timeout tuple");
assert_eq!(tuple.get(0), Some(Term::atom(Atom::TIMEOUT)));
assert_eq!(tuple.get(1), Some(start_ref));
assert_eq!(tuple.get(2), Some(Term::atom(Atom::OK)));
}
}