beamr 0.4.9

A Rust runtime with the BEAM's execution model, targeting Gleam
Documentation
use crate::atom::{Atom, AtomTable};
use crate::native::stdlib_stubs::lists_bifs::{
    bif_lists_flatten, bif_lists_keydelete, bif_lists_keyfind, bif_lists_keysort,
    bif_lists_keystore, bif_lists_last, bif_lists_member, bif_lists_nth, bif_lists_sort,
    bif_lists_unzip, bif_lists_zip,
};
use crate::native::stdlib_stubs::lists_hof_bifs::{
    bif_lists_filter, bif_lists_foreach, resume_lists_continuation,
};
use crate::native::stdlib_stubs::maps_bifs::{
    ContinuationStep, bif_maps_map, resume_maps_continuation,
};
use crate::native::{NativeContinuation, ProcessContext};
use crate::process::Process;
use crate::term::Term;
use crate::term::boxed::{Cons, Map, Tuple, write_closure};

fn context(process: &mut Process) -> ProcessContext<'_> {
    let mut context = ProcessContext::new();
    context.set_atom_table(Some(std::sync::Arc::new(AtomTable::with_common_atoms())));
    context.attach_process(process, 0);
    context
}

fn list_from_slice(ctx: &mut ProcessContext, elements: &[Term]) -> Term {
    let mut tail = Term::NIL;
    for element in elements.iter().rev() {
        tail = ctx
            .alloc_cons(*element, tail)
            .expect("test cons allocation");
    }
    tail
}

fn list_to_vec(term: Term) -> Vec<Term> {
    let mut elements = Vec::new();
    let mut current = term;
    while !current.is_nil() {
        let cons = Cons::new(current).expect("proper list");
        elements.push(cons.head());
        current = cons.tail();
    }
    elements
}

fn tuple_to_vec(term: Term) -> Vec<Term> {
    let tuple = Tuple::new(term).expect("tuple");
    (0..tuple.arity())
        .map(|index| tuple.get(index).expect("tuple element"))
        .collect()
}

fn closure(process: &mut Process, unique_id: u64) -> Term {
    closure_with_arity(process, 1, unique_id)
}

fn closure_with_arity(process: &mut Process, arity: u8, unique_id: u64) -> Term {
    let heap = process.heap_mut().alloc_slice(7).expect("closure heap");
    write_closure(heap, Atom::OK, 0, arity, 1, unique_id, &[]).expect("closure")
}

#[test]
fn list_query_bifs_return_expected_terms() {
    let mut process = Process::new(1, 512);
    let mut ctx = context(&mut process);
    let a = Term::atom(Atom::OK);
    let b = Term::atom(Atom::ERROR);
    let c = Term::atom(Atom::TRUE);
    let list = list_from_slice(&mut ctx, &[a, b, c]);

    assert_eq!(
        bif_lists_nth(&[Term::small_int(2), list], &mut ctx).expect("nth"),
        b
    );
    assert_eq!(
        bif_lists_member(&[b, list], &mut ctx).expect("member"),
        Term::atom(Atom::TRUE)
    );
    assert_eq!(bif_lists_last(&[list], &mut ctx).expect("last"), c);

    let tuple_a = ctx.alloc_tuple(&[a, Term::small_int(1)]).expect("tuple a");
    let tuple_b = ctx.alloc_tuple(&[b, Term::small_int(2)]).expect("tuple b");
    let tuples = list_from_slice(&mut ctx, &[tuple_a, tuple_b]);
    assert_eq!(
        bif_lists_keyfind(&[b, Term::small_int(1), tuples], &mut ctx).expect("keyfind hit"),
        tuple_b
    );
    assert_eq!(
        bif_lists_keyfind(&[c, Term::small_int(1), tuples], &mut ctx).expect("keyfind miss"),
        Term::atom(Atom::FALSE)
    );
}

#[test]
fn list_transform_bifs_build_expected_results() {
    let mut process = Process::new(1, 1024);
    let mut ctx = context(&mut process);
    let unsorted = list_from_slice(
        &mut ctx,
        &[Term::small_int(3), Term::small_int(1), Term::small_int(2)],
    );
    let sorted = bif_lists_sort(&[unsorted], &mut ctx).expect("sort");
    assert_eq!(
        list_to_vec(sorted),
        vec![Term::small_int(1), Term::small_int(2), Term::small_int(3)]
    );

    let nested_inner = list_from_slice(&mut ctx, &[Term::small_int(3)]);
    let nested_mid = list_from_slice(&mut ctx, &[Term::small_int(2), nested_inner]);
    let nested = list_from_slice(&mut ctx, &[Term::small_int(1), nested_mid]);
    let flattened = bif_lists_flatten(&[nested], &mut ctx).expect("flatten");
    assert_eq!(
        list_to_vec(flattened),
        vec![Term::small_int(1), Term::small_int(2), Term::small_int(3)]
    );

    let left = list_from_slice(&mut ctx, &[Term::atom(Atom::OK), Term::atom(Atom::ERROR)]);
    let right = list_from_slice(&mut ctx, &[Term::small_int(1), Term::small_int(2)]);
    let zipped = bif_lists_zip(&[left, right], &mut ctx).expect("zip");
    let pairs = list_to_vec(zipped);
    assert_eq!(
        tuple_to_vec(pairs[0]),
        vec![Term::atom(Atom::OK), Term::small_int(1)]
    );
    assert_eq!(
        tuple_to_vec(pairs[1]),
        vec![Term::atom(Atom::ERROR), Term::small_int(2)]
    );

    let unzipped = bif_lists_unzip(&[zipped], &mut ctx).expect("unzip");
    let parts = tuple_to_vec(unzipped);
    assert_eq!(
        list_to_vec(parts[0]),
        vec![Term::atom(Atom::OK), Term::atom(Atom::ERROR)]
    );
    assert_eq!(
        list_to_vec(parts[1]),
        vec![Term::small_int(1), Term::small_int(2)]
    );
}

