beamr 0.3.12

A Rust runtime with the BEAM's execution model, targeting Gleam
Documentation
use std::sync::Arc;
use std::sync::Mutex;

use crate::atom::{Atom, AtomTable};
use crate::native::{BifRegistryImpl, ProcessContext, stdlib_stubs::register_stdlib_stubs};
use crate::term::Term;
use crate::term::binary::{self, Binary};
use crate::term::boxed::{Cons, Float, Map, Tuple, write_float, write_tuple};

use super::gleam_stdlib_ffi2::*;

fn context() -> ProcessContext {
    let table = Arc::new(AtomTable::with_common_atoms());
    let mut context = ProcessContext::new();
    context.set_atom_table(Some(table));
    context
}

fn atom(context: &ProcessContext, name: &str) -> Term {
    let atom = context
        .atom_table()
        .map(|table| table.intern(name))
        .expect("atom table should be configured");
    Term::atom(atom)
}

fn binary(bytes: &[u8]) -> Term {
    let heap = Box::leak(vec![0u64; 2 + binary::packed_word_count(bytes.len())].into_boxed_slice());
    binary::write_binary(heap, bytes).expect("binary")
}

fn float(value: f64) -> Term {
    let heap = Box::leak(Box::new([0u64; 2]));
    write_float(heap, value).expect("float")
}

fn tuple(values: &[Term]) -> Term {
    let heap = Box::leak(vec![0u64; 1 + values.len()].into_boxed_slice());
    write_tuple(heap, values).expect("tuple")
}

fn list(values: &[Term]) -> Term {
    let mut tail = Term::NIL;
    for value in values.iter().rev() {
        let heap = Box::leak(Box::new([0u64; 2]));
        tail = crate::term::boxed::write_cons(heap, *value, tail).expect("cons");
    }
    tail
}

fn assert_binary(term: Term, expected: &[u8]) {
    let binary = Binary::new(term).expect("binary term");
    assert_eq!(binary.as_bytes(), expected);
}

fn assert_ok_tuple(term: Term, expected: Term) {
    let tuple = Tuple::new(term).expect("tuple");
    assert_eq!(tuple.arity(), 2);
    assert_eq!(tuple.get(0), Some(Term::atom(Atom::OK)));
    assert_eq!(tuple.get(1), Some(expected));
}

fn assert_error_nil_tuple(term: Term) {
    let tuple = Tuple::new(term).expect("tuple");
    assert_eq!(tuple.arity(), 2);
    assert_eq!(tuple.get(0), Some(Term::atom(Atom::ERROR)));
    assert_eq!(tuple.get(1), Some(Term::NIL));
}

#[test]
fn classify_dynamic_returns_type_binaries() {
    let mut context = context();

    let result = bif_classify_dynamic(&[Term::small_int(42)], &mut context).unwrap();
    assert_eq!(Binary::new(result).unwrap().as_bytes(), b"Int");

    let result = bif_classify_dynamic(&[binary(b"hello")], &mut context).unwrap();
    assert_eq!(Binary::new(result).unwrap().as_bytes(), b"String");

    let result = bif_classify_dynamic(&[Term::atom(Atom::TRUE)], &mut context).unwrap();
    assert_eq!(Binary::new(result).unwrap().as_bytes(), b"Bool");

    let result = bif_classify_dynamic(&[Term::atom(Atom::NIL)], &mut context).unwrap();
    assert_eq!(Binary::new(result).unwrap().as_bytes(), b"Nil");
}

#[test]
fn is_null_distinguishes_nil_from_other_terms() {
    let mut context = context();

    assert_eq!(
        bif_is_null(&[Term::NIL], &mut context),
        Ok(Term::atom(Atom::TRUE))
    );
    assert_eq!(
        bif_is_null(&[Term::small_int(42)], &mut context),
        Ok(Term::atom(Atom::FALSE))
    );
}

#[test]
fn list_stub_accepts_existing_lists() {
    let mut context = context();
    let values = list(&[Term::small_int(1), Term::small_int(2)]);

    assert_eq!(
        bif_list(
            &[Term::NIL, binary(b"List"), Term::NIL, values, Term::NIL],
            &mut context
        ),
        Ok(values)
    );
}

