#[path = "../common/mod.rs"]
mod common;
use common::*;
struct BoundRuntimeCase<'a> {
case: RuntimeCase<'a>,
bindings: Vec<HostBindingCase<'a>>,
}
fn run_bound_runtime_cases(cases: Vec<BoundRuntimeCase<'_>>) {
for case in &cases {
run_runtime_case_with_bindings(&case.case, &case.bindings);
}
}
fn local_visible_at_current_line(vm: &Vm, name: &str) -> bool {
let info = vm
.debug_info()
.expect("compiled program should include debug info");
let local = info
.locals
.iter()
.find(|local| local.name == name)
.unwrap_or_else(|| panic!("expected local '{name}' in debug info"));
let Some(line) = info.line_for_offset(vm.ip()) else {
return true;
};
if let Some(declared_line) = local.declared_line
&& line < declared_line
{
return false;
}
if let Some(last_line) = local.last_line
&& line > last_line
{
return false;
}
true
}
fn assert_builtin_namespace_stays_builtin(
temp_name: &str,
source: &str,
import_prefix: &str,
expected_stack: &[Value],
) {
let unique = format!(
"{temp_name}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let main_path = root.join("main.rss");
std::fs::write(&main_path, source).expect("main source should write");
let compiled = compile_source_file(main_path.as_path()).expect("compile should succeed");
assert!(
compiled
.program
.imports
.iter()
.all(|import| !import.name.starts_with(import_prefix)),
"{import_prefix} namespace calls should lower as builtins, not host imports"
);
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), expected_stack);
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn rustscript_host_import_runtime_cases_work() {
let cases = vec![
BoundRuntimeCase {
case: rustscript_runtime_case(
"runtime namespace host calls are supported",
r#"
use runtime;
runtime::sleep(41);
"#,
vec![Value::Int(42)],
),
bindings: vec![HostBindingCase {
name: "runtime::sleep",
factory: make_add_one,
}],
},
BoundRuntimeCase {
case: rustscript_runtime_case(
"http subnamespace host calls are supported",
r#"
use http;
http::request::get_header("x-client-id");
"#,
vec![Value::string("x-client-id")],
),
bindings: vec![HostBindingCase {
name: "http::request::get_header",
factory: make_echo_string,
}],
},
BoundRuntimeCase {
case: rustscript_runtime_case(
"host namespace import is supported",
r#"
use runtime;
runtime::sleep(41);
"#,
vec![Value::Int(42)],
),
bindings: vec![HostBindingCase {
name: "runtime::sleep",
factory: make_add_one,
}],
},
BoundRuntimeCase {
case: rustscript_runtime_case(
"host namespace alias import is supported",
r#"
use rate_limit as rl;
rl::allow("client-1", 3, 60);
"#,
vec![Value::Bool(true)],
),
bindings: vec![HostBindingCase {
name: "rate_limit::allow",
factory: make_always_allow,
}],
},
BoundRuntimeCase {
case: rustscript_runtime_case(
"named host imports are supported",
r#"
use runtime as rt;
rt::sleep(41);
"#,
vec![Value::Int(42)],
),
bindings: vec![HostBindingCase {
name: "runtime::sleep",
factory: make_add_one,
}],
},
];
run_bound_runtime_cases(cases);
}
#[test]
fn rustscript_io_namespace_builtin_calls_are_supported() {
let source = r#"
use io;
io::exists(".");
"#;
let compiled = compile_source(source).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
loop {
let status = vm.run().expect("vm should run");
match status {
VmStatus::Halted => break,
VmStatus::Yielded => continue,
VmStatus::Waiting(_op_id) => vm
.wait_for_host_op_blocking()
.expect("vm should complete builtin async op"),
}
}
assert_eq!(vm.stack(), &[Value::Bool(true)]);
}
#[test]
fn rustscript_builtin_and_namespace_runtime_cases_work() {
let cases = vec![
RuntimeCase {
name: "re namespace supports inline regex flags across functions",
source: r#"
use re;
let a = re::match("(?i)^foo$", "FoO");
let b = re::find("(?i)^foo", "FoO bar");
let c = re::replace("(?i)foo", "FoO bar", "x");
let d = re::split("(?i)x", "aXb");
let e = re::captures("(?i)^(foo)-([0-9]+)$", "FoO-42");
let mut score = 0;
if a {
score = score + 1;
}
if b.unwrap_or("") == "FoO" {
score = score + 1;
}
if c == "x bar" {
score = score + 1;
}
if d.length == 2 && d[0] == "a" && d[1] == "b" {
score = score + 1;
}
if e.length == 3 && e[1] == "FoO" && e[2] == "42" {
score = score + 1;
}
score;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(5)],
expected_locals: None,
},
RuntimeCase {
name: "json encode decode builtins are supported",
source: r#"
use json;
struct Inner { name: string }
struct Payload {
answer: int,
ok: bool,
arr: [int],
inner: Inner,
}
let payload = {
answer: 42,
ok: true,
arr: [1, 2],
inner: { name: "pd" },
};
let text = json::encode(payload);
let decoded = json::decode::<Payload>(text);
decoded.answer + decoded.arr[1];
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(44)],
expected_locals: None,
},
RuntimeCase {
name: "jit namespace builtins can configure and read jit",
source: r#"
use jit;
let _set = jit::set_hot_loop_threshold(3);
let after = jit::get_hot_loop_threshold();
let cfg = jit::get_config();
if after == 3 && cfg.hot_loop_threshold == 3 {
1;
} else {
0;
}
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(1)],
expected_locals: None,
},
RuntimeCase {
name: "plus equal is supported for numeric locals",
source: r#"
let mut total = 1;
total += 2;
total += 3;
total;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(6)],
expected_locals: None,
},
RuntimeCase {
name: "math namespace builtins provide numeric helpers",
source: r#"
use math;
let root = math::sqrt(81);
let angle = math::to_radians(180);
let diff = math::abs(angle - math::pi());
let growth = math::powi(2, 5);
if math::is_finite(root) && diff < 0.0000001 && growth == 32.0 {
1;
} else {
0;
}
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(1)],
expected_locals: None,
},
];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_plus_equal_rejects_non_numeric_values() {
let case = SourceErrorCase {
name: "plus equal rejects strings",
source: r#"
let mut value = "a";
value += "b";
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::BinaryOperandTypeMismatch),
expected_contains_all: &["'+=' assignment requires a numeric local"],
};
expect_source_error_case(&case);
}
#[test]
fn rustscript_builtin_and_namespace_parse_rejection_cases_work() {
let cases = vec![rustscript_parse_error_case(
"builtin namespace calls require use import",
r#"
json::encode("ok");
"#,
&["import builtin namespaces first"],
)];
for case in &cases {
expect_parse_error_case(case);
}
}
#[test]
fn compile_source_file_preserves_jit_builtin_namespace_use_directive() {
assert_builtin_namespace_stays_builtin(
"vm_rustscript_jit_builtin_namespace_test",
r#"
use jit;
let _set = jit::set_hot_loop_threshold(2);
let out = jit::get_hot_loop_threshold();
out;
"#,
"jit::",
&[Value::Int(2)],
);
}
#[test]
fn compile_source_file_preserves_math_builtin_namespace_use_directive() {
assert_builtin_namespace_stays_builtin(
"vm_rustscript_math_builtin_namespace_test",
r#"
use math;
let out = math::sqrt(81);
out;
"#,
"math::",
&[Value::Float(9.0)],
);
}
#[test]
fn rustscript_literal_and_slice_runtime_cases_work() {
let cases = vec![
RuntimeCase {
name: "float literal binding is supported",
source: r#"
let a=1.1;
a;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Float(1.1)],
expected_locals: None,
},
RuntimeCase {
name: "char and hex escape literals are supported",
source: r#"
let c = '\x41';
let s = "\x42";
c;
s;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("A"), Value::string("B")],
expected_locals: None,
},
RuntimeCase {
name: "array primitives are supported without namespace",
source: r#"
let mut values = [];
values[values.length] = 7;
values[0] + values.length;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(8)],
expected_locals: None,
},
RuntimeCase {
name: "bracket slice syntax is supported",
source: r#"
let text = "abcdef";
let end = -2;
let a = text.copy()[1:4];
let b = text.copy()[:3];
let c = text.copy()[2:];
let d = text.copy()[:-1];
let e = text.copy()[1:end];
let arr = [1, 2, 3, 4, 5];
let f = arr.copy()[1:4];
let g = arr.copy()[:2];
let h = arr.copy()[3:];
let i = arr.copy()[:-2];
a.length + b.length + c.length + d.length + e.length + f.length + g.length + h.length + i.length;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(28)],
expected_locals: None,
},
];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_print_runtime_cases_work() {
let cases = vec![
BoundRuntimeCase {
case: rustscript_runtime_case(
"print builtin works without decl",
r#"
print(40 + 2);
"#,
vec![Value::Int(42)],
),
bindings: vec![HostBindingCase {
name: "print",
factory: make_print_builtin,
}],
},
BoundRuntimeCase {
case: rustscript_runtime_case(
"println function adds newline",
r#"
println(40 + 2);
"#,
vec![Value::string("42\n")],
),
bindings: vec![HostBindingCase {
name: "print",
factory: make_print_builtin,
}],
},
BoundRuntimeCase {
case: rustscript_runtime_case(
"println function supports basic rust style formatting",
r#"
let foo = "hello";
let bar = 42;
println("{} {}!", foo, bar);
"#,
vec![Value::string("hello 42!\n")],
),
bindings: vec![HostBindingCase {
name: "print",
factory: make_print_builtin,
}],
},
BoundRuntimeCase {
case: rustscript_runtime_case(
"print function supports rust style formatting",
r#"
print("hex={:#x} bin={:08b} sci={:.1e}", 42, 5, 1234.0);
"#,
vec![Value::string("hex=0x2a bin=00000101 sci=1.2e3")],
),
bindings: vec![HostBindingCase {
name: "print",
factory: make_print_builtin,
}],
},
BoundRuntimeCase {
case: rustscript_runtime_case(
"println function supports rust style formatting",
r#"
println("{1} {0}", "left", "right");
"#,
vec![Value::string("right left\n")],
),
bindings: vec![HostBindingCase {
name: "print",
factory: make_print_builtin,
}],
},
BoundRuntimeCase {
case: rustscript_runtime_case(
"print alias handles mixed call arities",
r#"
print(1);
print("{}", 2);
"#,
vec![Value::Int(1), Value::string("2")],
),
bindings: vec![HostBindingCase {
name: "print",
factory: make_print_builtin,
}],
},
];
run_bound_runtime_cases(cases);
}
#[test]
fn rustscript_print_rejects_non_literal_format_string() {
let case = rustscript_parse_error_case(
"print rejects non literal format string",
r#"
let fmt = "{}";
print(fmt, 1);
"#,
&["print formatting requires a string literal as the first argument"],
);
expect_parse_error_case(&case);
}
#[test]
fn compile_source_file_with_rustscript_complex_fixture() {
let path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/example_complex.rss");
let compiled = compile_source_file(path.as_path()).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
for func in &compiled.functions {
match func.name.as_str() {
"print" => {
vm.register_function(Box::new(PrintBuiltin));
}
"add_one" => {
vm.register_function(Box::new(AddOne));
}
"runtime::sleep" => {
vm.register_function(Box::new(RuntimeSleep));
}
_ => panic!("unexpected function {}", func.name),
};
}
loop {
match vm.run().expect("vm should run") {
VmStatus::Halted => break,
VmStatus::Yielded => continue,
VmStatus::Waiting(_op_id) => vm
.wait_for_host_op_blocking()
.expect("vm should complete host operation"),
}
}
assert_eq!(vm.stack(), &[Value::Int(12)]);
}
#[test]
fn rustscript_named_function_capture_runtime_cases_work() {
let cases = [
RuntimeCase {
name: "named functions can compose other named functions",
source: r#"
fn inc(x) { x + 1 }
fn twice(x) { x * 2 }
fn combine(a, b) {
let left = inc(a);
let right = twice(b);
left + right;
}
combine(3, 4);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(12)],
expected_locals: None,
},
RuntimeCase {
name: "named function calls can nest without recursion",
source: r#"
fn inc(x) { x + 1 }
fn double_inc(x) { inc(inc(x)) }
fn score(x) {
let a = double_inc(x);
let b = inc(x);
a + b;
}
score(5);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(13)],
expected_locals: None,
},
RuntimeCase {
name: "nested named function implicitly captures outer local",
source: r#"
fn outer(base) {
fn add(v) { v + base }
add(2);
}
outer(5);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(7)],
expected_locals: None,
},
RuntimeCase {
name: "top-level named function capture snapshots at declaration time",
source: r#"
let mut base = 5;
fn add(v) { v + base }
base = 100;
add(1);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(6)],
expected_locals: None,
},
RuntimeCase {
name: "top-level named function keeps transitive captures alive across inline calls",
source: r#"
let lut = {"a": 1};
fn parse_key(text) { lut[text[0:1]] }
fn wrapper(text) { parse_key(text) }
wrapper("ab");
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(1)],
expected_locals: None,
},
RuntimeCase {
name: "top-level named function capture survives repeated calls",
source: r#"
let lut = {"a": 1};
fn parse_key(text) { lut[text[0:1]] }
parse_key("ab");
parse_key("ab");
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(1), Value::Int(1)],
expected_locals: None,
},
RuntimeCase {
name: "named function capture via copy keeps source reusable",
source: r#"
let a = "x";
fn add(v) { v + a.copy() }
let d = a;
add(d);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "nested named functions can capture outer locals and call siblings",
source: r#"
fn outer(base) {
fn inc(v) { v + 1 }
fn add_base(v) { inc(v) + base }
add_base(2);
}
outer(3);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(6)],
expected_locals: None,
},
];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_named_function_block_body_allows_trailing_expression_without_semicolon() {
let case = RuntimeCase {
name: "named function block body allows trailing expression without semicolon",
source: r#"
fn addme(x) {
let doubled = x + x;
doubled + 1
}
addme(20);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(41)],
expected_locals: None,
};
run_runtime_case(&case);
}
#[test]
fn rustscript_named_function_capture_error_cases_work() {
let cases = [
SourceErrorCase {
name: "named function default capture moves movable local",
source: r#"
let a = "";
fn add(v: string) -> string { v + a }
let d = a;
d;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Parse,
expected_contains_all: &[],
},
SourceErrorCase {
name: "named function repeated default move capture is rejected",
source: r#"
let lut = {"a": 1};
fn parse_a(text: string) -> int { lut[text[0:1]] }
fn parse_b(text: string) -> int { lut[text[0:1]] }
parse_a("ab");
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Parse,
expected_contains_all: &["lut", "moved"],
},
SourceErrorCase {
name: "named function recursion is still rejected",
source: r#"
fn recurse(x: int) -> int { recurse(x) }
recurse(1);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InlineFunctionRecursion),
expected_contains_all: &[],
},
];
for case in &cases {
expect_source_error_case(case);
}
}
#[test]
fn closure_captures_outer_value_at_definition_time() {
let case = RuntimeCase {
name: "closure captures outer value at definition time",
source: r#"
let mut base = 7;
let add = |value| value + base;
base = 8;
print(add(5));
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(12)],
expected_locals: None,
};
let bindings = [HostBindingCase {
name: "print",
factory: make_print_builtin,
}];
run_runtime_case_with_bindings(&case, &bindings);
}
#[test]
fn rustscript_closure_value_runtime_cases_work() {
let cases = vec![
RuntimeCase {
name: "closure values are first class and can be passed to functions",
source: r#"
fn apply_twice(func, value) {
let once = func(value);
func(once);
}
let inc = |x| x + 1;
apply_twice(inc, 40);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(42)],
expected_locals: None,
},
RuntimeCase {
name: "empty parameter closure literal captures and runs",
source: r#"
let x = 41;
let f = || x + 1;
f();
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(42)],
expected_locals: None,
},
RuntimeCase {
name: "named functions are first class and can be passed to functions",
source: r#"
fn add_one(value) {
value + 1;
}
fn apply_twice(func, value) {
let once = func(value);
func(once);
}
apply_twice(add_one, 40);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(42)],
expected_locals: None,
},
RuntimeCase {
name: "closure capture can feed named function call",
source: r#"
fn add(value, delta) {
value + delta;
}
let delta = 1;
let apply = |value| add(value, delta);
apply(41);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(42)],
expected_locals: None,
},
RuntimeCase {
name: "closure capture numeric field is copy by default",
source: r#"
let mut p = { a: 1 };
let f = |_| p.a + p.a;
f(0);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(2)],
expected_locals: None,
},
RuntimeCase {
name: "closure capture string concat auto copies field reads",
source: r#"
let p = { a: "x" };
let f = |_| p.a + p.a;
f(0);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "closure capture string concat with borrowed rhs is allowed",
source: r#"
let p = { a: "x" };
let f = |_| p.a + &p.a;
f(0);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "closure capture string concat with mut borrowed rhs is allowed",
source: r#"
let mut p = { a: "x" };
let f = |_| p.a + &mut p.a;
f(0);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "closure capture via copy keeps source reusable",
source: r#"
let a = "x";
let f = |d| d + a.copy();
let d = a;
f(d);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "closure capture via borrow keeps source reusable",
source: r#"
let a = "x";
let f = |d| d + &a;
let d = a;
f(d);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "closure capture via mut borrow keeps source reusable",
source: r#"
let mut a = "x";
let f = |d| d + &mut a;
let d = a;
f(d);
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "moved closure capture value stays alive until closure use",
source: r#"
fn apply_once(func, value) {
func(value);
}
let seed = "!";
let closure = |x| x + seed;
apply_once(closure, "a");
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("a!")],
expected_locals: None,
},
];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_closure_value_parse_rejection_cases_work() {
let cases = vec![
ParseErrorCase {
name: "closure capture respects non numeric field move checks",
source: r#"
let p = { a: "x" };
let _moved = p.a;
let f = |_| p.a;
f(0);
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["moved"],
},
ParseErrorCase {
name: "closure mut borrow still respects non numeric field move checks",
source: r#"
let mut p = { a: "x" };
let _moved = p.a;
let f = |_| &mut p.a;
f(0);
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["moved"],
},
ParseErrorCase {
name: "closure value from partial control flow is rejected on call",
source: r#"
if true {
let inc = |x| x + 1;
}
inc(1);
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["inc", "unavailable"],
},
ParseErrorCase {
name: "closure default capture moves movable local",
source: r#"
let a = "";
let x = |d| { d + a };
let d = a;
d;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["local 'a'", "moved"],
},
ParseErrorCase {
name: "function value from partial control flow is rejected on call",
source: r#"
fn add_one(value: int) -> int {
value + 1;
}
if true {
let f = add_one;
}
f(1);
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["f", "unavailable"],
},
];
for case in &cases {
expect_parse_error_case(case);
}
}
#[test]
fn rustscript_closure_captured_callable_invocation_is_rejected() {
let cases = vec![
SourceErrorCase {
name: "captured function-valued local cannot be invoked from closure body",
source: r#"
fn add_one(value) {
value + 1;
}
let func = add_one;
let apply = |value| func(value);
apply(41);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::NonCallableLocal),
expected_contains_all: &[],
},
SourceErrorCase {
name: "captured closure-valued local cannot be invoked from closure body",
source: r#"
let inc = |x| x + 1;
let apply = |value| inc(value);
apply(41);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::NonCallableLocal),
expected_contains_all: &[],
},
];
for case in &cases {
expect_source_error_case(case);
}
}
#[test]
fn rustscript_move_and_alias_runtime_cases_work() {
let cases = vec![
RuntimeCase {
name: "numeric field access is copy by default",
source: r#"
let p = { a: 1, b: 1.5 };
let first = p.a;
let second = p.a;
let sum = p.b + p.b;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(2)],
expected_locals: None,
},
RuntimeCase {
name: "numeric field access is copy by default when initialized from numeric local",
source: r#"
let n = 1;
let p = { a: n };
let first = p.a;
let second = p.a;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(2)],
expected_locals: None,
},
RuntimeCase {
name: "copyable field access remains copyable through local roundtrip",
source: r#"
let p = { a: 1 };
let x = p.a;
let q = { b: x };
let first = q.b;
let second = q.b;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(2)],
expected_locals: None,
},
RuntimeCase {
name: "bool fields are copy by default and null object entries are omitted",
source: r#"
let p = { b: true, n: null };
let b1 = p.b;
let b2 = p.b;
if b1 && b2 && !p.has("n") {
1;
} else {
0;
}
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(1)],
expected_locals: None,
},
RuntimeCase {
name: "non numeric field access can be copied with copy",
source: r#"
let p = { a: "x" };
let first = p.a.copy();
let second = p.a;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "non copy local can be explicitly copied before local move",
source: r#"
let a = "2";
let b = a.copy();
a + b;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("22")],
expected_locals: None,
},
RuntimeCase {
name: "array sibling index remains accessible after partial move",
source: r#"
let arr = [1, 2, 3, 4];
let first = arr[0];
let second = arr[1];
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(3)],
expected_locals: None,
},
RuntimeCase {
name: "map sibling field remains accessible after partial move",
source: r#"
let m = { a: 1, b: 2 };
let first = m.a;
let second = m.b;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(3)],
expected_locals: None,
},
RuntimeCase {
name: "non numeric field access can be borrowed with ampersand",
source: r#"
let p = { a: "x" };
let first = &p.a;
let second = p.a;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "non numeric field access can be borrowed with rust style mut ampersand",
source: r#"
let mut p = { a: "x" };
let first = &mut p.a;
let second = p.a;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "non numeric field access can be borrowed with spaced mut ampersand",
source: r#"
let mut p = { a: "x" };
let first = & mut p.a;
let second = p.a;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "non numeric field access can be mut borrowed with parenthesized field",
source: r#"
let mut p = { a: "x" };
let first = &mut (p.a);
let second = p.a;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "non numeric field mut borrow can be passed through function call",
source: r#"
fn id(x) {
x;
}
let mut p = { a: "x" };
let first = id(&mut p.a);
let second = p.a;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "moved field can be reinitialized with indexed assignment",
source: r#"
let mut p = { a: "222", b: "666" };
let _moved = p.a;
p.a = "444";
let y = p.a;
y + p.b;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("444666")],
expected_locals: None,
},
RuntimeCase {
name: "moved field reinitialized inside loop remains usable",
source: r#"
let mut p = { a: "start" };
let mut i = 0;
while i < 2 {
let _moved = p.a;
p.a = "new";
i = i + 1;
}
p.a;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("new")],
expected_locals: None,
},
RuntimeCase {
name: "mutating after copy detach is allowed",
source: r#"
let mut p = { a: 1 };
let mut q = p;
q = q.copy();
p.a = 2;
p.a + q.a;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(3)],
expected_locals: None,
},
RuntimeCase {
name: "reassigning collection breaks old alias for target local",
source: r#"
let mut a = [1];
let b = a;
a = [2];
a[0] = 3;
a[0] + b[0];
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(4)],
expected_locals: None,
},
];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_local_move_consumes_source_slot_at_runtime() {
let source = r#"
let a = "2";
let b = a;
b;
"#;
let compiled = vm::compile_source_for_repl(source).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::string("2")]);
assert!(
!local_visible_at_current_line(&vm, "a"),
"moved source local should not remain visible at the final line"
);
assert!(
local_visible_at_current_line(&vm, "b"),
"move target should remain visible at the final line"
);
}
#[test]
fn rustscript_interprocedural_consumed_param_moves_caller_local_at_runtime() {
let source = r#"
fn consume_once(value) {
let taken = value;
taken;
}
let a = "2";
let b = consume_once(a);
b;
"#;
let compiled = compile_source(source).expect("compile should succeed");
let debug = compiled
.program
.debug
.as_ref()
.expect("debug info should exist");
let a_index = debug.local_index("a").expect("a binding should exist");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::string("2")]);
assert_eq!(vm.locals()[a_index as usize], Value::Null);
}
#[test]
fn rustscript_field_move_updates_runtime_container_state() {
let source = r#"
let mut p = { a: "x", b: "y" };
let moved = p.a;
let rest = p.b;
moved + rest;
"#;
let compiled = vm::compile_source_for_repl(source).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::string("xy")]);
}
#[test]
fn rustscript_field_move_expr_statement_updates_runtime_container_state() {
let source = r#"
let mut p = { a: "x", b: "y" };
p.a;
"#;
let compiled = vm::compile_source_for_repl(source).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::string("x")]);
let Some(Value::Map(entries)) = vm.locals().first() else {
panic!("expected first local to be moved map container");
};
let mut saw_a_present = false;
let mut saw_b_y = false;
for (key, value) in entries.iter() {
match (key, value) {
(Value::String(name), _) if name.as_str() == "a" => saw_a_present = true,
(Value::String(name), Value::String(text))
if name.as_str() == "b" && text.as_str() == "y" =>
{
saw_b_y = true
}
_ => {}
}
}
assert!(
!saw_a_present,
"expected moved field 'a' to be removed from local container"
);
assert!(
saw_b_y,
"expected untouched field 'b' to remain present in local container"
);
}
#[test]
fn rustscript_index_move_updates_runtime_container_state() {
let source = r#"
let mut arr = ["x", "y"];
let moved = arr[0];
let rest = arr[1];
moved + rest;
"#;
let compiled = vm::compile_source_for_repl(source).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::string("xy")]);
}
#[test]
fn rustscript_move_and_alias_parse_rejection_cases_work() {
let branch_case = SourceErrorCase {
name: "local move in one branch is rejected after merge",
source: r#"
let value = "x";
if true {
let _moved = value;
} else {
0;
}
value;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Parse,
expected_contains_all: &["moved earlier", "value.copy()"],
};
expect_source_error_case(&branch_case);
let loop_branch_case = SourceErrorCase {
name: "move in one loop branch is visible after loop",
source: r#"
let p = { a: "x" };
let mut i = 0;
while i < 2 {
if i == 0 {
let _moved = p.a;
}
i = i + 1;
}
p.a;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Parse,
expected_contains_all: &["moved earlier", "p.a.copy()"],
};
expect_source_error_case(&loop_branch_case);
let cases = vec![
ParseErrorCase {
name: "non numeric field access is moved by default",
source: r#"
let p = { a: "x" };
let first = p.a;
let second = p.a;
second;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["p.a", "moved"],
},
ParseErrorCase {
name: "whole local use after field move is rejected",
source: r#"
let p = { a: "x", b: "y" };
let _moved = p.a;
p;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["p", "partially moved"],
},
ParseErrorCase {
name: "whole local use after index move is rejected",
source: r#"
let arr = [1, 2, 3];
let _moved = arr[0];
arr.length;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["arr", "partially moved"],
},
ParseErrorCase {
name: "whole local use after slice move is rejected",
source: r#"
let arr = [1, 2, 3, 4];
let _moved = arr[1:3];
arr.length;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["arr", "partially moved"],
},
ParseErrorCase {
name: "non copy local assignment moves source by default",
source: r#"
let a = "2";
let b = a;
a + b;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["local 'a'", "moved"],
},
ParseErrorCase {
name: "callee consumed parameter moves caller local",
source: r#"
fn consume_once(value) {
let taken = value;
taken;
}
let a = "x";
consume_once(a);
a;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["local 'a'", "moved"],
},
ParseErrorCase {
name: "transitive consumed parameter moves caller local",
source: r#"
fn consume_once(value) {
let taken = value;
taken;
}
fn forward(input) {
consume_once(input);
0;
}
let a = "x";
forward(a);
a;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["local 'a'", "moved"],
},
ParseErrorCase {
name: "local move in loop body is rejected on next iteration",
source: r#"
let value = "x";
let mut i = 0;
while i < 2 {
let _moved = value;
i = i + 1;
}
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["local 'value'", "moved"],
},
ParseErrorCase {
name: "borrowed then moved then second move still fails",
source: r#"
let p = { a: "x" };
let _loan = &p.a;
let _move = p.a;
let again = p.a;
again;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["moved"],
},
ParseErrorCase {
name: "borrowed field still respects prior move errors",
source: r#"
let p = { a: "x" };
let _moved = p.a;
let again = &p.a;
again;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["moved"],
},
ParseErrorCase {
name: "mut borrowed field still respects prior move errors",
source: r#"
let mut p = { a: "x" };
let _moved = p.a;
let again = &mut p.a;
again;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["moved"],
},
ParseErrorCase {
name: "mut borrow rejects temporary expression target",
source: r#"
let value = &mut (1 + 2);
value;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["mutable borrow target"],
},
ParseErrorCase {
name: "mut borrow rejects readonly borrow expression target",
source: r#"
let p = { a: "x" };
let value = &mut &p.a;
value;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["mutable borrow target"],
},
ParseErrorCase {
name: "mut borrowed function argument still respects prior move errors",
source: r#"
fn id(x) {
x;
}
let mut p = { a: "x" };
let _moved = p.a;
id(&mut p.a);
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["moved"],
},
ParseErrorCase {
name: "while loop repeated field move is rejected",
source: r#"
let p = { a: "x" };
let mut i = 0;
while i < 2 {
let _moved = p.a;
i = i + 1;
}
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["p.a", "moved"],
},
ParseErrorCase {
name: "for loop repeated field move is rejected",
source: r#"
let p = { a: "x" };
for i in 0..2 {
let _moved = p.a;
}
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["p.a", "moved"],
},
ParseErrorCase {
name: "break path preserves moved field state after loop",
source: r#"
let p = { a: "x" };
while true {
let _moved = p.a;
break;
}
let again = p.a;
again;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["p.a", "moved"],
},
ParseErrorCase {
name: "continue path rechecks moved field on next iteration",
source: r#"
let p = { a: "x" };
let mut i = 0;
while i < 2 {
let _moved = p.a;
i = i + 1;
continue;
}
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["p.a", "moved"],
},
ParseErrorCase {
name: "match arm move marks field as possibly moved",
source: r#"
let p = { a: "x" };
let key = 1;
let _v = match key {
1 => p.a,
_ => "fallback",
};
p.a;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["p.a", "moved"],
},
ParseErrorCase {
name: "map mutation is rejected while collection alias exists",
source: r#"
let mut p = { a: 1 };
let q = p;
p.a = 2;
p.a + q.a;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["aliased", "p"],
},
ParseErrorCase {
name: "array mutation is rejected while collection alias exists",
source: r#"
let mut a = [1];
let b = a;
a[0] = 2;
a[0] + b[0];
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["aliased", "a"],
},
ParseErrorCase {
name: "array append is rejected while collection alias exists",
source: r#"
let mut a = [1];
let b = a;
a[a.length] = 2;
b[0];
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["aliased", "a"],
},
ParseErrorCase {
name: "array mutation is rejected while mut borrow alias exists",
source: r#"
let mut a = [1];
let b = &mut a;
a[0] = 2;
b[0];
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["aliased", "a"],
},
ParseErrorCase {
name: "collection alias from passthrough function is tracked",
source: r#"
fn id(x) {
x;
}
let mut a = [1];
let b = id(a);
a[0] = 2;
a[0] + b[0];
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["aliased", "a"],
},
ParseErrorCase {
name: "collection alias from passthrough mut borrow function is tracked",
source: r#"
fn id(x) {
x;
}
let mut a = [1];
let b = id(&mut a);
a[0] = 2;
a[0] + b[0];
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["aliased", "a"],
},
];
for case in &cases {
expect_parse_error_case(case);
}
}
#[test]
fn rustscript_mutability_runtime_cases_work() {
let cases = vec![
RuntimeCase {
name: "mutable local assignment requires and supports let mut",
source: r#"
let mut value = 1;
value = value + 1;
value;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(2)],
expected_locals: None,
},
RuntimeCase {
name: "mutable member assignment supports let mut binding",
source: r#"
let mut profile = { score: 1 };
profile.score = 2;
profile.score;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(2)],
expected_locals: None,
},
RuntimeCase {
name: "mutable index assignment supports let mut binding",
source: r#"
let mut arr = [1];
arr[0] = 2;
arr[0];
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(2)],
expected_locals: None,
},
RuntimeCase {
name: "mutable borrow requires mutable root binding and succeeds with let mut",
source: r#"
let mut p = { a: "x" };
let first = &mut p.a;
let second = p.a;
first + second;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::string("xx")],
expected_locals: None,
},
RuntimeCase {
name: "rust style exclusive range for loop works",
source: r#"
let mut total = 0;
for i in 0..3 {
total = total + i;
}
total;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(3)],
expected_locals: None,
},
RuntimeCase {
name: "rust style inclusive range for loop works",
source: r#"
let mut total = 0;
for i in 1..=3 {
total = total + i;
}
total;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(6)],
expected_locals: None,
},
];
run_runtime_cases(&cases);
}
#[test]
fn c_style_for_loop_parentheses_are_rejected() {
let err = match compile_source(
r#"
let mut total = 0;
for (let mut i = 0; i < 3; i = i + 1) {
total = total + i;
}
total;
"#,
) {
Ok(_) => panic!("parenthesized for loop should be rejected"),
Err(err) => err,
};
let text = err.to_string();
assert!(
text.contains("expected Rust-style for-in loop") && text.contains("for i in 0..n"),
"unexpected error: {text}"
);
}
#[test]
fn rustscript_mutability_parse_rejection_cases_work() {
let cases = vec![
ParseErrorCase {
name: "assignment to immutable local is rejected",
source: r#"
let value = 1;
value = 2;
value;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["immutable local", "let mut value"],
},
ParseErrorCase {
name: "member assignment through immutable local is rejected",
source: r#"
let profile = { score: 1 };
profile.score = 2;
profile.score;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["immutable local", "let mut profile"],
},
ParseErrorCase {
name: "index assignment through immutable local is rejected",
source: r#"
let arr = [1];
arr[0] = 2;
arr[0];
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["immutable local", "let mut arr"],
},
ParseErrorCase {
name: "mutable borrow of immutable local is rejected",
source: r#"
let profile = { score: 1 };
let b = &mut profile.score;
b;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["immutable local", "let mut profile"],
},
ParseErrorCase {
name: "mutable borrow of immutable collection local is rejected",
source: r#"
let arr = [1];
let b = &mut arr;
b[0];
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["immutable local", "let mut arr"],
},
];
for case in &cases {
expect_parse_error_case(case);
}
}
#[test]
fn liveness_clears_local_after_closure_value_last_use() {
let source = r#"
fn apply_once(func, value) {
func(value);
}
let mut closure = "stale";
let base = 1;
closure = |x| x + base;
let out = apply_once(closure, 41);
out;
"#;
let compiled = compile_source(source).expect("compile should succeed");
let debug = compiled
.program
.debug
.as_ref()
.expect("debug info should exist");
let closure_index = debug
.local_index("closure")
.expect("closure binding should exist");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(
vm.stack().len(),
1,
"apply_once result should be the only remaining stack value"
);
assert_eq!(vm.stack()[0], Value::Int(42));
assert!(
!vm.stack()
.iter()
.any(|value| matches!(value, Value::String(text) if text.as_str() == "stale")),
"stack should not retain pre-call placeholder values"
);
assert_eq!(vm.locals()[closure_index as usize], Value::Null);
}
#[test]
fn liveness_clears_local_after_function_value_last_use() {
let source = r#"
fn add_one(value) {
value + 1;
}
fn apply_once(func, value) {
func(value);
}
let mut func = "stale";
func = add_one;
let out = apply_once(func, 41);
out;
"#;
let compiled = compile_source(source).expect("compile should succeed");
let debug = compiled
.program
.debug
.as_ref()
.expect("debug info should exist");
let func_index = debug
.local_index("func")
.expect("func binding should exist");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(
vm.stack().len(),
1,
"apply_once result should be the only remaining stack value"
);
assert_eq!(vm.stack()[0], Value::Int(42));
assert!(
!vm.stack()
.iter()
.any(|value| matches!(value, Value::String(text) if text.as_str() == "stale")),
"stack should not retain pre-call placeholder values"
);
assert_eq!(vm.locals()[func_index as usize], Value::Null);
}
#[test]
fn inline_function_call_frame_slots_are_cleared_after_return() {
let source = r#"
fn make_pair() {
let left = "L";
let right = "R";
left + right;
}
make_pair();
0;
"#;
let compiled = compile_source(source).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack().last(), Some(&Value::Int(0)));
assert!(
vm.locals().iter().all(|value| matches!(value, Value::Null)),
"expected all inline call-frame locals to be cleared after return, got {:?}",
vm.locals()
);
}
#[test]
fn interprocedural_closure_capture_slots_are_cleared_after_last_use() {
let source = r#"
fn apply_once(func, value) {
func(value);
}
let seed = "!";
let closure = |x| x + seed;
apply_once(closure, "a");
0;
"#;
let compiled = compile_source(source).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack().last(), Some(&Value::Int(0)));
assert!(
vm.locals().iter().all(|value| matches!(value, Value::Null)),
"expected closure capture and call-frame slots to clear after last use, got {:?}",
vm.locals()
);
}
#[test]
fn rustscript_callable_values_cannot_be_stored_in_arrays() {
let case = SourceErrorCase {
name: "callable values cannot be stored in arrays",
source: r#"
fn add_one(value) {
value + 1;
}
let func = add_one;
let values = [func];
values.length;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::CallableUsedAsValue),
expected_contains_all: &[],
};
expect_source_error_case(&case);
}
#[test]
fn rustscript_callable_values_cannot_be_returned_from_functions() {
let case = SourceErrorCase {
name: "callable values cannot be returned from functions",
source: r#"
fn add_one(value: int) -> int {
value + 1;
}
fn get_adder() {
add_one;
}
let func = get_adder();
func(41);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::CallableUsedAsValue),
expected_contains_all: &[],
};
expect_source_error_case(&case);
}
#[test]
fn rustscript_if_and_match_runtime_cases_work() {
let cases = vec![
RuntimeCase {
name: "if expression assignment syntax is supported",
source: r#"
let x = if 2 > 1 => { 42 } else => { 0 };
x;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(42)],
expected_locals: None,
},
RuntimeCase {
name: "if expression branch blocks support multiline statements",
source: r#"
let base = 40;
let out = if true => {
let bump = base + 2;
bump
} else => {
let fallback = base - 1;
fallback
};
out;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(42)],
expected_locals: None,
},
RuntimeCase {
name: "if expression assignment executes else branch",
source: r#"
let mut marker = 0;
let out = if false => {
marker = 1;
10
} else => {
marker = 2;
20
};
marker + out;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(22)],
expected_locals: None,
},
RuntimeCase {
name: "if expression supports else if chains",
source: r#"
let key = 2;
let out = if key == 1 => { 10 } else if key == 2 => { 20 } else => { 0 };
out;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(20)],
expected_locals: None,
},
RuntimeCase {
name: "match expression supports int and wildcard patterns",
source: r#"
let value = 2;
let out = match value {
1 => 10,
2 => 20,
_ => 0,
};
out;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(20)],
expected_locals: None,
},
RuntimeCase {
name: "match expression supports string patterns",
source: r#"
let key = "beta";
let out = match key {
"alpha" => 1,
"beta" => 2,
_ => 0,
};
out;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(2)],
expected_locals: None,
},
RuntimeCase {
name: "match expression supports type patterns",
source: r#"
let a = match "value" {
Some(String) => 1,
_ => 0,
};
let b = match 7 {
Some(Number) => 2,
_ => 0,
};
let c = match true {
Some(Number) => 100,
_ => 3,
};
a + b + c;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(6)],
expected_locals: None,
},
RuntimeCase {
name: "match expression supports None and Some binding for optional values",
source: r#"
struct Data { values: [int] }
let data: Data = { values: [41] };
let present = match data?.values?.[0] {
None => 0,
Some(value) => value + 1,
_ => 0,
};
let missing = match data?.values?.[1] {
None => 1,
Some(value) => value,
_ => 0,
};
present + missing;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(43)],
expected_locals: None,
},
];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_if_and_match_parse_rejection_cases_work() {
let cases = vec![
ParseErrorCase {
name: "if expression requires else branch",
source: r#"
let x = if true => { 1 };
x;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["requires an else branch"],
},
ParseErrorCase {
name: "match expression rejects unsupported patterns",
source: r#"
let value = 1;
match value {
true => 10,
_ => 0,
};
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["int/string/null literals, None, Some(name), type patterns"],
},
];
for case in &cases {
expect_parse_error_case(case);
}
}
#[test]
fn compile_source_file_rustscript_imports_merge_with_scoped_locals() {
let unique = format!(
"vm_rss_import_scope_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let module_path = root.join("module.rss");
let main_path = root.join("main.rss");
std::fs::write(
&module_path,
r#"
pub fn add_one(x) -> int;
let shared = 40;
"#,
)
.expect("module source should write");
std::fs::write(
&main_path,
r#"
use module;
let shared = add_one(1);
shared;
"#,
)
.expect("main source should write");
let compiled = compile_source_file(main_path.as_path()).expect("compile should succeed");
let debug = compiled
.program
.debug
.as_ref()
.expect("compiled program should include debug info");
assert!(
debug
.locals
.iter()
.any(|local| local.name == "module::shared"),
"module-scoped local should remain visible in debug metadata"
);
assert!(
debug.locals.iter().any(|local| local.name == "shared"),
"root-scoped local should remain visible in debug metadata"
);
assert_eq!(
compiled
.functions
.iter()
.filter(|func| func.name == "add_one")
.count(),
1,
"imported function should only be declared once",
);
let mut vm = Vm::new(compiled.program);
vm.bind_function("add_one", Box::new(AddOne));
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::Int(2)]);
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_file(module_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn compile_source_file_rustscript_imported_function_capture_binds_once() {
let unique = format!(
"vm_rss_import_capture_bind_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let module_path = root.join("module.rss");
let main_path = root.join("main.rss");
std::fs::write(
&module_path,
r#"
let t = [7];
fn inner_arg(x) { x[0] }
pub fn run() { inner_arg(t) }
"#,
)
.expect("module source should write");
std::fs::write(
&main_path,
r#"
use self::module as m;
m::run();
"#,
)
.expect("main source should write");
let compiled = compile_source_file(main_path.as_path()).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::Int(7)]);
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_file(module_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn compile_source_file_rustscript_imported_borrow_capture_survives_nested_calls() {
let unique = format!(
"vm_rss_import_direct_capture_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let module_path = root.join("module.rss");
let main_path = root.join("main.rss");
std::fs::write(
&module_path,
r#"
let lut = {"a": 1};
let digits = [0, 2];
fn parse_key(text: string) -> int { (&lut)[text[0:1]] }
fn read_digit() -> int { (&digits)[1] }
pub fn run() -> int {
let a = parse_key("ab");
let b = read_digit();
let c = parse_key("ab");
a + b + c;
}
"#,
)
.expect("module source should write");
std::fs::write(
&main_path,
r#"
use self::module as m;
m::run();
"#,
)
.expect("main source should write");
let compiled = compile_source_file(main_path.as_path()).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::Int(4)]);
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_file(module_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn compile_source_file_rustscript_imported_direct_capture_multiple_move_is_rejected() {
let unique = format!(
"vm_rss_import_direct_capture_error_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let module_path = root.join("module.rss");
let main_path = root.join("main.rss");
std::fs::write(
&module_path,
r#"
let lut = {"a": 1};
fn parse_key(text: string) -> int { lut[text[0:1]] }
fn parse_again(text: string) -> int { lut[text[0:1]] }
pub fn run() {
parse_key("ab");
}
"#,
)
.expect("module source should write");
std::fs::write(
&main_path,
r#"
use self::module as m;
m::run();
"#,
)
.expect("main source should write");
let err = match compile_source_file(main_path.as_path()) {
Ok(_) => panic!("compile should fail"),
Err(err) => err,
};
match err {
vm::SourcePathError::Source(vm::SourceError::Parse(parse)) => {
assert!(
parse.message.contains("lut") && parse.message.contains("moved"),
"unexpected parse error: {parse:?}"
);
}
other => panic!("expected parse error, got {other:?}"),
}
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_file(module_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn compile_source_file_rustscript_rejects_import_keyword() {
let unique = format!(
"vm_rss_use_keyword_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let main_path = root.join("main.rss");
std::fs::write(&main_path, "import \"./module.rss\";\n1;\n").expect("source should write");
let err = match compile_source_file(main_path.as_path()) {
Ok(_) => panic!("legacy import syntax should be rejected for RustScript"),
Err(err) => err,
};
assert!(
matches!(
err,
vm::SourcePathError::InvalidImportSyntax { ref message, .. }
if message.contains("uses 'use', not 'import'")
),
"expected use-keyword guidance, got {err:?}"
);
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn compile_source_file_rustscript_supports_namespace_and_named_imports() {
let unique = format!(
"vm_rustscript_namespace_import_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let module_path = root.join("strings.rss");
std::fs::write(
&module_path,
r#"
fn eq(lhs, rhs) {
lhs == rhs;
}
pub fn is_empty(value) {
eq(value, "");
}
pub fn non_empty(value) {
eq(is_empty(value), false);
}
"#,
)
.expect("module source should write");
let main_path = root.join("main.rss");
std::fs::write(
&main_path,
r#"
use strings as string;
use strings::{is_empty as is_empty};
string::non_empty("rss");
is_empty("");
"#,
)
.expect("main source should write");
let compiled = compile_source_file(main_path.as_path()).expect("compile should succeed");
assert!(
compiled.functions.is_empty(),
"module functions should be fully inlined for RustScript root"
);
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::Bool(true), Value::Bool(true)]);
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_file(module_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn compile_source_file_rustscript_all_public_import_supports_namespace_calls() {
let unique = format!(
"vm_rustscript_all_public_namespace_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let module_path = root.join("runtime.rss");
std::fs::write(
&module_path,
r#"
pub fn sleep(ms) {
ms;
}
"#,
)
.expect("module source should write");
let main_path = root.join("main.rss");
std::fs::write(
&main_path,
r#"
use runtime;
runtime::sleep(3);
"#,
)
.expect("main source should write");
let compiled = compile_source_file(main_path.as_path()).expect("compile should succeed");
let mut vm = Vm::new(compiled.program);
let status = vm.run().expect("vm should run");
assert_eq!(status, VmStatus::Halted);
assert_eq!(vm.stack(), &[Value::Int(3)]);
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_file(module_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn compile_source_file_rustscript_missing_runtime_module_falls_back_to_host_namespace() {
let unique = format!(
"vm_rustscript_runtime_host_fallback_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let main_path = root.join("main.rss");
std::fs::write(
&main_path,
r#"
use runtime;
runtime::sleep(3);
"#,
)
.expect("main source should write");
let compiled = compile_source_file(main_path.as_path()).expect("compile should succeed");
assert!(
compiled
.program
.imports
.iter()
.any(|import| import.name == "runtime::sleep"),
"missing runtime.rss should fall back to runtime host namespace import"
);
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn compile_source_file_rustscript_host_namespace_alias_maps_to_host_import() {
let unique = format!(
"rustscript_runtime_alias_host_fallback_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos()
);
let root = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&root).expect("temp module root should be created");
let main_path = root.join("main.rss");
std::fs::write(
&main_path,
r#"
use rate_limit as rl;
rl::allow("client-a", 2, 30);
"#,
)
.expect("main source should write");
let compiled = compile_source_file(main_path.as_path()).expect("compile should succeed");
assert!(
compiled
.program
.imports
.iter()
.any(|import| import.name == "rate_limit::allow"),
"namespace alias should map to rate_limit host import"
);
let _ = std::fs::remove_file(main_path);
let _ = std::fs::remove_dir(root);
}
#[test]
fn rustscript_language_runtime_cases_work() {
let cases = vec![
RuntimeCase {
name: "modulo and logical operators work",
source: r#"
let a = 17 % 5;
let b = true && false;
let c = true || false;
let d = (10 > 5) && (3 < 7);
let e = (10 < 5) || (3 > 7);
let f = 100 % 7;
a + f;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(4)],
expected_locals: None,
},
RuntimeCase {
name: "null literal is supported",
source: r#"
let v = null;
type(v) == "null";
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Bool(true)],
expected_locals: None,
},
RuntimeCase {
name: "null values and type patterns are supported",
source: r#"
let some = 1 + 1;
let none = null;
let some2 = 40;
let a = match none {
null => 1,
_ => 0,
};
let b = match some {
null => 0,
_ => 1,
};
let c = match some2 {
Some(Number) => 1,
_ => 0,
};
let t = type(null);
if t == "null" {
(a + b + c) + some2;
} else {
0;
}
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(43)],
expected_locals: None,
},
RuntimeCase {
name: "match type pattern is not shadowed by local name",
source: r#"
let String = 3;
let b = match "" {
Some(String) => 2,
_ => 3,
};
b;
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(2)],
expected_locals: None,
},
RuntimeCase {
name: "null literal can be used as map key",
source: r#"
let m = { null: 42 };
m[null];
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(42)],
expected_locals: None,
},
];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_language_parse_rejection_cases_work() {
let cases = vec![
ParseErrorCase {
name: "legacy option aliases are rejected",
source: r#"
let some = Some(1);
some;
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["unknown function 'Some'"],
},
ParseErrorCase {
name: "type name of val alias is rejected",
source: r#"
type_name_of_val(null);
"#,
flavor: SourceFlavor::RustScript,
expected_contains_all: &["unknown function 'type_name_of_val'"],
},
];
for case in &cases {
expect_parse_error_case(case);
}
}
#[test]
fn rustscript_non_strict_comparisons_and_integer_edge_literals_work() {
let cases = vec![RuntimeCase {
name: "non strict comparisons and integer edge literals",
source: r#"
let le = 1 <= 1;
let ge = 2 >= 1;
let hex = 0x2a;
let min_dec = -9223372036854775808;
let min_hex = -0x8000000000000000;
if le && ge && hex == 42 && min_dec == min_hex {
min_dec;
} else {
0;
}
"#,
flavor: SourceFlavor::RustScript,
expected_stack: vec![Value::Int(i64::MIN)],
expected_locals: None,
}];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_referenced_struct_schema_runtime_cases_work() {
let cases = vec![
rustscript_runtime_case(
"referenced struct schema propagates through optional chain subobject",
r#"
struct Age { first: int }
struct User { age: Age }
let user: User = { age: { first: 41 } };
let age = user?.age;
age?.first;
"#,
vec![Value::Int(41)],
),
rustscript_runtime_case(
"referenced struct schema propagates through functions and closures",
r#"
struct Age { first: int }
struct User { age: Age }
fn first_age(user) {
user.age.first
}
let user_for_fn: User = { age: { first: 41 } };
let user_for_capture: User = { age: { first: 41 } };
let user_for_closure: User = { age: { first: 41 } };
let read = || user_for_capture.age.first;
let pick = |entry| entry.age.first;
let via_fn = first_age(user_for_fn) + 1;
let via_capture = read() + 2;
let via_closure_param = pick(user_for_closure) + 3;
via_fn;
via_capture;
via_closure_param;
"#,
vec![Value::Int(42), Value::Int(43), Value::Int(44)],
),
];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_referenced_struct_schema_compile_rejections_work() {
let cases = vec![
SourceErrorCase {
name: "referenced struct schema rejects missing field access",
source: r#"
struct User { name: string }
let user: User = { name: "Ada" };
user.age;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["field 'age' is not declared"],
},
SourceErrorCase {
name: "referenced struct schema rejects wrong array index type",
source: r#"
struct User { colors: [int] }
let user: User = { colors: [1, 2] };
user.colors["first"];
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["array access requires an int index"],
},
SourceErrorCase {
name: "referenced struct schema rejects optional access to missing field",
source: r#"
struct User { name: string }
let user: User = { name: "Ada" };
user?.age;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["field 'age' is not declared"],
},
SourceErrorCase {
name: "referenced struct schema rejects invalid function param field",
source: r#"
struct Age { first: int }
struct User { age: Age }
fn first_age(user) {
user.age.agxe
}
let user: User = { age: { first: 41 } };
first_age(user);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["field 'agxe' is not declared"],
},
SourceErrorCase {
name: "referenced struct schema rejects invalid closure capture field",
source: r#"
struct Age { first: int }
struct User { age: Age }
let user: User = { age: { first: 41 } };
let read = || user.age.agxe;
read();
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["field 'agxe' is not declared"],
},
SourceErrorCase {
name: "referenced struct schema rejects invalid closure param field",
source: r#"
struct Age { first: int }
struct User { age: Age }
let pick = |entry| entry.age.agxe;
let user: User = { age: { first: 41 } };
pick(user);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["field 'agxe' is not declared"],
},
];
run_source_error_cases(&cases);
}
#[test]
fn rustscript_recursive_struct_schema_runtime_cases_work() {
let cases = vec![rustscript_runtime_case(
"self recursive struct schema propagates after optional chain assignment",
r#"
struct Node {
value: int,
next: Node,
}
let node: Node = { value: 1, next: { value: 41, next: {} }};
let next = node?.next;
next?.value;
"#,
vec![Value::Int(41)],
)];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_recursive_struct_schema_compile_rejections_work() {
let cases = vec![
SourceErrorCase {
name: "self recursive struct schema rejects invalid direct field after optional chain assignment",
source: r#"
struct Node {
value: int,
next: Node,
}
let node: Node = { value: 1, next: { value: 41, next: {} }};
node.next.agxe;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["field 'agxe' is not declared"],
},
SourceErrorCase {
name: "self recursive struct schema rejects invalid optional field after optional chain assignment",
source: r#"
struct Node {
value: int,
next: Node,
}
let node: Node = { value: 1, next: { value: 41, next: {} }};
let next = node?.next;
next?.agxe;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["field 'agxe' is not declared"],
},
];
run_source_error_cases(&cases);
}
#[test]
fn rustscript_optional_chain_requires_declared_schema_and_explicit_handling() {
let runtime_cases = vec![rustscript_runtime_case(
"declared schema optional chain unwrap_or keeps concrete type",
r#"
struct Stats { score: int }
struct Profile { stats: Stats }
let present: Profile = { stats: { score: 41 } };
let missing: Profile? = null;
let present_score = present?.stats?.score.unwrap_or(0);
let missing_score = missing?.stats?.score.unwrap_or(0);
present_score + missing_score + 1;
"#,
vec![Value::Int(42)],
)];
run_runtime_cases(&runtime_cases);
let error_cases = vec![
SourceErrorCase {
name: "optional chain rejects undeclared rustscript schema",
source: r#"
let profile = { stats: { score: 41 } };
profile?.stats?.score.unwrap_or(0);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &[
"optional access requires a user-declared schema in RustScript",
],
},
SourceErrorCase {
name: "optional chain result must be unwrapped before arithmetic",
source: r#"
struct Stats { score: int }
struct Profile { stats: Stats }
let profile: Profile = { stats: { score: 41 } };
let score = profile?.stats?.score;
score + 1;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["optional value must be unwrapped before binary operation"],
},
SourceErrorCase {
name: "optional chain capture in fn must be handled before arithmetic",
source: r#"
struct Stats { score: int }
struct Profile { stats: Stats }
let profile: Profile = { stats: { score: 41 } };
let score = profile?.stats?.score;
fn add() {
score + 1
}
add();
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["optional value must be unwrapped before binary operation"],
},
SourceErrorCase {
name: "Some and None match patterns require an optional value",
source: r#"
let value = 41;
match value {
None => 0,
Some(found) => found + 1,
_ => 0,
};
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["Some(...) and None match patterns require an optional value"],
},
];
run_source_error_cases(&error_cases);
}
#[test]
fn rustscript_optional_chain_rejects_statistically_mismatched_declared_schemas() {
let error_cases = vec![
SourceErrorCase {
name: "optional chain rejects wrong nested field type under declared schema",
source: r#"
struct Stats { score: int }
struct Profile { stats: Stats }
let profile: Profile = { stats: { score: "oops" } };
profile?.stats?.score.unwrap_or(0);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &[
"field 'stats.score' is declared as schema type 'int' but was assigned string",
],
},
SourceErrorCase {
name: "optional chain rejects missing required nested field under declared schema",
source: r#"
struct Stats { score: int }
struct Profile { stats: Stats }
let profile: Profile = {};
profile?.stats?.score.unwrap_or(0);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &[
"field 'stats' is required by the declared schema but is missing",
],
},
SourceErrorCase {
name: "optional chain rejects reassignment that violates declared schema",
source: r#"
struct Stats { score: int }
struct Profile { stats: Stats }
let mut profile: Profile = { stats: { score: 41 } };
profile = { stats: { score: "oops" } };
profile?.stats?.score.unwrap_or(0);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &[
"field 'stats.score' is declared as schema type 'int' but was assigned string",
],
},
SourceErrorCase {
name: "optional match binding still rejects wrong nested field type under declared schema",
source: r#"
struct Stats { score: int }
struct Profile { stats: Stats }
let profile: Profile = { stats: { score: "oops" } };
match profile?.stats?.score {
None => 0,
Some(score) => score + 1,
_ => 0,
};
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &[
"field 'stats.score' is declared as schema type 'int' but was assigned string",
],
},
];
run_source_error_cases(&error_cases);
}
#[test]
fn rustscript_explicit_optional_type_annotations_work() {
let runtime_cases = vec![
rustscript_runtime_case(
"optional local and return annotations preserve concrete inner typing",
r#"
fn maybe_score(text: string) -> int? {
let mut out: int? = null;
if text == "ok" {
out = 41;
}
out
}
maybe_score("ok").unwrap_or(0) + maybe_score("bad").unwrap_or(1);
"#,
vec![Value::Int(42)],
),
rustscript_runtime_case(
"typed callable locals enforce closure body schemas",
r#"
let mapper: fn(int) -> int = |value| value + 1;
mapper(41);
"#,
vec![Value::Int(42)],
),
];
run_runtime_cases(&runtime_cases);
let error_cases = vec![
SourceErrorCase {
name: "non optional local rejects null assignment",
source: r#"
let value: int = null;
value;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &[
"local is declared as schema type 'int' but was assigned null",
],
},
SourceErrorCase {
name: "non optional return rejects null result",
source: r#"
fn bad() -> int {
null
}
bad();
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::StrictTypingRequired),
expected_contains_all: &[
"function 'bad' is declared to return 'int' but produced null",
],
},
SourceErrorCase {
name: "typed callable locals reject mismatched closure results",
source: r#"
let mapper: fn(int) -> int = |value| value == 41;
mapper(41);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::CallableArgumentTypeMismatch),
expected_contains_all: &["callable body result expects 'int'", "got bool"],
},
SourceErrorCase {
name: "json encode rejects bytes under strict rustscript typing",
source: r#"
use json;
json::encode(b"abc");
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::CallableArgumentTypeMismatch),
expected_contains_all: &["json::encode", "bytes"],
},
];
run_source_error_cases(&error_cases);
}
#[test]
fn rustscript_positive_min_magnitude_literal_is_rejected_with_hint() {
expect_parse_error_contains_any_case(
"positive min magnitude literal is rejected with hint",
"0x8000000000000000;",
SourceFlavor::RustScript,
&["out of range", "i64::MIN"],
);
}
#[test]
fn rustscript_generic_runtime_cases_work() {
let cases = vec![
rustscript_runtime_case(
"generic passthrough functions support multiple concrete instantiations",
r#"
fn myfn<T>(v: T) {
let b = v;
b
}
let text = myfn::<string>("hello");
let number = myfn::<int>(41);
text;
number;
"#,
vec![Value::string("hello"), Value::Int(41)],
),
rustscript_runtime_case(
"generic function returns preserve instantiated struct schemas",
r#"
struct Box<T> { value: T }
fn myfn<T>(v: T) {
let b = v;
b
}
let input: Box<string> = { value: "hello" };
let boxed = myfn::<Box<string>>(input);
boxed.value;
"#,
vec![Value::string("hello")],
),
rustscript_runtime_case(
"host generic return schemas enable typed field access",
r#"
use json;
struct Stats { score: int }
struct Profile { stats: Stats }
let profile = json::decode::<Profile>("{\"stats\":{\"score\":41}}");
profile.stats.score + 1;
"#,
vec![Value::Int(42)],
),
];
run_runtime_cases(&cases);
}
#[test]
fn rustscript_generic_parse_errors_are_reported() {
let cases = vec![
rustscript_parse_error_case(
"generic functions currently require explicit type arguments",
r#"
fn myfn<T>(v: T) {
v
}
myfn("hello");
"#,
&["function 'myfn' expects 1 type arguments, got 0"],
),
rustscript_parse_error_case(
"non generic functions reject explicit type arguments",
r#"
fn plain(v) {
v
}
plain::<string>("hello");
"#,
&["function 'plain' does not accept explicit type arguments"],
),
rustscript_parse_error_case(
"host generic calls validate type argument arity",
r#"
use json;
json::decode::<int, string>("{}");
"#,
&["function 'json::decode' expects 1 type arguments, got 2"],
),
rustscript_parse_error_case(
"generic struct schemas validate type argument arity",
r#"
struct Box<T> { value: T }
let value: Box<string, int> = { value: "hello" };
value;
"#,
&["struct schema 'Box' expects 1 type arguments, got 2"],
),
rustscript_parse_error_case(
"duplicate function type parameters are rejected",
r#"
fn bad<T, T>(value: T) {
value
}
"#,
&["duplicate type parameter 'T' in function 'bad'"],
),
rustscript_parse_error_case(
"duplicate struct type parameters are rejected",
r#"
struct Bad<T, T> { value: T }
"#,
&["duplicate type parameter 'T' in struct 'Bad'"],
),
];
for case in &cases {
expect_parse_error_case(case);
}
}
#[test]
fn rustscript_generic_schema_errors_are_reported() {
let cases = vec![
SourceErrorCase {
name: "opaque generic parameters do not allow field access",
source: r#"
fn bad<T>(value: T) {
value.score
}
bad::<int>(1);
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &["cannot access fields on unresolved generic parameter 'T'"],
},
SourceErrorCase {
name: "generic struct instantiations enforce substituted field schemas",
source: r#"
struct Box<T> { value: T }
let boxed: Box<int> = { value: "oops" };
boxed.value;
"#,
flavor: SourceFlavor::RustScript,
expected_kind: SourceErrorKind::Compile(CompileErrorKind::InvalidFieldAccess),
expected_contains_all: &[
"field 'value' is declared as schema type 'int' but was assigned string",
],
},
];
run_source_error_cases(&cases);
}