use tatara_lisp_eval::{install_full_stdlib_with, EvalError, Interpreter, Seal, Value};
fn prototype() -> Interpreter<()> {
let mut interp = Interpreter::new();
let mut host = ();
install_full_stdlib_with(&mut interp, &mut host);
interp
}
fn eval(interp: &mut Interpreter<()>, src: &str) -> Result<Value, EvalError> {
let forms = tatara_lisp_eval::read_spanned(src).expect("parse");
let mut host = ();
interp.eval_program(&forms, &mut host)
}
fn int(v: Value) -> i64 {
match v {
Value::Int(n) => n,
other => panic!("expected an integer, got {other:?}"),
}
}
#[test]
fn a_fork_runs_the_full_stdlib_it_never_built() {
let mut child = prototype().fork();
let sum = eval(&mut child, "(foldl + 0 (range 1 5))").expect("stdlib is reachable");
assert_eq!(int(sum), 10, "1+2+3+4");
}
#[test]
fn the_shared_frames_are_shared_not_copied() {
let mut parent = prototype();
let mut child = parent.fork();
eval(&mut parent, "(define minted-after-the-fork 7)").expect("parent define");
let seen = eval(&mut child, "minted-after-the-fork").expect("child sees it");
assert_eq!(
int(seen),
7,
"a copied environment could not observe a later parent define"
);
}
#[test]
fn a_childs_define_is_invisible_to_the_parent_and_to_its_siblings() {
let parent = prototype();
let mut a = parent.fork();
let mut b = parent.fork();
eval(&mut a, "(define only-in-a 1)").expect("a defines");
let in_b = eval(&mut b, "only-in-a");
assert!(
matches!(in_b, Err(EvalError::UnboundSymbol { .. })),
"a sibling must not see it, got {in_b:?}"
);
let mut c = parent.fork();
let in_c = eval(&mut c, "only-in-a");
assert!(
matches!(in_c, Err(EvalError::UnboundSymbol { .. })),
"a later fork of the same parent must not see it either, got {in_c:?}"
);
}
#[test]
fn a_child_cannot_set_a_binding_it_inherited() {
let mut parent = prototype();
eval(&mut parent, "(define shared-counter 0)").expect("parent define");
let mut a = parent.fork();
let mut b = parent.fork();
let refused = eval(&mut a, "(set! shared-counter 99)");
match refused {
Err(EvalError::SetSealed { ref name, seal, .. }) => {
assert_eq!(&**name, "shared-counter");
assert_eq!(seal, Seal::Fork, "and it must say WHICH boundary was hit");
}
other => panic!("expected a sealed-write refusal, got {other:?}"),
}
let from_sibling = eval(&mut b, "shared-counter").expect("still bound");
assert_eq!(int(from_sibling), 0, "the write must not have landed");
}
#[test]
fn a_child_may_shadow_an_inherited_binding_and_mutate_the_shadow() {
let mut parent = prototype();
eval(&mut parent, "(define shared-counter 0)").expect("parent define");
let mut a = parent.fork();
let mut b = parent.fork();
eval(&mut a, "(define shared-counter 1)").expect("shadow it");
eval(&mut a, "(set! shared-counter 42)").expect("the shadow IS writable");
assert_eq!(int(eval(&mut a, "shared-counter").expect("read")), 42);
assert_eq!(
int(eval(&mut b, "shared-counter").expect("read")),
0,
"the sibling still sees the parent's value"
);
}
#[test]
fn the_refusal_is_worded_for_the_boundary_that_was_actually_crossed() {
let mut parent = prototype();
eval(&mut parent, "(define shared 0)").expect("define");
let mut child = parent.fork();
let err = eval(&mut child, "(set! shared 1)").expect_err("refused");
let msg = err.to_string();
assert!(
msg.contains("forked") || msg.contains("parent environment"),
"must name the fork boundary: {msg}"
);
assert!(
!msg.contains("macro"),
"must NOT claim this is a macro body: {msg}"
);
}
#[test]
fn popping_cannot_take_a_child_below_its_write_floor() {
use tatara_lisp_eval::Env;
let parent = Env::new();
parent.define("inherited", Value::Int(1));
let mut child = parent.sealed_below_top(Seal::Fork);
let floor_depth = child.frame_depth();
for _ in 0..10 {
child.pop();
}
assert_eq!(
child.frame_depth(),
floor_depth,
"the writable frame must survive"
);
child.define("mine", Value::Int(2));
assert!(
!child.set("inherited", Value::Int(99)),
"the inherited binding is still unwritable"
);
assert!(
matches!(parent.lookup("inherited"), Some(Value::Int(1))),
"and the parent is untouched"
);
}
#[test]
fn the_floor_guard_does_not_change_an_unsealed_environment() {
use tatara_lisp_eval::Env;
let mut env = Env::new();
assert_eq!(env.write_floor(), 0);
env.push();
env.push();
assert_eq!(env.frame_depth(), 3);
env.pop();
env.pop();
assert_eq!(env.frame_depth(), 1, "pops down to the root");
env.pop();
assert_eq!(env.frame_depth(), 1, "and stops there, as before");
}
#[test]
fn a_preamble_is_evaluated_once_and_every_body_gets_a_clean_slate() {
let mut proto = prototype();
eval(&mut proto, "(define (double x) (* x 2))").expect("preamble");
for i in 0..3 {
let mut body = proto.fork();
assert!(
eval(&mut body, "per-test").is_err(),
"iteration {i} started dirty"
);
eval(&mut body, "(define per-test (double 21))").expect("body");
assert_eq!(int(eval(&mut body, "per-test").expect("read")), 42);
}
}
#[test]
fn a_restart_does_not_inherit_the_dead_incarnations_state() {
let proto = prototype();
let mut first = proto.fork();
eval(&mut first, "(define crashed-with 'poison)").expect("first incarnation");
drop(first);
let mut restarted = proto.fork();
assert!(
eval(&mut restarted, "crashed-with").is_err(),
"the restart must start clean"
);
}