makepad/platform/script/tests/apply_diff.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

367 lines
14 KiB
Rust

//! Behaviour transcript of `#[derive(Script)]`'s generated `script_apply`: the same documents
//! applied to representative types under every `Apply` kind, each struct dumped field by field.
//! The transcript is the contract of the derive: a codegen change must reproduce it byte for
//! byte (run on the old and the new tree with `APPLY_DIFF_OUT=<file>` and compare the files).
//! The asserts pin the rules the transcript would otherwise only record.
use makepad_script::*;
use std::fmt::Write;
#[derive(Clone, Copy, Debug, PartialEq, Script, ScriptHook)]
enum Mode {
#[pick]
Fill,
Fixed,
Fit,
}
#[derive(Debug, Default, Script, ScriptHook)]
struct Inner {
#[live(1.0)]
a: f64,
#[live]
b: bool,
#[live]
s: String,
#[rust(7u32)]
r: u32,
}
#[derive(Debug, Script, ScriptHook)]
struct Base {
#[live(2.0)]
x: f64,
#[live]
mode: Mode,
#[live]
inner: Inner,
}
#[derive(Debug, Script, ScriptHook)]
struct Derived {
#[deref]
base: Base,
#[live(3.0)]
y: f32,
#[live]
opt: Option<Splat>,
#[live]
list: Vec<Splat>,
#[live]
id: LiveId,
}
/// Two deref levels: Deep -> Derived -> Base; each level resolves its own fields from the same value.
#[derive(Debug, Script, ScriptHook)]
struct Deep {
#[deref]
d: Derived,
#[live(9.0)]
q: f64,
}
#[derive(Debug, Default, Script, ScriptHook)]
struct Splat {
#[live(4.0)]
w: f64,
#[live]
h: f64,
}
#[derive(Debug, Script, ScriptHook)]
struct WithSplat {
#[walk]
walk: Splat,
#[layout]
layout: Inner,
#[live]
z: f64,
}
#[derive(Debug, Script)]
struct Hooked {
#[live]
a: f64,
#[live]
b: f64,
#[rust]
log: Vec<String>,
}
impl ScriptHook for Hooked {
fn on_custom_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, _scope: &mut Scope, value: ScriptValue) -> bool {
// a value with `custom: true` is handled here: the generated field code must not run
let custom = value.as_object().map(|o| get(vm, o, id!(custom))).is_some_and(|v| v.as_bool() == Some(true));
self.log.push(format!("custom({:?})={}", apply, custom));
if custom {
self.a = -1.0;
}
custom
}
fn on_before_apply(&mut self, _vm: &mut ScriptVm, apply: &Apply, _scope: &mut Scope, _value: ScriptValue) {
self.log.push(format!("before({:?}) a={} b={}", apply, self.a, self.b));
}
fn on_after_apply(&mut self, _vm: &mut ScriptVm, apply: &Apply, _scope: &mut Scope, _value: ScriptValue) {
self.log.push(format!("after({:?}) a={} b={}", apply, self.a, self.b));
}
}
#[derive(Debug, Script, ScriptHook)]
struct State {
#[live(1.0)]
#[apply_state]
kept: f64,
#[live(true)]
#[imperative]
vis: bool,
#[live(5.0)]
plain: f64,
}
/// A field type whose apply_default returns the sub-object named by its `state` key (an
/// animator's shape): the owner re-applies itself with that block under Apply::Default(n+1).
#[derive(Debug, Default)]
struct Anim {
state: LiveId,
applied: Vec<String>,
}
impl ScriptHook for Anim {}
impl ScriptNew for Anim {
fn script_new(_vm: &mut ScriptVm) -> Self {
Self::default()
}
}
impl ScriptApply for Anim {
fn script_apply(&mut self, _vm: &mut ScriptVm, apply: &Apply, _scope: &mut Scope, value: ScriptValue) {
self.applied.push(format!("{:?}", apply));
if let Some(id) = value.as_id() {
self.state = id;
}
}
fn script_to_value(&self, _vm: &mut ScriptVm) -> ScriptValue {
self.state.into()
}
}
impl ScriptApplyDefault for Anim {
fn script_apply_default(&mut self, vm: &mut ScriptVm, apply: &Apply, _scope: &mut Scope, value: ScriptValue) -> Option<ScriptValue> {
if apply.as_default().is_some() || self.state == LiveId(0) {
return None;
}
let o = value.as_object()?;
let block = get(vm, o, self.state);
block.as_object().map(|b| b.into())
}
}
#[derive(Debug, Script, ScriptHook)]
struct Animated {
#[live(10.0)]
height: f64,
#[live(1.0)]
alpha: f64,
#[apply_default]
anim: Anim,
}
fn get(vm: &mut ScriptVm, o: ScriptObject, key: LiveId) -> ScriptValue {
let t = vm.bx.threads.cur().trap.pass();
vm.bx.heap.value(o, key.into(), t)
}
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 obj(vm: &mut ScriptVm, proto: ScriptValue, kv: &[(LiveId, ScriptValue)]) -> ScriptObject {
let o = vm.bx.heap.new_with_proto(proto);
for (k, v) in kv {
let t = vm.bx.threads.cur().trap.pass();
vm.bx.heap.set_value(o, (*k).into(), *v, t);
}
o
}
fn errors(vm: &mut ScriptVm) -> Vec<String> {
vm.bx.threads.cur().trap.err_take().into_iter().map(|e| e.message).collect()
}
fn applies() -> [Apply; 7] {
[Apply::New, Apply::Reload, Apply::Rebake, Apply::ScriptReapply, Apply::Animate, Apply::Eval, Apply::Default(0)]
}
fn transcript() -> String {
let vm = &mut test_vm();
let mut out = String::new();
let derived_api: ScriptValue = Derived::script_api(vm).into();
let inner_api: ScriptValue = Inner::script_api(vm).into();
let splat_api: ScriptValue = WithSplat::script_api(vm).into();
let hooked_api: ScriptValue = Hooked::script_api(vm).into();
let state_api: ScriptValue = State::script_api(vm).into();
let anim_api: ScriptValue = Animated::script_api(vm).into();
let splat_api0: ScriptValue = Splat::script_api(vm).into();
let mode_api0: ScriptValue = Mode::script_api(vm).into();
let s_hello = vm.bx.heap.new_string_from_str("hello");
let s_world = vm.bx.heap.new_string_from_str("world");
// prototype chain: grand (x, y, inner{a}) <- parent (mode, list) <- child (y, opt)
let inner_doc = obj(vm, inner_api, &[(id!(a), 11.0.into()), (id!(s), s_hello)]);
let grand = obj(vm, derived_api, &[(id!(x), 20.0.into()), (id!(y), 30.0.into()), (id!(inner), inner_doc.into())]);
let arr = vm.bx.heap.new_array();
for v in [1.0, 2.0, 3.0] {
let e = obj(vm, splat_api0, &[(id!(w), v.into())]);
let t = vm.bx.threads.cur().trap.pass();
vm.bx.heap.array_push(arr, e.into(), t);
}
let fit = get(vm, mode_api0.as_object().unwrap(), id!(Fit));
let parent = obj(vm, grand.into(), &[(id!(mode), fit), (id!(list), arr.into())]);
let splat_small = obj(vm, splat_api0, &[(id!(h), 0.5.into())]);
let child = obj(vm, parent.into(), &[(id!(y), 31.0.into()), (id!(opt), splat_small.into()), (id!(id), id!(abc).into())]);
// partial / explicit nil documents
let partial = obj(vm, derived_api, &[(id!(y), 7.0.into())]);
let nils = obj(vm, ScriptValue::NIL, &[(id!(x), NIL), (id!(opt), NIL), (id!(y), NIL), (id!(mode), NIL)]);
let inner_nil = obj(vm, ScriptValue::NIL, &[(id!(s), NIL), (id!(b), true.into())]);
let with_inner_nil = obj(vm, ScriptValue::NIL, &[(id!(inner), inner_nil.into())]);
let inner_world = obj(vm, inner_api, &[(id!(s), s_world)]);
let with_inner_world = obj(vm, derived_api, &[(id!(inner), inner_world.into())]);
assert_eq!(errors(vm), Vec::<String>::new(), "building the documents raised errors");
let docs: Vec<(&str, ScriptValue)> = vec![
("api", derived_api),
("grand", grand.into()),
("parent", parent.into()),
("child", child.into()),
("partial", partial.into()),
("nils", nils.into()),
("inner_nil", with_inner_nil.into()),
("inner_world", with_inner_world.into()),
("nil", NIL),
];
for apply in applies() {
for (name, doc) in &docs {
// a non-default prior state: what stays untouched is visible
let mut d = Derived {
base: Base { x: -1.0, mode: Mode::Fixed, inner: Inner { a: -2.0, b: false, s: "prior".into(), r: 99 } },
y: -3.0,
opt: Some(Splat { w: -4.0, h: -4.5 }),
list: vec![Splat { w: -5.0, h: -5.5 }],
id: id!(prior),
};
d.script_apply(vm, &apply, &mut Scope::empty(), *doc);
let _ = writeln!(out, "derived {:?} {name}: {:?}", apply, d);
// the reverse direction: the struct back to a value, keys in insertion order
let v = d.script_to_value(vm);
let mut json = String::new();
vm.bx.heap.to_json_inner(v, &mut json);
let _ = writeln!(out, " to_value: {json}");
}
}
// two deref levels on the same documents
let deep_api: ScriptValue = Deep::script_api(vm).into();
let deep_doc = obj(vm, child.into(), &[]);
let _ = deep_api;
for apply in applies() {
for (name, doc) in [("child", ScriptValue::from(child)), ("deep", deep_doc.into())] {
let mut d = Deep {
d: Derived {
base: Base { x: -1.0, mode: Mode::Fixed, inner: Inner { a: -2.0, b: false, s: "prior".into(), r: 99 } },
y: -3.0,
opt: None,
list: vec![],
id: id!(prior),
},
q: -9.0,
};
d.script_apply(vm, &apply, &mut Scope::empty(), doc);
let _ = writeln!(out, "deep {:?} {name}: {:?}", apply, d);
}
}
// the missing field on reload takes the registered type default
let inner_default = obj(vm, inner_api, &[(id!(a), 42.0.into())]);
assert!(vm.bx.heap.set_type_default(inner_default));
let mode_api: ScriptValue = Mode::script_api(vm).into();
let mode_default = vm.bx.heap.new_with_proto(mode_api);
assert!(vm.bx.heap.set_type_default(mode_default));
for apply in applies() {
let mut d = Derived {
base: Base { x: -1.0, mode: Mode::Fixed, inner: Inner { a: -2.0, b: false, s: "prior".into(), r: 99 } },
y: -3.0,
opt: Some(Splat { w: -4.0, h: -4.5 }),
list: vec![Splat { w: -5.0, h: -5.5 }],
id: id!(prior),
};
d.script_apply(vm, &apply, &mut Scope::empty(), partial.into());
let _ = writeln!(out, "derived-with-defaults {:?} partial: {:?}", apply, d);
if matches!(apply, Apply::Reload) {
assert_eq!(d.base.inner.a, 42.0, "reload default for a missing nested field");
assert_eq!(d.base.inner.r, 99, "rust fields are never applied");
assert_eq!(d.base.mode, Mode::Fill, "reload default for a missing enum field");
assert_eq!(d.y, 7.0);
}
if matches!(apply, Apply::New) {
assert_eq!(d.base.x, -1.0, "a missing field stays untouched");
assert_eq!(d.base.inner.a, -2.0);
}
}
// splat / walk / layout: the same value is applied to the sub-structs
let splat_doc = obj(vm, splat_api, &[(id!(w), 40.0.into()), (id!(a), 41.0.into()), (id!(z), 42.0.into())]);
for apply in applies() {
let mut s = WithSplat { walk: Splat { w: -1.0, h: -2.0 }, layout: Inner { a: -3.0, b: true, s: "p".into(), r: 1 }, z: -4.0 };
s.script_apply(vm, &apply, &mut Scope::empty(), splat_doc.into());
let _ = writeln!(out, "splat {:?}: {:?}", apply, s);
let v = s.script_to_value(vm);
let mut json = String::new();
vm.bx.heap.to_json_inner(v, &mut json);
let _ = writeln!(out, " to_value: {json}");
}
// on_custom_apply short-circuit and hook order
let hooked_plain = obj(vm, hooked_api, &[(id!(a), 1.0.into()), (id!(b), 2.0.into())]);
let hooked_custom = obj(vm, ScriptValue::NIL, &[(id!(a), 1.0.into()), (id!(custom), true.into())]);
for apply in applies() {
for (name, doc) in [("plain", hooked_plain), ("custom", hooked_custom)] {
let mut h = Hooked { a: 0.0, b: 0.0, log: Vec::new() };
h.script_apply(vm, &apply, &mut Scope::empty(), doc.into());
let _ = writeln!(out, "hooked {:?} {name}: {:?}", apply, h);
if name == "custom" {
assert_eq!(h.b, 0.0, "on_custom_apply skips the generated field code");
}
}
}
// apply_state / visible preserve runtime state on reapply
let state_doc = obj(vm, state_api, &[(id!(kept), 2.0.into()), (id!(vis), false.into()), (id!(plain), 6.0.into())]);
for apply in applies() {
let mut s = State { kept: 9.0, vis: true, plain: 9.0 };
s.script_apply(vm, &apply, &mut Scope::empty(), state_doc.into());
let _ = writeln!(out, "state {:?}: {:?}", apply, s);
}
// animator: the state block re-applies a small object to the owner (Apply::Default(n+1))
let hidden = obj(vm, ScriptValue::NIL, &[(id!(height), 0.0.into())]);
let _ = anim_api;
let anim_doc = obj(vm, ScriptValue::NIL, &[(id!(height), 100.0.into()), (id!(anim), id!(hidden).into()), (id!(hidden), hidden.into())]);
let anim_small = obj(vm, ScriptValue::NIL, &[(id!(alpha), 0.25.into())]);
for apply in applies() {
let mut a = Animated { height: -1.0, alpha: -1.0, anim: Anim::default() };
a.script_apply(vm, &apply, &mut Scope::empty(), anim_doc.into());
let _ = writeln!(out, "animated {:?} doc: {:?}", apply, a);
a.script_apply(vm, &Apply::Animate, &mut Scope::empty(), anim_small.into());
let _ = writeln!(out, "animated {:?} then small block: {:?}", apply, a);
}
let _ = writeln!(out, "errors: {:?}", errors(vm));
out
}
#[test]
fn apply_transcript() {
let t = transcript();
if let Ok(path) = std::env::var("APPLY_DIFF_OUT") {
std::fs::write(path, &t).unwrap();
}
// the rules every codegen keeps
assert!(t.contains("derived New partial: Derived { base: Base { x: -1.0, mode: Fixed"), "missing fields untouched:\n{t}");
}