#[test]
fn map_get_returns_gleam_result_tuples() {
    let mut context = context();
    let a = atom(&context, "a");
    let b = atom(&context, "b");
    let map = bif_dict(&[list(&[tuple(&[a, Term::small_int(1)])])], &mut context).expect("map");

    assert_ok_tuple(
        bif_map_get(&[map, a], &mut context).expect("ok tuple"),
        Term::small_int(1),
    );
    assert_error_nil_tuple(bif_map_get(&[map, b], &mut context).expect("error tuple"));
}

#[derive(Default)]
struct RecordingSink(Mutex<Vec<u8>>);

impl crate::io::IoSink for RecordingSink {
    fn write(&self, bytes: &[u8]) {
        self.0.lock().expect("sink lock").extend_from_slice(bytes);
    }
}

#[test]
fn print_wrappers_write_to_configured_sink() {
    let sink = Arc::new(RecordingSink::default());
    let mut context = context();
    context.set_io_sink(sink.clone());

    assert_eq!(
        bif_print(&[binary(b"a")], &mut context),
        Ok(Term::atom(Atom::OK))
    );
    assert_eq!(
        bif_print_error(&[binary(b"b")], &mut context),
        Ok(Term::atom(Atom::OK))
    );
    assert_eq!(
        bif_println(&[binary(b"c")], &mut context),
        Ok(Term::atom(Atom::OK))
    );
    assert_eq!(
        bif_println_error(&[binary(b"d")], &mut context),
        Ok(Term::atom(Atom::OK))
    );
    assert_eq!(&*sink.0.lock().expect("sink lock"), b"abc\nd\n");
}

#[test]
fn dict_converts_pair_list_to_map_with_last_value_winning() {
    let mut context = context();
    let a = atom(&context, "a");
    let b = atom(&context, "b");
    let pairs = list(&[
        tuple(&[a, Term::small_int(1)]),
        tuple(&[b, Term::small_int(2)]),
        tuple(&[a, Term::small_int(3)]),
    ]);

    let result = bif_dict(&[pairs], &mut context).expect("map");
    let map = Map::new(result).expect("map");
    assert_eq!(map.len(), 2);
    assert_eq!(map.get(a), Some(Term::small_int(3)));
    assert_eq!(map.get(b), Some(Term::small_int(2)));
}

#[test]
fn float_converts_int_and_float_to_string_formats_float() {
    let mut context = context();
    let converted = bif_float(&[Term::small_int(7)], &mut context).expect("float");
    assert_eq!(Float::new(converted).expect("float").value(), 7.0);

    let result = bif_float_to_string(&[float(314_f64 / 100.0)], &mut context).expect("binary");
    assert_binary(result, b"3.14");
}

#[test]
fn index_reads_tuple_and_list_with_zero_based_index() {
    let mut context = context();
    let tuple = tuple(&[Term::small_int(10), Term::small_int(20)]);
    let list = list(&[Term::small_int(30), Term::small_int(40)]);

    assert_eq!(
        bif_index(&[tuple, Term::small_int(1)], &mut context),
        Ok(Term::small_int(20))
    );
    assert_eq!(
        bif_index(&[list, Term::small_int(0)], &mut context),
        Ok(Term::small_int(30))
    );
}

#[test]
fn int_accepts_small_int_only() {
    let mut context = context();
    assert_eq!(
        bif_int(&[Term::small_int(42)], &mut context),
        Ok(Term::small_int(42))
    );
    assert_eq!(
        bif_int(&[binary(b"42")], &mut context),
        Err(Term::atom(Atom::BADARG))
    );
}

#[test]
fn parse_int_and_int_from_base_string_return_result_tuples() {
    let mut context = context();

    let parsed = bif_parse_int(&[binary(b"42")], &mut context).expect("tuple");
    assert_ok_tuple(parsed, Term::small_int(42));
    let failed = bif_parse_int(&[binary(b"abc")], &mut context).expect("tuple");
    assert_error_nil_tuple(failed);
    let parsed_base = bif_int_from_base_string(&[binary(b"FF"), Term::small_int(16)], &mut context)
        .expect("tuple");
    assert_ok_tuple(parsed_base, Term::small_int(255));
}

