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>
200 lines
6.3 KiB
Rust
200 lines
6.3 KiB
Rust
//! Regression tests for object map keys as seen from script: `delete(@key)`
|
|
//! removes the field and returns the removed value, `for k, v in obj` keys
|
|
//! compare equal to the `@name` id literal (also after a round trip through
|
|
//! an array), and a loop that ends a module's source runs at top level as it
|
|
//! does inside a `fn`.
|
|
|
|
use makepad_script::*;
|
|
|
|
fn test_vm() -> ScriptVm<'static> {
|
|
let host = Box::leak(Box::new(ScriptVmHost::new((), ())));
|
|
let mut vm = ScriptVm {
|
|
host: host.host_context(),
|
|
bx: Box::new(ScriptVmBase::new()),
|
|
};
|
|
vm.bx.captured_errors = Some(Vec::new());
|
|
vm
|
|
}
|
|
|
|
fn eval(vm: &mut ScriptVm, name: &str, code: &str) -> ScriptValue {
|
|
vm.with_instruction_limit(500_000, |vm| {
|
|
vm.eval(ScriptMod::new("", "", &format!("object_keys_{name}.octoscript"), 0, 0, &format!("{code}\n;"), &[]))
|
|
})
|
|
}
|
|
|
|
fn number(vm: &mut ScriptVm, name: &str, code: &str) -> f64 {
|
|
let value = eval(vm, name, code);
|
|
let errors = vm.take_errors();
|
|
assert!(errors.is_empty(), "{name}: {errors:?}");
|
|
value
|
|
.as_number()
|
|
.unwrap_or_else(|| panic!("{name}: expected a number, got {value:?}"))
|
|
}
|
|
|
|
fn boolean(vm: &mut ScriptVm, name: &str, code: &str) -> bool {
|
|
let value = eval(vm, name, code);
|
|
let errors = vm.take_errors();
|
|
assert!(errors.is_empty(), "{name}: {errors:?}");
|
|
value
|
|
.as_bool()
|
|
.unwrap_or_else(|| panic!("{name}: expected a bool, got {value:?}"))
|
|
}
|
|
|
|
#[test]
|
|
fn delete_id_literal_removes_the_field_and_returns_its_value() {
|
|
let vm = &mut test_vm();
|
|
assert_eq!(
|
|
number(vm, "delete_ret", "let o = {a: 1 b: 2}\no.delete(@a)"),
|
|
1.0
|
|
);
|
|
assert_eq!(
|
|
number(vm, "delete_len", "let o = {a: 1 b: 2}\no.delete(@a)\no.map_len()"),
|
|
1.0
|
|
);
|
|
assert!(boolean(
|
|
vm,
|
|
"delete_rest",
|
|
"let o = {a: 1 b: 2}\no.delete(@a)\no.b == 2 && o.map_len() == 1"
|
|
));
|
|
// Deleting a key held in a variable (as `for k in obj` hands it out).
|
|
assert_eq!(
|
|
number(
|
|
vm,
|
|
"delete_var",
|
|
"let o = {a: 1 b: 2}\nlet k = @b\no.delete(k)\no.map_len()"
|
|
),
|
|
1.0
|
|
);
|
|
// A missing key removes nothing and returns nil.
|
|
assert!(boolean(
|
|
vm,
|
|
"delete_missing",
|
|
"let o = {a: 1}\no.delete(@zz) == nil && o.map_len() == 1"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn delete_of_iteration_key_removes_the_field() {
|
|
let vm = &mut test_vm();
|
|
assert_eq!(
|
|
number(
|
|
vm,
|
|
"delete_iter",
|
|
"fn f(o){ let ks = []\n for k, v in o { ks.push(k) }\n for k in ks { o.delete(k) }\n o.map_len() }\nf({a: 1 b: 2 c: 3})"
|
|
),
|
|
0.0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn iteration_keys_equal_id_literals() {
|
|
let vm = &mut test_vm();
|
|
assert_eq!(
|
|
number(
|
|
vm,
|
|
"iter_eq_fn",
|
|
"fn f(o){ let n = 0\n for k, v in o { if k == @name { n = v } }\n n }\nf({other: 1 name: 7})"
|
|
),
|
|
7.0
|
|
);
|
|
assert!(boolean(
|
|
vm,
|
|
"iter_ne_fn",
|
|
"fn f(o){ let hit = false\n for k, v in o { if k != @name { hit = true } }\n hit }\nf({name: 7})"
|
|
) == false);
|
|
assert!(boolean(
|
|
vm,
|
|
"iter_str",
|
|
"fn f(o){ let same = false\n for k, v in o { same = k.to_string() == \"name\" }\n same }\nf({name: 7})"
|
|
));
|
|
}
|
|
|
|
/// Evaluates `code` as given, without the `\n;` terminator the
|
|
/// `script_mod!` path appends: a host such as FlowVm evaluates raw source,
|
|
/// so the source ends directly on its last statement.
|
|
fn eval_raw(vm: &mut ScriptVm, name: &str, code: &str) -> (ScriptValue, Vec<String>) {
|
|
let value = vm.with_instruction_limit(500_000, |vm| {
|
|
vm.eval(ScriptMod::new("", "", &format!("object_keys_{name}.octoscript"), 0, 0, &code, &[]))
|
|
});
|
|
(value, vm.take_errors())
|
|
}
|
|
|
|
/// A loop that ends the source leaves no value: auto-close must not pop it
|
|
/// as the module's result ("pop_stack_resolved on empty stack").
|
|
#[test]
|
|
fn loop_as_last_statement_at_top_level() {
|
|
let vm = &mut test_vm();
|
|
for (name, code) in [
|
|
("for_map", "let o = {a: 1 b: 2}\nlet r = []\nfor k, v in o { r.push([k, v]) }"),
|
|
("for_map_nl", "let o = {a: 1 b: 2}\nlet r = []\nfor k, v in o { r.push([k, v]) }\n"),
|
|
("for_range", "let r = []\nfor i in 0..2 { r.push(i) }"),
|
|
("for_assign", "let r = 0\nfor i in 0..2 { r = i }"),
|
|
("while", "let r = []\nlet i = 0\nwhile i < 2 { r.push(i) i += 1 }"),
|
|
("loop", "let r = []\nloop { r.push(1) break }"),
|
|
] {
|
|
let (value, errors) = eval_raw(vm, name, code);
|
|
assert!(errors.is_empty(), "{name}: {errors:?}");
|
|
assert!(value.is_nil(), "{name}: expected nil, got {value:?}");
|
|
}
|
|
// A value statement after the loop is still the module's result.
|
|
let (value, errors) = eval_raw(
|
|
vm,
|
|
"for_then_value",
|
|
"let o = {a: 1 b: 2}\nlet r = []\nfor k, v in o { r.push([k, v]) }\nr.len()",
|
|
);
|
|
assert!(errors.is_empty(), "for_then_value: {errors:?}");
|
|
assert_eq!(value.as_number(), Some(2.0));
|
|
}
|
|
|
|
#[test]
|
|
fn map_for_loop_building_arrays_matches_fn_body() {
|
|
let vm = &mut test_vm();
|
|
assert_eq!(
|
|
number(
|
|
vm,
|
|
"top_level_push",
|
|
"let o = {a: 1 b: 2}\nlet r = []\nfor k, v in o { r.push([k, v]) }\nr.len()"
|
|
),
|
|
2.0
|
|
);
|
|
assert_eq!(
|
|
number(
|
|
vm,
|
|
"top_level_eq",
|
|
"let o = {a: 1 name: 5}\nlet n = 0\nfor k, v in o { if k == @name { n = v } }\nn"
|
|
),
|
|
5.0
|
|
);
|
|
assert_eq!(
|
|
number(
|
|
vm,
|
|
"fn_push",
|
|
"fn f(o){ let r = []\n for k, v in o { r.push([k, v]) }\n r.len() }\nf({a: 1 b: 2})"
|
|
),
|
|
2.0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn id_literal_index_reads_and_writes_the_plain_key() {
|
|
let vm = &mut test_vm();
|
|
assert_eq!(number(vm, "index_read", "let o = {a: 1 b: 2}\no[@b]"), 2.0);
|
|
assert_eq!(
|
|
number(vm, "index_write", "let o = {a: 1}\no[@a] = 5\no.a + o.map_len()"),
|
|
6.0
|
|
);
|
|
// Keys pushed into an array keep their identity when read back.
|
|
assert!(boolean(
|
|
vm,
|
|
"key_in_array",
|
|
"let o = {a: 1 b: 2}\nlet ks = []\nfor k, v in o { ks.push(k) }\nks[1] == @b"
|
|
));
|
|
assert_eq!(
|
|
number(
|
|
vm,
|
|
"key_lookup",
|
|
"let o = {a: 1 b: 2}\nlet n = 0\nfor k, v in o { n += o[k] }\nn"
|
|
),
|
|
3.0
|
|
);
|
|
}
|