use crate::atom::{Atom, AtomTable};
use crate::native::{BifRegistryImpl, NativeFn, NativeRegistrationError, ProcessContext};
use crate::term::Term;
use crate::term::boxed::{Cons, Tuple};
static ANYTHING_ATOM: std::sync::OnceLock<Atom> = std::sync::OnceLock::new();
fn anything_atom() -> Option<Atom> {
ANYTHING_ATOM.get().copied()
}
type SelectorBif = (&'static str, u8, NativeFn);
const SELECTOR_BIFS: &[SelectorBif] = &[
("new_selector", 0, bif_new_selector),
("insert_selector_handler", 3, bif_insert_selector_handler),
("map_selector", 2, bif_map_selector),
("merge_selector", 2, bif_merge_selector),
("remove_selector_handler", 2, bif_remove_selector_handler),
("select", 1, bif_select),
("select", 2, bif_select_with_timeout),
];
pub fn register_selector_bifs(
registry: &mut BifRegistryImpl,
atom_table: &AtomTable,
) -> Result<(), NativeRegistrationError> {
let module = atom_table.intern("gleam_erlang_ffi");
let _ = ANYTHING_ATOM.set(atom_table.intern("anything"));
for &(function_name, arity, native_function) in SELECTOR_BIFS {
let function = atom_table.intern(function_name);
registry.register(module, function, arity, native_function)?;
}
Ok(())
}
pub fn bif_new_selector(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
if !args.is_empty() {
return Err(badarg());
}
Ok(Term::NIL)
}
pub fn bif_insert_selector_handler(
args: &[Term],
context: &mut ProcessContext,
) -> Result<Term, Term> {
let [selector, tag, handler] = args else {
return Err(badarg());
};
let entry = context.alloc_tuple(&[*tag, *handler])?;
let cons = context.alloc_cons(entry, *selector)?;
Ok(cons)
}
pub fn bif_map_selector(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [selector, map_fun] = args else {
return Err(badarg());
};
let entries = list_to_vec(*selector)?;
let mut result = Term::NIL;
for entry_term in entries.into_iter().rev() {
let entry = Tuple::new(entry_term).ok_or_else(badarg)?;
if entry.arity() < 2 {
return Err(badarg());
}
let tag = entry.get(0).ok_or_else(badarg)?;
let original_handler = entry.get(1).ok_or_else(badarg)?;
let mapped_atom = Term::atom(Atom::new(mapped_atom_index()));
let wrapped = context.alloc_tuple(&[mapped_atom, *map_fun, original_handler])?;
let new_entry = context.alloc_tuple(&[tag, wrapped])?;
result = context.alloc_cons(new_entry, result)?;
}
Ok(result)
}
pub fn bif_merge_selector(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [selector_a, selector_b] = args else {
return Err(badarg());
};
if selector_a.is_nil() {
return Ok(*selector_b);
}
let entries_a = list_to_vec(*selector_a)?;
let mut result = *selector_b;
for entry in entries_a.into_iter().rev() {
result = context.alloc_cons(entry, result)?;
}
Ok(result)
}
pub fn bif_remove_selector_handler(
args: &[Term],
context: &mut ProcessContext,
) -> Result<Term, Term> {
let [selector, remove_tag] = args else {
return Err(badarg());
};
let entries = list_to_vec(*selector)?;
let mut result = Term::NIL;
for entry_term in entries.into_iter().rev() {
let entry = Tuple::new(entry_term).ok_or_else(badarg)?;
if entry.arity() < 2 {
return Err(badarg());
}
let tag = entry.get(0).ok_or_else(badarg)?;
if tag != *remove_tag {
result = context.alloc_cons(entry_term, result)?;
}
}
Ok(result)
}
pub fn bif_select(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [selector] = args else {
return Err(badarg());
};
select_impl(*selector, None, context)
}
pub fn bif_select_with_timeout(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [selector, timeout] = args else {
return Err(badarg());
};
let timeout_ms = timeout.as_small_int().ok_or_else(badarg)?;
if timeout_ms < 0 {
return Err(badarg());
}
let timeout_ms = timeout_ms as u64;
select_impl(*selector, Some(timeout_ms), context)
}
fn select_impl(
selector: Term,
timeout_ms: Option<u64>,
context: &mut ProcessContext,
) -> Result<Term, Term> {
let handlers = list_to_vec(selector)?;
if handlers.is_empty() {
if timeout_ms.is_some() {
return error_nil_tuple(context);
}
context.request_suspend(timeout_ms);
return Err(badarg());
}
let facility = match context.select_facility() {
Some(f) => f,
None => return Err(badarg()),
};
let message_count = facility.message_count();
for msg_index in 0..message_count {
let message = match facility.peek_message(msg_index) {
Some(m) => m,
None => continue,
};
for handler_term in &handlers {
let entry = Tuple::new(*handler_term).ok_or_else(badarg)?;
if entry.arity() < 2 {
return Err(badarg());
}
let tag = entry.get(0).ok_or_else(badarg)?;
let handler = entry.get(1).ok_or_else(badarg)?;
if message_matches_tag(message, tag) {
facility.remove_message(msg_index);
context.set_trampoline(handler, vec![message]);
return Ok(Term::NIL);
}
}
}
if let Some(0) = timeout_ms {
return error_nil_tuple(context);
}
context.request_suspend(timeout_ms);
Ok(Term::NIL)
}
fn message_matches_tag(message: Term, tag: Term) -> bool {
if let Some(atom) = tag.as_atom()
&& anything_atom().is_some_and(|a| a == atom)
{
return true;
}
if let Some(tuple) = Tuple::new(message)
&& let Some(first) = tuple.get(0)
&& first == tag
{
return true;
}
message == tag
}
fn error_nil_tuple(context: &mut ProcessContext) -> Result<Term, Term> {
context.alloc_tuple(&[Term::atom(Atom::ERROR), Term::NIL])
}
fn list_to_vec(term: Term) -> Result<Vec<Term>, Term> {
let mut elements = Vec::new();
let mut current = term;
loop {
if current.is_nil() {
return Ok(elements);
}
let cons = Cons::new(current).ok_or_else(badarg)?;
elements.push(cons.head());
current = cons.tail();
}
}
fn mapped_atom_index() -> u32 {
MAPPED_ATOM.get().map_or(9999, |a| a.index())
}
static MAPPED_ATOM: std::sync::OnceLock<Atom> = std::sync::OnceLock::new();
fn badarg() -> Term {
Term::atom(Atom::BADARG)
}
#[cfg(test)]
#[path = "selector_ffi_tests.rs"]
mod tests;