#[test]
fn parse_float_returns_result_tuple() {
    let mut context = context();
    let parsed = bif_parse_float(&[binary(b"2.5")], &mut context).expect("tuple");
    let tuple = Tuple::new(parsed).expect("tuple");
    assert_eq!(tuple.get(0), Some(Term::atom(Atom::OK)));
    assert_eq!(
        Float::new(tuple.get(1).expect("value"))
            .expect("float")
            .value(),
        2.5
    );
    let failed = bif_parse_float(&[binary(b"nan?")], &mut context).expect("tuple");
    assert_error_nil_tuple(failed);
}

#[test]
fn wrap_list_preserves_lists_and_wraps_non_lists() {
    let mut context = context();
    let existing = list(&[Term::small_int(1)]);
    assert_eq!(bif_wrap_list(&[existing], &mut context), Ok(existing));

    let wrapped = bif_wrap_list(&[Term::small_int(2)], &mut context).expect("list");
    let cons = Cons::new(wrapped).expect("cons");
    assert_eq!(cons.head(), Term::small_int(2));
    assert_eq!(cons.tail(), Term::NIL);
}

#[test]
fn base16_and_base64_wrappers_encode_and_decode() {
    let mut context = context();
    let standard = atom(&context, "standard");

    let hex = bif_base16_encode(&[binary(b"hi")], &mut context).expect("hex");
    assert_binary(hex, b"6869");
    let decoded_hex = bif_base16_decode(&[hex], &mut context).expect("decoded");
    assert_binary(decoded_hex, b"hi");

    let encoded = bif_base64_encode(&[binary(b"hi"), standard], &mut context).expect("base64");
    assert_binary(encoded, b"aGk=");
    let decoded = bif_base64_decode(&[encoded], &mut context).expect("decoded");
    assert_binary(decoded, b"hi");
}

#[test]
fn bit_array_wrappers_operate_on_byte_aligned_binaries() {
    let mut context = context();

    let bit_array = bif_bit_array(&[binary(b"bits")], &mut context).expect("binary");
    assert_binary(bit_array, b"bits");

    let concatenated =
        bif_bit_array_concat(&[list(&[binary(b"a"), binary(b"b")])], &mut context).expect("binary");
    assert_binary(concatenated, b"ab");

    let padded = bif_bit_array_pad_to_bytes(&[binary(b"x")], &mut context).expect("binary");
    assert_binary(padded, b"x");

    let sliced = bif_bit_array_slice(
        &[binary(b"hello"), Term::small_int(1), Term::small_int(3)],
        &mut context,
    )
    .expect("slice");
    assert_binary(sliced, b"ell");

    let int_and_size =
        bif_bit_array_to_int_and_size(&[binary(&[0x01, 0x02])], &mut context).expect("tuple");
    let tuple = Tuple::new(int_and_size).expect("tuple");
    assert_eq!(tuple.get(0), Some(Term::small_int(258)));
    assert_eq!(tuple.get(1), Some(Term::small_int(16)));
}

#[test]
fn register_stdlib_stubs_includes_gleam_stdlib_ffi2_bifs() {
    let atom_table = AtomTable::with_common_atoms();
    let registry = BifRegistryImpl::new();
    register_stdlib_stubs(&registry, &atom_table).expect("registration");

    let expected = [
        ("classify_dynamic", 1),
        ("dict", 1),
        ("is_null", 1),
        ("list", 5),
        ("map_get", 2),
        ("print", 1),
        ("print_error", 1),
        ("println", 1),
        ("println_error", 1),
        ("float", 1),
        ("float_to_string", 1),
        ("index", 2),
        ("int", 1),
        ("int_from_base_string", 2),
        ("parse_float", 1),
        ("parse_int", 1),
        ("wrap_list", 1),
        ("base16_decode", 1),
        ("base16_encode", 1),
        ("base64_decode", 1),
        ("base64_encode", 2),
        ("bit_array", 1),
        ("bit_array_concat", 1),
        ("bit_array_pad_to_bytes", 1),
        ("bit_array_slice", 3),
        ("bit_array_to_int_and_size", 1),
    ];
    let module = atom_table.intern("gleam_stdlib");
    for (name, arity) in expected {
        let function = atom_table.intern(name);
        assert!(
            registry.lookup(module, function, arity).is_some(),
            "missing gleam_stdlib:{name}/{arity}"
        );
    }
}