use stet_core::context::Context;
use stet_core::object::PsValue;
fn ctx() -> Context {
let mut ctx = Context::new();
stet_ops::build_system_dict(&mut ctx);
ctx.exec_sync_fn = Some(stet_engine::eval::exec_sync);
ctx
}
fn run(ctx: &mut Context, src: &str) {
stet_engine::eval::parse_and_exec(ctx, src.as_bytes())
.unwrap_or_else(|e| panic!("PS execution failed: {e:?}\nsource:\n{src}"));
}
fn top_int(ctx: &mut Context) -> i64 {
match ctx.o_stack.pop().expect("operand expected").value {
PsValue::Int(v) => v,
other => panic!("expected an integer, got {other:?}"),
}
}
#[test]
fn restore_reclaims_arrays() {
let mut c = ctx();
run(&mut c, "/warm [1 2 3] def\n");
let before = c.arrays.local.allocated_objects();
let entities_before = c.arrays.local.entities.len();
run(&mut c, "save [ 0 1 2 3 4 5 6 7 8 9 ] pop restore\n");
assert_eq!(
c.arrays.local.allocated_objects(),
before,
"array slots allocated under the save should be reclaimed"
);
assert_eq!(
c.arrays.local.entities.len(),
entities_before,
"array entities allocated under the save should be reclaimed"
);
}
#[test]
fn restore_reclaims_strings_and_dicts() {
let mut c = ctx();
run(&mut c, "/warm (x) def\n");
let str_before = c.strings.local.data_len();
let dict_before = c.dicts.local.dict_slots();
run(&mut c, "save 4096 string pop 32 dict pop restore\n");
assert_eq!(
c.strings.local.data_len(),
str_before,
"string bytes allocated under the save should be reclaimed"
);
assert_eq!(
c.dicts.local.dict_slots(),
dict_before,
"dict slots allocated under the save should be reclaimed"
);
}
#[test]
fn repeated_save_restore_cycles_do_not_accumulate() {
let mut c = ctx();
run(
&mut c,
"/cycle { save [ 0 1 2 3 4 5 6 7 8 9 ] pop 64 string pop restore } def\n",
);
run(&mut c, "100 { cycle } repeat\n");
let a = c.arrays.local.allocated_objects();
run(&mut c, "100 { cycle } repeat\n");
let b = c.arrays.local.allocated_objects();
run(&mut c, "20000 { cycle } repeat\n");
let d = c.arrays.local.allocated_objects();
assert_eq!(
d - b,
b - a,
"20000 cycles must cost no more arena than 100 -- growth should come \
only from parsing the procedure, not from the iterations ({} vs {})",
d - b,
b - a
);
}
#[test]
fn restore_reverts_mutation_of_pre_save_array() {
let mut c = ctx();
run(&mut c, "/a [1 2 3] def\n");
run(&mut c, "save a 0 99 put restore\n");
run(&mut c, "a 0 get\n");
assert_eq!(top_int(&mut c), 1, "put under a save must be rolled back");
run(&mut c, "a 2 get\n");
assert_eq!(top_int(&mut c), 3, "untouched elements must survive");
}
#[test]
fn restore_reverts_mutation_of_pre_save_string_and_dict() {
let mut c = ctx();
run(&mut c, "/s (abc) def\n/d 4 dict def\nd /k 1 put\n");
run(&mut c, "save s 0 (Z) 0 get put d /k 42 put restore\n");
run(&mut c, "s 0 get\n");
assert_eq!(top_int(&mut c), b'a' as i64, "string put must roll back");
run(&mut c, "d /k get\n");
assert_eq!(top_int(&mut c), 1, "dict put must roll back");
}
#[test]
fn nested_restore_to_outer_level_reclaims_all_levels() {
let mut c = ctx();
run(&mut c, "/a [1 2 3] def\n");
let before = c.arrays.local.allocated_objects();
run(
&mut c,
"save % outer
[ 0 1 2 3 ] pop
a 0 50 put
save % inner
[ 4 5 6 7 ] pop
a 1 60 put
pop % discard inner save object
restore % restore straight to the outer level
",
);
assert_eq!(
c.arrays.local.allocated_objects(),
before,
"restoring to the outer level should reclaim both levels"
);
run(&mut c, "a 0 get\n");
assert_eq!(top_int(&mut c), 1, "outer mutation rolled back");
run(&mut c, "a 1 get\n");
assert_eq!(top_int(&mut c), 2, "inner mutation rolled back");
}
#[test]
fn entities_allocated_after_restore_do_not_alias_reclaimed_ones() {
let mut c = ctx();
run(&mut c, "/keep [7 7 7] def\n");
run(&mut c, "save /gone [1 2 3] def restore\n");
run(&mut c, "userdict /gone known\n");
let known = matches!(
c.o_stack.pop().expect("bool expected").value,
PsValue::Bool(true)
);
assert!(
!known,
"a definition made under the save should not survive"
);
run(&mut c, "/fresh [8 8 8] def\nfresh 0 get\n");
assert_eq!(
top_int(&mut c),
8,
"reused entity must hold the new contents"
);
run(&mut c, "keep 0 get\n");
assert_eq!(top_int(&mut c), 7, "unrelated pre-save object is untouched");
run(&mut c, "fresh length\n");
assert_eq!(top_int(&mut c), 3);
}
#[test]
fn invalidrestore_is_refused_and_reclaims_nothing() {
let mut c = ctx();
run(&mut c, "/warm [1] def\n");
let src = "save [ 1 2 3 ] exch restore\n";
let err = stet_engine::eval::parse_and_exec(&mut c, src.as_bytes());
assert!(err.is_err(), "restore across a newer composite must fail");
run(&mut c, "count 0 gt { } if\n");
}