#[test]
fn key_manipulation_bifs_preserve_order_and_key_semantics() {
    let mut process = Process::new(1, 1024);
    let mut ctx = context(&mut process);
    let a = Term::atom(Atom::OK);
    let b = Term::atom(Atom::ERROR);
    let tuple_a = ctx.alloc_tuple(&[a, Term::small_int(1)]).expect("tuple a");
    let tuple_b = ctx.alloc_tuple(&[b, Term::small_int(2)]).expect("tuple b");
    let tuples = list_from_slice(&mut ctx, &[tuple_a, tuple_b]);
    let new_a = ctx
        .alloc_tuple(&[a, Term::small_int(99)])
        .expect("new tuple a");

    let stored =
        bif_lists_keystore(&[a, Term::small_int(1), tuples, new_a], &mut ctx).expect("keystore");
    assert_eq!(list_to_vec(stored), vec![new_a, tuple_b]);

    let deleted =
        bif_lists_keydelete(&[a, Term::small_int(1), tuples], &mut ctx).expect("keydelete");
    assert_eq!(list_to_vec(deleted), vec![tuple_b]);

    let keyed = list_from_slice(&mut ctx, &[tuple_b, tuple_a]);
    let sorted = bif_lists_keysort(&[Term::small_int(1), keyed], &mut ctx).expect("keysort");
    assert_eq!(list_to_vec(sorted), vec![tuple_b, tuple_a]);
}

#[test]
fn list_query_bifs_reject_invalid_positions_and_empty_last() {
    let mut process = Process::new(1, 512);
    let mut ctx = context(&mut process);
    let list = list_from_slice(&mut ctx, &[Term::small_int(1)]);
    let short_tuple = ctx.alloc_tuple(&[Term::small_int(1)]).expect("tuple");
    let tuple_list = list_from_slice(&mut ctx, &[short_tuple]);

    assert_eq!(
        bif_lists_nth(&[Term::small_int(0), list], &mut ctx),
        Err(Term::atom(Atom::BADARG))
    );
    assert_eq!(
        bif_lists_nth(&[Term::small_int(2), list], &mut ctx),
        Err(Term::atom(Atom::BADARG))
    );
    assert_eq!(
        bif_lists_keyfind(
            &[Term::small_int(1), Term::small_int(2), tuple_list],
            &mut ctx
        ),
        Err(Term::atom(Atom::BADARG))
    );
    assert_eq!(
        bif_lists_last(&[Term::NIL], &mut ctx),
        Err(Term::atom(Atom::BADARG))
    );
}

#[test]
fn transform_bifs_reject_improper_shapes() {
    let mut process = Process::new(1, 512);
    let mut ctx = context(&mut process);
    let one = list_from_slice(&mut ctx, &[Term::small_int(1)]);
    let two = list_from_slice(&mut ctx, &[Term::small_int(1), Term::small_int(2)]);
    let bad_pair = ctx
        .alloc_tuple(&[Term::small_int(1), Term::small_int(2), Term::small_int(3)])
        .expect("bad pair");
    let bad_pair_list = list_from_slice(&mut ctx, &[bad_pair]);

    assert_eq!(
        bif_lists_zip(&[one, two], &mut ctx),
        Err(Term::atom(Atom::BADARG))
    );
    assert_eq!(
        bif_lists_flatten(&[Term::small_int(1)], &mut ctx),
        Err(Term::atom(Atom::BADARG))
    );
    assert_eq!(
        bif_lists_unzip(&[bad_pair_list], &mut ctx),
        Err(Term::atom(Atom::BADARG))
    );
}

