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>
205 lines
9.4 KiB
Rust
205 lines
9.4 KiB
Rust
//! Regression tests for the language gaps found while authoring the
|
|
//! reference-film plates (FP1, 2026-09-30).
|
|
|
|
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 run(name: &str, code: &str) -> (ScriptValue, Vec<String>, ScriptVm<'static>) {
|
|
let mut vm = test_vm();
|
|
vm.bx.captured_errors = Some(Vec::new());
|
|
let value = vm.with_instruction_limit(500_000, |vm| {
|
|
vm.eval(ScriptMod::new("", "", &format!("fp1_{name}"), 0, 0, &code, &[]))
|
|
});
|
|
let mut errs: Vec<String> = vm.bx.captured_errors.take().unwrap().into_iter().map(|e| format!("{e:?}")).collect();
|
|
errs.extend(vm.take_errors().into_iter().map(|e| format!("{e:?}")));
|
|
(value, errs, vm)
|
|
}
|
|
|
|
fn number(name: &str, code: &str) -> f64 {
|
|
let (value, errs, _vm) = run(name, code);
|
|
assert!(errs.is_empty(), "{name}: eval errors: {errs:?}");
|
|
value.as_number().unwrap_or_else(|| panic!("{name}: not a number: {value:?}"))
|
|
}
|
|
|
|
fn fails(name: &str, code: &str) -> Vec<String> {
|
|
let (_value, errs, _vm) = run(name, code);
|
|
assert!(!errs.is_empty(), "{name}: expected an error");
|
|
errs
|
|
}
|
|
|
|
const PRELUDE: &str = "use mod.math.*\nuse mod.pod.*\n";
|
|
|
|
/// A binary op folds a constant right operand into its immediate. The fold
|
|
/// took the last constant of an if/match arm or a `||` operand too, and
|
|
/// popping it moved their jump targets: `var z = 1.0 * (if v { 0.2 } else
|
|
/// { 1.0 })` skipped the declaration when the true arm ran.
|
|
#[test]
|
|
fn an_op_after_an_if_ending_in_a_whole_float() {
|
|
let f = |expr: &str, arg: &str| format!("let v = 2\nlet f = fn(c) {{\n var z = {expr}\n return z\n}}\nf({arg})");
|
|
for (name, expr, t, e) in [
|
|
("fp1", "1.0 * (if v == 2 { 0.2 } else { 1.0 })", 0.2, 0.2),
|
|
("param", "1.0 * (if c { 0.2 } else { 1.0 })", 0.2, 1.0),
|
|
("mul", "3 * (if c { 0.5 } else { 2.0 })", 1.5, 6.0),
|
|
("add", "1 + (if c { 2 } else { 3.0 })", 3.0, 4.0),
|
|
("sub", "10 - (if c { 2.5 } else { 4.0 })", 7.5, 6.0),
|
|
("nested", "2 * (1 + (if c { 1 } else { 3.0 }))", 4.0, 8.0),
|
|
("elif", "2 * (if c { 1 } elif v == 3 { 5 } else { 3.0 })", 2.0, 6.0),
|
|
("match", "2 * (match c { true => 1.5 _ => 3.0 })", 3.0, 6.0),
|
|
("or", "2 * (c || 3.0)", 2.0, 6.0),
|
|
("nil_or", "2 * ((if c { 1.5 } else { nil }) |? 3.0)", 3.0, 6.0),
|
|
("cam", "1.0 * (1.0 + 0.5 * 2.0 * (if c { 0.2 } else { 1.0 }))", 1.2, 2.0),
|
|
] {
|
|
assert_eq!(number(name, &f(expr, "true")), t, "{name} (true)");
|
|
assert_eq!(number(name, &f(expr, "false")), e, "{name} (false)");
|
|
let with_let = f(expr, "true").replace("var z", "let z");
|
|
assert_eq!(number(name, &with_let), t, "{name} (let)");
|
|
}
|
|
// Constants still fold where they are the whole operand.
|
|
assert_eq!(number("plain", "let a = 7\na * 2.0 + a - 3.0"), 18.0);
|
|
}
|
|
|
|
/// `int(x)` (and `i32`, `u32`, `f32`, `f16`) make a scalar pod: it indexes
|
|
/// arrays and does arithmetic as the number it holds.
|
|
#[test]
|
|
fn int_values_index_arrays() {
|
|
let a = format!("{PRELUDE}let a = [5, 6, 7]\n");
|
|
assert_eq!(number("int_lit", &format!("{a}a[int(1.7)]")), 6.0);
|
|
assert_eq!(number("int_var", &format!("{a}let i = int(2)\na[i]")), 7.0);
|
|
assert_eq!(number("i32", &format!("{a}a[i32(0)]")), 5.0);
|
|
assert_eq!(number("u32", &format!("{a}a[u32(2)]")), 7.0);
|
|
assert_eq!(number("f32", &format!("{a}a[f32(1.0)]")), 6.0);
|
|
assert_eq!(number("sum", &format!("{a}a[int(0.9) + 1]")), 6.0);
|
|
assert_eq!(number("arith", &format!("{PRELUDE}int(2.9) * 1.5 + u32(1)")), 4.0);
|
|
assert_eq!(number("cmp", &format!("{PRELUDE}if int(2.5) < 3 {{ 1 }} else {{ 0 }}")), 1.0);
|
|
assert_eq!(number("loop", &format!("{PRELUDE}let a = [1, 2, 3]\nvar s = 0\nfor k in 0..3 {{ s += a[int(k)] }}\ns")), 6.0);
|
|
assert_eq!(number("assign", &format!("{a}a[int(1)] = 9\na[1]")), 9.0);
|
|
// Still only whole, non-negative, in-range indices.
|
|
fails("negative", &format!("{a}a[int(-1)]"));
|
|
fails("fraction", &format!("{a}a[f32(0.5)]"));
|
|
// `float(1.0)` stays a pod, which shader declarations take their type from.
|
|
let (value, errs, _vm) = run("pod", &format!("{PRELUDE}float(1.0)"));
|
|
assert!(errs.is_empty() && value.as_pod().is_some(), "{value:?} {errs:?}");
|
|
}
|
|
|
|
/// A call of `mod` is the modulo of shading languages; `mod` itself stays
|
|
/// the module root.
|
|
#[test]
|
|
fn mod_call_is_modulo_and_mod_stays_the_modules() {
|
|
assert_eq!(number("mod", "mod(7, 3)"), 1.0);
|
|
assert_eq!(number("mod_neg", "mod(-1, 3)"), 2.0);
|
|
assert_eq!(number("mod_frac", "mod(5.5, 2)"), 1.5);
|
|
assert_eq!(number("mod_math", &format!("{PRELUDE}mod(7.5, 2.0) + floor(0.5)")), 1.5);
|
|
assert_eq!(number("mod_in_fn", &format!("{PRELUDE}let f = fn(t) {{ mod(t, 2.0) }}\nf(5.5)")), 1.5);
|
|
assert_eq!(number("mod_vec", &format!("{PRELUDE}let v = mod(vec2(5, -1), vec2(3, 3))\nv.x * 10 + v.y")), 22.0);
|
|
// mod.x and use mod.x.* mean what they did.
|
|
assert_eq!(number("mod_field", "let m = mod.math\nm.floor(2.5)"), 2.0);
|
|
assert_eq!(number("mod_path", "mod.math.floor(3.5)"), 3.0);
|
|
assert_eq!(number("mod_use", "use mod.math.*\nfloor(4.5)"), 4.0);
|
|
assert_eq!(number("modf", &format!("{PRELUDE}modf(-1, 3)")), -1.0);
|
|
// A local named mod is that local.
|
|
assert_eq!(number("mod_shadow", "let mod = fn(a, b) { a + b }\nmod(1, 2)"), 3.0);
|
|
}
|
|
|
|
/// `a.b ?? d`: an optional field read (and `a[i] ?? d`, `a[i].b ?? d`). `??` was a parse error before
|
|
/// ("Parser stuck on character Operator(?)") in every spelling, so no
|
|
/// program that parsed changes meaning. It is `|?` (nil-or) whose left
|
|
/// operand's trailing field reads are quiet: a missing field, or a field of
|
|
/// nil, is nil without an error.
|
|
#[test]
|
|
fn optional_field_read() {
|
|
let o = "let o = {a: 1 n: nil f: false s: {c: 2}}\n";
|
|
for (name, expr, want) in [
|
|
("missing", "o.b ?? 7", 7.0),
|
|
("present", "o.a ?? 7", 1.0),
|
|
("nil_field", "o.n ?? 5", 5.0),
|
|
("chain_missing", "o.x.c ?? 9", 9.0),
|
|
("chain_present", "o.s.c ?? 9", 2.0),
|
|
("chain_tail_missing", "o.s.d ?? 9", 9.0),
|
|
("then", "o.b ?? o.x ?? 8", 8.0),
|
|
("precedence", "o.b ?? 3 + 4", 7.0),
|
|
("in_fn", "let f = fn(x) { x.b ?? x.a }\nf(o)", 1.0),
|
|
("plain", "let a = 3\na ?? 4", 3.0),
|
|
("pod", "use mod.pod.*\nlet v = vec2(3, 4)\nv.y ?? 0", 4.0),
|
|
("nospace", "o.b??6", 6.0),
|
|
// The index read before the trailing fields is optional too.
|
|
("index_out", "let a = [1, 2]\na[5] ?? 7", 7.0),
|
|
("index_in", "let a = [1, 2]\na[1] ?? 7", 2.0),
|
|
("index_field", "let w = [{s: [{start: 1.5}]}]\nw[0].s[1].start ?? 9", 9.0),
|
|
("index_field_in", "let w = [{s: [{start: 1.5}]}]\nw[0].s[0].start ?? 9", 1.5),
|
|
("index_nil", "let a = nil\na[0] ?? 4", 4.0),
|
|
("index_map", "let m = {k: 3}\nm[@x] ?? 5", 5.0),
|
|
] {
|
|
assert_eq!(number(name, &format!("{o}{expr}")), want, "{name}");
|
|
}
|
|
// false is a value, not absent.
|
|
let (value, errs, _vm) = run("false", &format!("{o}o.f ?? 5"));
|
|
assert!(errs.is_empty() && value.as_bool() == Some(false), "{value:?} {errs:?}");
|
|
// Without `??` a missing field is still an error.
|
|
fails("plain_missing", &format!("{o}o.b"));
|
|
fails("nil_or_missing", &format!("{o}o.b |? 7"));
|
|
fails("plain_index", "let a = [1]\na[3]");
|
|
}
|
|
|
|
/// `ln(x)` is `log(x)`, the natural log, in script code; in shaders it
|
|
/// compiles to each backend's `log` (tests/if_for_else.rs's harness).
|
|
#[test]
|
|
fn ln_is_the_natural_log() {
|
|
assert!((number("ln", &format!("{PRELUDE}ln(2.718281828)")) - 1.0).abs() < 1e-5);
|
|
assert!((number("log", &format!("{PRELUDE}log(2.718281828)")) - 1.0).abs() < 1e-5);
|
|
assert!((number("ln_vec", &format!("{PRELUDE}ln(vec2(1.0, 2.718281828)).y")) - 1.0).abs() < 1e-5);
|
|
}
|
|
|
|
mod shader_ln {
|
|
use makepad_script::makepad_math::*;
|
|
use makepad_script::traits::*;
|
|
use makepad_script::*;
|
|
|
|
#[derive(Script, ScriptHook)]
|
|
#[repr(C)]
|
|
pub struct ShaderLnTest {
|
|
#[live]
|
|
pub tint: Vec4f,
|
|
}
|
|
|
|
const SHADER: &str = r#"
|
|
use mod.shader
|
|
use mod.pod.*
|
|
use mod.math.*
|
|
|
|
let sh = #(0){
|
|
vertex_pos: shader.vertex_position(vec4f)
|
|
fb0: shader.fragment_output(0, vec4f)
|
|
vertex: fn() {
|
|
return vec4(0.0, 0.0, 0.0, 1.0)
|
|
}
|
|
fragment: fn() {
|
|
let x = max(self.tint.x, 0.5)
|
|
self.fb0 = vec4(ln(x), log(x), ln(vec2(x, x)).y, 1.0)
|
|
}
|
|
}
|
|
"#;
|
|
|
|
#[test]
|
|
fn ln_compiles_to_log_on_every_backend() {
|
|
let host = Box::leak(Box::new(ScriptVmHost::new(0i32, ())));
|
|
let mut vm = ScriptVm {
|
|
host: host.host_context(),
|
|
bx: Box::new(ScriptVmBase::new()),
|
|
};
|
|
for backend in ["metal", "hlsl", "glsl", "wgsl"] {
|
|
let shader_obj = ShaderLnTest::script_shader(&mut vm);
|
|
let value = vm.eval(ScriptMod::new(env!("CARGO_MANIFEST_DIR"), "shader_ln", "shader_ln.rs", 1, 1, &format!("{SHADER}\n shader.test_compile_draw_source(sh, \"{backend}\", false)"), &[shader_obj]));
|
|
assert!(!value.is_err(), "{backend}: script errored: {value:?}");
|
|
let source = vm.bx.heap.string_with(value, |_heap, s| s.to_string()).unwrap_or_default();
|
|
assert!(!source.is_empty() && !source.starts_with("ERRORS"), "{backend}: {source}");
|
|
assert!(!source.contains("ln("), "{backend}: ln reached the backend:\n{source}");
|
|
}
|
|
}
|
|
}
|