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

328 lines
16 KiB
Rust

//! Regression tests: an `if` branch whose block holds a `for` loop, followed
//! by `else`.
//!
//! Found by the frontier game lane (2026-09-27): `if c { for … { } } else
//! { … }` fails at eval with "pop_stack_value on empty stack": an arm that
//! ends in a loop, a `let`, a `;` or nothing leaves no value while the other
//! arm leaves one. Pushing nil for such arms fixed scripts but broke shader
//! compilation (shader ifs are statements whose arms must stay valueless).
//! Fixed in the parser (2026-09-29): an if in statement position whose arms
//! all leave no value leaves none; otherwise (one arm valued, or expression
//! position) a valueless arm yields nil through IF_ELSE's NEED_NIL or a
//! trailing NIL_ARM, both of which the shader compiler ignores. A match is
//! an expression: its valueless arms always yield nil.
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!("if_for_else_{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(code: &str, name: &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:?}"))
}
#[test]
fn if_branch_ending_in_a_for_loop_then_else() {
let code = "fn f(k) { let n = 0\n if k == 1 { for i in 0..3 { n += 1 } } else { n = 100 }\n return n }\nf(1) * 1000 + f(2)";
assert_eq!(number(code, "ending"), 3100.0);
}
#[test]
fn if_branch_with_a_for_loop_then_more_then_else() {
// The frontier repro: the loop body holds its own if/else.
let code = "fn f(kind) {\n let xs = []\n if kind == \"a\" { for i in 0..3 { if i == 1 { xs.push(i) } else { xs.push(2) } }\n xs.push(9) } else { xs.push(1) }\n return xs\n}\nf(\"a\").len() * 10 + f(\"b\").len()";
assert_eq!(number(code, "frontier"), 41.0);
}
#[test]
fn every_arm_shape_leaves_one_value() {
// An arm ending in `;`, a `let`, nothing, or a loop is worth nil; the
// other arm keeps its value; nothing is leaked or over-popped.
let w = |body: &str| format!("fn f(k) {{ let n = 0\n {body}\n return n }}\nf(1) * 1000 + f(2)");
for (name, body, want) in [
("sep", "if k == 1 { n = 5; } else { n = 50 }", 5050.0),
("sep_else", "if k == 1 { n = 5 } else { n = 50; }", 5050.0),
("empty", "if k == 1 { } else { n = 50 }", 50.0),
("for_noelse", "if k == 1 { for i in 0..4 { n += 1 } }", 4000.0),
("else_for", "if k == 1 { n = 5 } else { for i in 0..4 { n += 1 } }", 5004.0),
("let_last", "if k == 1 { n = 3 let z = 3 } else { n = 50 }", 3050.0),
("nested", "if k == 1 { if n == 0 { for i in 0..2 { n += 1 } } else { n = 9 } } else { for i in 0..3 { n += 2 } }", 2006.0),
] {
assert_eq!(number(&w(body), name), want, "{name}");
}
let value = "fn f(k) { let v = if k == 1 { 3 } else { 4 }\n return v }\nf(1) * 10 + f(2)";
assert_eq!(number(value, "value"), 34.0);
let nil_arm = "fn f(k) { let v = if k == 1 { for i in 0..2 { } } else { 4 }\n if v == nil { return 1 } return v }\nf(1) * 10 + f(2)";
assert_eq!(number(nil_arm, "nil_arm"), 14.0);
}
#[test]
fn top_level_if_for_else() {
let code = "let n = 0\nlet k = 1\nif k == 1 { for i in 0..4 { n += 1 } } else { n = 50 }\nfn get() { return n }\nget()";
assert_eq!(number(code, "toplevel"), 4.0);
}
fn nil_or_number(code: &str, name: &str) -> Option<f64> {
let (value, errs, _vm) = run(name, code);
assert!(errs.is_empty(), "{name}: eval errors: {errs:?}");
if value.is_nil() {
return None;
}
Some(value.as_number().unwrap_or_else(|| panic!("{name}: not a number: {value:?}")))
}
#[test]
fn statement_ifs_without_else() {
let w = |body: &str| format!("fn f(k) {{ let n = 0\n {body}\n return n }}\nf(1) * 1000 + f(2)");
for (name, body, want) in [
("sep_noelse", "if k == 1 { n = 5; }", 5000.0),
("let_noelse", "if k == 1 { let z = 1 n = 2 let y = 3 }", 2000.0),
("empty_noelse", "if k == 1 { }", 0.0),
("ret_noelse", "if k == 1 { return 7; }", 7000.0),
("ret_value_noelse", "if k == 1 { return 7 }", 7000.0),
("two_in_a_row", "if k == 1 { n = 1; } if k == 2 { for i in 0..3 { n += 1 } }", 1003.0),
("expr_arm", "if k == 1 n = 4", 4000.0),
] {
assert_eq!(number(&w(body), name), want, "{name}");
}
}
#[test]
fn else_if_chains_and_nesting() {
let w = |body: &str| format!("fn f(k) {{ let n = 0\n {body}\n return n }}\nf(1) * 10000 + f(2) * 100 + f(3)");
for (name, body, want) in [
("chain_for_mid", "if k == 1 { n = 1 } else if k == 2 { for i in 0..2 { n += 1 } } else { n = 3 }", 10203.0),
("chain_all_valueless", "if k == 1 { n = 1; } else if k == 2 { for i in 0..2 { n += 1 } } else { let z = 0 n = 3; }", 10203.0),
("chain_no_final_else", "if k == 1 { for i in 0..5 { n += 1 } } else if k == 2 { n = 2 }", 50200.0),
("elif_chain", "if k == 1 { n = 1; } elif k == 2 { n = 2 } else { for i in 0..3 { n += 1 } }", 10203.0),
("nested_valueless", "if k != 3 { if k == 1 { for i in 0..4 { n += 1 } } else { n = 2; } } else { n = 3 }", 40203.0),
("nested_in_loop", "for j in 0..2 { if k == 1 { for i in 0..2 { n += 1 } } else if k == 2 { n += 5 } else { n += 7; } }", 41014.0),
] {
assert_eq!(number(&w(body), name), want, "{name}");
}
}
#[test]
fn if_as_the_value_of_a_fn_or_block() {
// The last expression of a fn body is its value: a valueless arm is nil.
let mixed = "fn g(k) { if k == 1 { for i in 0..2 { } } else { 5 } }\ng(";
assert_eq!(nil_or_number(&format!("{mixed}1)"), "last_mixed_1"), None);
assert_eq!(nil_or_number(&format!("{mixed}2)"), "last_mixed_2"), Some(5.0));
let valueless = "fn g(k) { let n = 0 if k == 1 { for i in 0..2 { n += 1 } } else { n = 1; } }\ng(1)";
assert_eq!(nil_or_number(valueless, "last_valueless"), None);
let noelse = "fn g(k) { if k == 1 { 3 } }\nlet a = g(1) let b = g(2) if b == nil { a } else { 0 }";
assert_eq!(nil_or_number(noelse, "last_noelse"), Some(3.0));
// An if as the value of an enclosing arm block.
let inner = "fn g(k) { let v = if k > 0 { if k == 1 { for i in 0..2 { } } else { k } } else { 4 }\n return v }\n";
assert_eq!(nil_or_number(&format!("{inner}g(1)"), "inner_1"), None);
assert_eq!(nil_or_number(&format!("{inner}g(2)"), "inner_2"), Some(2.0));
assert_eq!(nil_or_number(&format!("{inner}g(0)"), "inner_0"), Some(4.0));
// Expression position: every valueless shape is nil.
for (name, expr) in [
("expr_noelse", "if k == 1 { for i in 0..2 { } }"),
("expr_both", "if k == 1 { for i in 0..2 { } } else { let z = 1 }"),
("expr_sep", "if k == 1 { 5; } else { 6; }"),
("expr_chain", "if k == 1 { for i in 0..2 { } } else if k == 2 { } else { 3; }"),
] {
for k in 1..=3 {
let code = format!("fn g(k) {{ let v = {expr}\n return v }}\ng({k})");
assert_eq!(nil_or_number(&code, name), None, "{name} k={k}");
}
}
// A loop as the last statement of a fn body leaves no value.
let tail_loop = "fn g() { let n = 0 for i in 0..3 { n += 1 } }\nlet r = g() if r == nil { 1 } else { 0 }";
assert_eq!(number(tail_loop, "tail_loop"), 1.0);
}
#[test]
fn match_arms_leave_one_value() {
let w = |body: &str| format!("fn f(k) {{ let n = 0\n {body}\n return n }}\nf(1) * 1000 + f(2)");
for (name, body, want) in [
("match_mixed", "match k { 1 => { for i in 0..4 { n += 1 } } _ => { n = 50 } }", 4050.0),
("match_sep", "match k { 1 => { n = 5; } 2 => { n = 50 } }", 5050.0),
("match_no_wildcard", "match k { 1 => { for i in 0..4 { n += 1 } } 3 => 9 }", 4000.0),
("match_all_valueless", "match k { 1 => { n = 1; } _ => { for i in 0..2 { n += 1 } } }", 1002.0),
] {
assert_eq!(number(&w(body), name), want, "{name}");
}
let value = "fn g(k) { let v = match k { 1 => { for i in 0..2 { } } 2 => 5 }\n return v }\n";
assert_eq!(nil_or_number(&format!("{value}g(1)"), "match_value_1"), None);
assert_eq!(nil_or_number(&format!("{value}g(2)"), "match_value_2"), Some(5.0));
assert_eq!(nil_or_number(&format!("{value}g(3)"), "match_value_3"), None);
}
#[test]
fn the_repro_from_the_brief() {
let code = "fn f(k) { let rows = []; if k == 1 { for a in [1,2] { rows.push(a) } } else { rows.push(2) }; return rows }\nf(1).len() * 10 + f(2).len()";
assert_eq!(number(code, "brief"), 21.0);
}
/// A closure passed straight into a call, whose body ends in a statement if
/// holding a loop, is still a function (leap's `game.on_touch(|a, b, side|
/// { … if t == "portal" { for p in portals { … } } })`). The body's
/// valueless-end marker used to survive the removal of the body's slot
/// frame placeholder, land on the code right after the body, and make the
/// fn literal read as a statement that leaves no value: the call got nil,
/// the game's touch handler was never registered and its portals did
/// nothing.
#[test]
fn a_closure_argument_ending_in_a_statement_if_with_a_loop_is_a_function() {
let take = "let seen = []\nfn take(f) { seen.push(f) }\nfn call(f) { return f(3) }\nfn keep(f) { return f }\n";
for (name, closure) in [
("if_for", "|a| { if a > 0 { for s in [1, 2] { let k = s } } }"),
("if_for_multiline", "|a| {\n if a > 0 {\n for s in [1, 2] {\n let k = s\n }\n }\n}"),
("if_for_if", "|a| { let t = 1\n if t == 1 { for s in [1] { if s == a { t = 2 } } } }"),
("nested_closure", "|a| { if a > 0 { for s in [1] { keep(|| { let q = s }) } } }"),
("if_else_for", "|a| { if a > 0 { for s in [1] { let k = s } } else { for s in [2] { let k = s } } }"),
] {
let code = format!("{take}take({closure})\nlet f = seen[0]\ncall(f)\nseen.len()");
assert_eq!(number(&code, name), 1.0, "{name}: the closure argument arrived");
let (value, errs, _vm) = run(name, &format!("{take}call({closure})\n7"));
assert!(errs.is_empty(), "{name}: calling the closure argument failed: {errs:?}");
assert_eq!(value.as_number(), Some(7.0), "{name}");
// Two arguments, the closure last, as `game.after(secs, || { … })`.
let two = format!("{take}fn later(n, f) {{ return f(n) }}\nlater(3, {closure})\n9");
assert_eq!(number(&two, name), 9.0, "{name}: second argument");
}
}
mod shader_ifs {
//! Shader ifs are statements: their arms stay valueless whatever the
//! parser does for script ifs. Every if/match shape compiles on every
//! backend. SHADER_IFS_DUMP=<dir> writes the generated sources: against
//! the parser before the fix they were identical (bar a match temp's
//! position-derived name), and it could not compile `tail_if` at all.
use makepad_script::makepad_math::*;
use makepad_script::traits::*;
use makepad_script::*;
#[derive(Script, ScriptHook)]
#[repr(C)]
pub struct ShaderIfsTest {
#[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)
}
sep_noelse: fn(x: f32) -> f32 {
var r = 0.0
if x > 0.5 { r = 1.0; }
return r
}
value_noelse: fn(x: f32) -> f32 {
var r = 0.0
if x > 0.5 { r = 1.0 }
return r
}
for_else: fn(x: f32) -> f32 {
var r = 0.0
if x > 0.5 { for i in 0..3 { r += 1.0 } } else { r = 5.0 }
return r
}
else_for: fn(x: f32) -> f32 {
var r = 0.0
if x > 0.5 { r = 5.0 } else { for i in 0..3 { r += 1.0 } }
return r
}
let_arms: fn(x: f32) -> f32 {
var r = 0.0
if x > 0.5 { let y = x r = y } else { let z = x * 2.0 r = z; }
return r
}
chain: fn(x: f32) -> f32 {
var r = 0.0
if x > 0.8 { r = 3.0; } else if x > 0.5 { for i in 0..2 { r += 1.0 } } else if x > 0.2 { r = 1.0 } else { let q = 1.0 }
return r
}
nested: fn(x: f32, y: f32) -> f32 {
var r = 0.0
if x > 0.5 { if y > 0.5 { r = 1.0; } else { for i in 0..2 { r += 0.5 } } } else { r = 2.0 }
return r
}
ret_sep: fn(x: f32) -> f32 {
if x > 0.5 { return 1.0; }
if x > 0.2 { return 0.5; } else { return 0.25; }
}
ret_mixed: fn(x: f32) -> f32 {
var r = 0.0
if x > 0.5 { return 1.0 } else { r = 2.0; }
return r
}
select: fn(x: f32) -> f32 {
let v = if x > 0.5 { 1.0 } else { 2.0 }
return v
}
matched: fn(x: f32) -> f32 {
var r = 0.0
let k = i32(x * 4.0)
match k {
0 => { r = 1.0; }
1 => { for i in 0..2 { r += 1.0 } }
_ => { r = 3.0 }
}
return r
}
tail_if: fn(x: f32) {
if x > 0.5 { self.fb0 = vec4(1.0, 0.0, 0.0, 1.0); } else { self.fb0 = vec4(0.0); }
}
fragment: fn() {
let x = self.tint.x
let y = self.tint.y
let a = self.sep_noelse(x) + self.value_noelse(x) + self.for_else(x) + self.else_for(x)
let b = self.let_arms(x) + self.chain(x) + self.nested(x, y) + self.ret_sep(x)
let c = self.ret_mixed(x) + self.select(x) + self.matched(x)
self.tail_if(x)
self.fb0 = vec4(a, b, c, 1.0)
}
}
"#;
#[test]
fn shader_ifs_compile_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()),
};
let dump = std::env::var("SHADER_IFS_DUMP").ok();
for backend in ["metal", "hlsl", "glsl", "wgsl"] {
let shader_obj = ShaderIfsTest::script_shader(&mut vm);
let value = vm.eval(ScriptMod::new(env!("CARGO_MANIFEST_DIR"), "shader_ifs", "shader_ifs.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("nil"), "{backend}: a nil reached the shader:\n{source}");
if let Some(dir) = &dump {
std::fs::write(format!("{dir}/{backend}.txt"), &source).unwrap();
}
}
}
}