#[test]
fn filter_uses_continuation_trampoline() {
    let mut process = Process::new(1, 1024);
    let fun = closure(&mut process, 0x127);
    let mut ctx = context(&mut process);
    let list = list_from_slice(
        &mut ctx,
        &[Term::small_int(1), Term::small_int(2), Term::small_int(3)],
    );

    let placeholder = bif_lists_filter(&[fun, list], &mut ctx).expect("filter starts");
    assert_eq!(placeholder, Term::NIL);
    let request = ctx.take_trampoline().expect("filter trampoline");
    assert_eq!(request.fun, fun);
    assert_eq!(request.args, vec![Term::small_int(1)]);
    let Some(NativeContinuation::Lists(state)) = request.continuation else {
        panic!("expected lists continuation");
    };

    let next =
        resume_lists_continuation(state, Term::atom(Atom::TRUE), &mut ctx).expect("filter resumes");
    let ContinuationStep::Call {
        args, continuation, ..
    } = next
    else {
        panic!("expected next filter call");
    };
    assert_eq!(args, vec![Term::small_int(2)]);

    let next = resume_lists_continuation(
        match continuation {
            NativeContinuation::Lists(state) => state,
            _ => panic!("expected lists continuation"),
        },
        Term::atom(Atom::FALSE),
        &mut ctx,
    )
    .expect("filter resumes again");
    let ContinuationStep::Call { continuation, .. } = next else {
        panic!("expected final filter call");
    };
    let done = resume_lists_continuation(
        match continuation {
            NativeContinuation::Lists(state) => state,
            _ => panic!("expected lists continuation"),
        },
        Term::atom(Atom::TRUE),
        &mut ctx,
    )
    .expect("filter completes");
    let ContinuationStep::Done(result) = done else {
        panic!("expected filter done");
    };
    assert_eq!(
        list_to_vec(result),
        vec![Term::small_int(1), Term::small_int(3)]
    );
}

#[test]
fn maps_map_uses_continuation_trampoline_and_preserves_original() {
    let mut process = Process::new(1, 1024);
    let fun = closure_with_arity(&mut process, 2, 0x169);
    let mut ctx = context(&mut process);
    let keys = [Term::atom(Atom::OK), Term::atom(Atom::ERROR)];
    let values = [Term::small_int(1), Term::small_int(2)];
    let map_term = ctx.alloc_map(&keys, &values).expect("map");

    let placeholder = bif_maps_map(&[fun, map_term], &mut ctx).expect("map starts");
    assert_eq!(placeholder, Term::NIL);
    let request = ctx.take_trampoline().expect("map trampoline");
    assert_eq!(request.fun, fun);
    assert_eq!(request.args, vec![keys[0], values[0]]);
    let Some(NativeContinuation::Maps(state)) = request.continuation else {
        panic!("expected maps continuation");
    };

    let next = resume_maps_continuation(state, Term::small_int(2), &mut ctx).expect("map resumes");
    let ContinuationStep::Call {
        args, continuation, ..
    } = next
    else {
        panic!("expected next map call");
    };
    assert_eq!(args, vec![keys[1], values[1]]);

    let done = resume_maps_continuation(
        match continuation {
            NativeContinuation::Maps(state) => state,
            _ => panic!("expected maps continuation"),
        },
        Term::small_int(3),
        &mut ctx,
    )
    .expect("map completes");
    let ContinuationStep::Done(result) = done else {
        panic!("expected map done");
    };
    let mapped = Map::new(result).expect("mapped result");
    assert_eq!(mapped.get(keys[0]), Some(Term::small_int(2)));
    assert_eq!(mapped.get(keys[1]), Some(Term::small_int(3)));
    let original = Map::new(map_term).expect("original map");
    assert_eq!(original.get(keys[0]), Some(values[0]));
    assert_eq!(original.get(keys[1]), Some(values[1]));
}

#[test]
fn foreach_discards_results_and_finishes_with_ok() {
    let mut process = Process::new(1, 1024);
    let fun = closure(&mut process, 0x128);
    let mut ctx = context(&mut process);
    let list = list_from_slice(&mut ctx, &[Term::small_int(1), Term::small_int(2)]);

    assert_eq!(
        bif_lists_foreach(&[fun, list], &mut ctx).expect("foreach starts"),
        Term::NIL
    );
    let request = ctx.take_trampoline().expect("foreach trampoline");
    assert_eq!(request.args, vec![Term::small_int(1)]);
    let Some(NativeContinuation::Lists(state)) = request.continuation else {
        panic!("expected lists continuation");
    };
    let next =
        resume_lists_continuation(state, Term::small_int(99), &mut ctx).expect("foreach resumes");
    let ContinuationStep::Call {
        args, continuation, ..
    } = next
    else {
        panic!("expected second foreach call");
    };
    assert_eq!(args, vec![Term::small_int(2)]);
    let done = resume_lists_continuation(
        match continuation {
            NativeContinuation::Lists(state) => state,
            _ => panic!("expected lists continuation"),
        },
        Term::small_int(100),
        &mut ctx,
    )
    .expect("foreach completes");
    let ContinuationStep::Done(result) = done else {
        panic!("expected foreach done");
    };
    assert_eq!(result, Term::atom(Atom::OK));
}