makepad/platform/script/tests/transient_bodies.rs
Admin 62d4f31b1e script: Splash VM heaps, isolates and lazy system modules
Every heap ref carries its heap tag, generation checks always on, rooted
async values. Splash isolates get a domain table and curated minimal
preludes; system modules under `mod` expand lazily on first touch. No
wall-clock script caps: isolation and force-stop instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

97 lines
3.8 KiB
Rust

//! Source evaluated at runtime with `eval_transient`: its body is freed once
//! nothing made from it is alive, and its slot reused; and a fn's content key
//! (what the draw shader cache keys by) follows its text, not its body slot.
use makepad_script::*;
fn test_vm() -> ScriptVm<'static> {
let host = Box::leak(Box::new(ScriptVmHost::new(0i32, ())));
ScriptVm {
host: host.host_context(),
bx: Box::new(ScriptVmBase::new()),
}
}
fn module(file: &str, code: &str) -> ScriptMod {
ScriptMod::new("", "", &file, 0, 0, &code, &[])
}
fn body_count(vm: &ScriptVm) -> usize {
vm.bx.code.bodies.borrow().iter().filter(|b| !matches!(b.source, ScriptSource::Free)).count()
}
fn fn_ip(vm: &ScriptVm, obj: ScriptObject, name: LiveId) -> ScriptIp {
let f = vm.bx.heap.value(obj, name.into(), NoTrap).as_object().expect("a fn");
match vm.bx.heap.as_fn(f) {
Some(ScriptFnPtr::Script(ip)) => ip,
_ => panic!("not a script fn"),
}
}
#[test]
fn unreferenced_transient_bodies_are_freed_and_reused() {
let mut vm = test_vm();
vm.gc();
let before = body_count(&vm);
let len_before = vm.bx.code.bodies.borrow().len();
for k in 0..50 {
let v = vm.eval_transient(module(&format!("gen://{k}"), &format!("{{a: {k} f: fn() {{ return {k} }}}}")));
assert!(v.as_object().is_some());
vm.gc();
}
// Nothing kept any of them: every one was freed and its slot reused
// (the last stays while the thread's ip points into it).
assert!(body_count(&vm) <= before + 1);
assert!(vm.bx.code.bodies.borrow().len() <= len_before + 2, "slots are reused");
}
#[test]
fn a_live_fn_keeps_its_body() {
let mut vm = test_vm();
let v = vm.eval_transient(module("gen://kept", "{f: fn() { return 7 }}"));
let obj = v.as_object().unwrap();
let kept = vm.bx.heap.new_object_ref(obj);
let ip = fn_ip(&vm, obj, id!(f));
for k in 0..20 {
vm.eval_transient(module(&format!("gen://other/{k}"), "{g: fn() { return 1 }}"));
vm.gc();
}
let bodies = vm.bx.code.bodies.borrow();
let body = &bodies[ip.body as usize];
assert!(matches!(&body.source, ScriptSource::Mod(m) if m.file == "gen://kept"), "the kept fn's body is still its own");
drop(bodies);
drop(kept);
vm.gc();
assert!(matches!(vm.bx.code.bodies.borrow()[ip.body as usize].source, ScriptSource::Free));
}
#[test]
fn a_kept_closure_keeps_its_body_and_scope() {
let mut vm = test_vm();
// The closure reads `k` from the transient body's scope: holding the fn
// keeps the body, and the body keeps that scope.
let f = vm.eval_transient(module("gen://closure", "let k = 41\nfn() { return k + 1 }")).as_object().unwrap();
let f = vm.bx.heap.new_fn_ref(f);
for k in 0..20 {
vm.eval_transient(module(&format!("gen://churn/{k}"), "{g: fn() { return 1 } o: {x: [1 2 3]}}"));
vm.gc();
}
let r = vm.call(f.as_object().into(), &[]);
assert_eq!(r.as_f64(), Some(42.0));
}
#[test]
fn fn_content_key_follows_the_text() {
let mut vm = test_vm();
let a = vm.eval(module("gen://same", "{f: fn() { return 1 }}")).as_object().unwrap();
let _keep_a = vm.bx.heap.new_object_ref(a);
let key_a = vm.bx.code.fn_content_key(fn_ip(&vm, a, id!(f)));
// The same text in another body can name other things: another key.
let b = vm.eval(module("gen://elsewhere", "{f: fn() { return 1 }}")).as_object().unwrap();
assert_ne!(vm.bx.code.fn_content_key(fn_ip(&vm, b, id!(f))), key_a);
// The first body re-evaluated with other text keeps its ips but not its key.
let c = vm.eval(module("gen://same", "{f: fn() { return 2 }}")).as_object().unwrap();
let ip_c = fn_ip(&vm, c, id!(f));
assert_eq!(ip_c, fn_ip(&vm, a, id!(f)));
assert_ne!(vm.bx.code.fn_content_key(ip_c), key_a);
}