Files
aster/src/interpreter/tests.rs
T
2026-06-17 15:57:45 +08:00

1736 lines
47 KiB
Rust

use crate::interpreter::{Interpreter, Value};
use crate::lexer::Lexer;
use crate::parser::Parser;
/// Full pipeline: source → tokens → ast → interpret → last expression value.
/// Wraps in `let __result = <expr>;` so we can read the value back.
fn eval_expr(input: &str) -> Value {
let wrapped = format!("let __result = {};", input);
let (tokens, _) = Lexer::new(&wrapped).tokenize();
let mut parser = Parser::new(tokens);
let (stmts, errors) = parser.parse();
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
let mut interpreter = Interpreter::new();
interpreter.interpret(stmts).expect("Runtime error");
interpreter.env.borrow().get("__result").expect("No __result in env")
}
/// Full pipeline for multiple statements. Returns the interpreter for env inspection.
fn run(input: &str) -> Interpreter {
let (tokens, _) = Lexer::new(input).tokenize();
let mut parser = Parser::new(tokens);
let (stmts, errors) = parser.parse();
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
let mut interpreter = Interpreter::new();
interpreter.interpret(stmts).expect("Runtime error");
interpreter
}
/// Helper: get a variable from the interpreter's environment.
fn get_var(interp: &Interpreter, name: &str) -> Value {
interp.env.borrow().get(name).unwrap_or(Value::Nil)
}
/// Helper: assert a number value.
fn assert_num(val: &Value, expected: f64) {
match val {
Value::Number(n) => assert!((n - expected).abs() < f64::EPSILON,
"Expected {}, got {}", expected, n),
_ => panic!("Expected Number({}), got {:?}", expected, val),
}
}
/// Helper: assert a bool value.
fn assert_bool(val: &Value, expected: bool) {
match val {
Value::Bool(b) => assert_eq!(*b, expected),
_ => panic!("Expected Bool({}), got {:?}", expected, val),
}
}
/// Helper: assert nil.
fn assert_nil(val: &Value) {
match val {
Value::Nil => {}
_ => panic!("Expected Nil, got {:?}", val),
}
}
// =========================================================================
// Literals
// =========================================================================
#[test]
fn eval_number() {
assert_num(&eval_expr("42"), 42.0);
}
#[test]
fn eval_string() {
match eval_expr("\"hello\"") {
Value::String(s) => assert_eq!(s, "hello"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_true() {
assert_bool(&eval_expr("true"), true);
}
#[test]
fn eval_false() {
assert_bool(&eval_expr("false"), false);
}
#[test]
fn eval_nil() {
assert_nil(&eval_expr("nil"));
}
// =========================================================================
// Arithmetic
// =========================================================================
#[test]
fn eval_add() {
assert_num(&eval_expr("1 + 2"), 3.0);
}
#[test]
fn eval_sub() {
assert_num(&eval_expr("5 - 3"), 2.0);
}
#[test]
fn eval_mul() {
assert_num(&eval_expr("4 * 3"), 12.0);
}
#[test]
fn eval_div() {
assert_num(&eval_expr("10 / 4"), 2.5);
}
#[test]
fn eval_mod() {
assert_num(&eval_expr("7 % 3"), 1.0);
}
#[test]
fn eval_complex_arithmetic() {
assert_num(&eval_expr("1 + 2 * 3"), 7.0);
assert_num(&eval_expr("(1 + 2) * 3"), 9.0);
}
// =========================================================================
// String concatenation
// =========================================================================
#[test]
fn eval_string_concat() {
match eval_expr("\"hello \" + \"world\"") {
Value::String(s) => assert_eq!(s, "hello world"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_single_quoted_string() {
match eval_expr("'hello'") {
Value::String(s) => assert_eq!(s, "hello"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_mixed_quotes_concat() {
match eval_expr("\"a\" + 'b'") {
Value::String(s) => assert_eq!(s, "ab"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_single_quote_containing_double() {
match eval_expr("'he\"llo'") {
Value::String(s) => assert_eq!(s, "he\"llo"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_string_plus_number() {
match eval_expr("\"a\" + 42") {
Value::String(s) => assert_eq!(s, "a42"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_number_plus_string() {
match eval_expr("42 + \"a\"") {
Value::String(s) => assert_eq!(s, "42a"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_string_plus_bool() {
match eval_expr("\"t\" + true") {
Value::String(s) => assert_eq!(s, "ttrue"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_bool_plus_string() {
match eval_expr("false + \"x\"") {
Value::String(s) => assert_eq!(s, "falsex"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_string_plus_nil() {
match eval_expr("\"x\" + nil") {
Value::String(s) => assert_eq!(s, "xnil"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_plusequal_string_concat() {
let interp = run("let s = \"hello\"; s += \" world\"; s += 42;");
match get_var(&interp, "s") {
Value::String(s) => assert_eq!(s, "hello world42"),
v => panic!("Expected String, got {:?}", v),
}
}
// =========================================================================
// Comparison
// =========================================================================
#[test]
fn eval_equal_numbers() {
assert_bool(&eval_expr("1 == 1"), true);
assert_bool(&eval_expr("1 == 2"), false);
}
#[test]
fn eval_not_equal() {
assert_bool(&eval_expr("1 != 2"), true);
assert_bool(&eval_expr("1 != 1"), false);
}
#[test]
fn eval_greater_less() {
assert_bool(&eval_expr("5 > 3"), true);
assert_bool(&eval_expr("3 > 5"), false);
assert_bool(&eval_expr("5 >= 5"), true);
assert_bool(&eval_expr("3 < 5"), true);
assert_bool(&eval_expr("5 < 3"), false);
assert_bool(&eval_expr("3 <= 3"), true);
}
#[test]
fn eval_equal_strings() {
assert_bool(&eval_expr("\"a\" == \"a\""), true);
assert_bool(&eval_expr("\"a\" == \"b\""), false);
}
#[test]
fn eval_equal_bools() {
assert_bool(&eval_expr("true == true"), true);
assert_bool(&eval_expr("true == false"), false);
}
#[test]
fn eval_nil_equals_nil() {
assert_bool(&eval_expr("nil == nil"), true);
}
// =========================================================================
// Logical operators
// =========================================================================
#[test]
fn eval_logical_and_short_circuit() {
assert_bool(&eval_expr("true && true"), true);
assert_bool(&eval_expr("true && false"), false);
assert_bool(&eval_expr("false && true"), false);
// Short-circuit: false && <anything> returns false (the left value)
match eval_expr("false && 999") {
Value::Bool(false) => {}
v => panic!("Expected Bool(false), got {:?}", v),
}
}
#[test]
fn eval_logical_or_short_circuit() {
assert_bool(&eval_expr("true || false"), true);
assert_bool(&eval_expr("false || true"), true);
assert_bool(&eval_expr("false || false"), false);
// Short-circuit: true || <anything> returns true (the left value)
match eval_expr("true || 999") {
Value::Bool(true) => {}
v => panic!("Expected Bool(true), got {:?}", v),
}
}
// =========================================================================
// Ternary
// =========================================================================
#[test]
fn eval_ternary_true_condition() {
assert_num(&eval_expr("true ? 1 : 2"), 1.0);
}
#[test]
fn eval_ternary_false_condition() {
assert_num(&eval_expr("false ? 1 : 2"), 2.0);
}
#[test]
fn eval_ternary_truthy_condition() {
// Non-nil, non-false values are truthy
assert_num(&eval_expr("1 ? 42 : 99"), 42.0);
}
#[test]
fn eval_ternary_nil_condition() {
assert_num(&eval_expr("nil ? 1 : 2"), 2.0);
}
#[test]
fn eval_ternary_nested() {
let interp = run("let x = 5; let r = x > 3 ? (x > 10 ? 100 : 50) : 0;");
assert_num(&get_var(&interp, "r"), 50.0);
}
#[test]
fn eval_ternary_with_computed_branches() {
let interp = run("let x = 10; let r = x > 5 ? x * 2 : x / 2;");
assert_num(&get_var(&interp, "r"), 20.0);
}
#[test]
fn eval_ternary_short_circuit_then() {
// When condition is false, then_branch is not evaluated
let interp = run("let x = 0; let r = false ? (x = 999) : (x = 42);");
assert_num(&get_var(&interp, "x"), 42.0);
}
#[test]
fn eval_ternary_short_circuit_else() {
// When condition is true, else_branch is not evaluated
let interp = run("let x = 0; let r = true ? (x = 42) : (x = 999);");
assert_num(&get_var(&interp, "x"), 42.0);
}
// =========================================================================
// Unary
// =========================================================================
#[test]
fn eval_negation() {
assert_num(&eval_expr("-5"), -5.0);
assert_num(&eval_expr("--5"), 5.0);
}
#[test]
fn eval_not() {
assert_bool(&eval_expr("!true"), false);
assert_bool(&eval_expr("!false"), true);
assert_bool(&eval_expr("!nil"), true);
}
// =========================================================================
// Assignment and variables
// =========================================================================
#[test]
fn eval_variable_definition_and_use() {
let interp = run("let x = 10;");
assert_num(&get_var(&interp, "x"), 10.0);
}
#[test]
fn eval_simple_assignment() {
let interp = run("let x = 5; x = 10;");
assert_num(&get_var(&interp, "x"), 10.0);
}
#[test]
fn eval_compound_plus_equal() {
let interp = run("let x = 5; x += 3;");
assert_num(&get_var(&interp, "x"), 8.0);
}
#[test]
fn eval_compound_minus_equal() {
let interp = run("let x = 10; x -= 3;");
assert_num(&get_var(&interp, "x"), 7.0);
}
#[test]
fn eval_compound_star_equal() {
let interp = run("let x = 4; x *= 3;");
assert_num(&get_var(&interp, "x"), 12.0);
}
#[test]
fn eval_compound_slash_equal() {
let interp = run("let x = 10; x /= 2;");
assert_num(&get_var(&interp, "x"), 5.0);
}
#[test]
fn eval_compound_percent_equal() {
let interp = run("let x = 7; x %= 3;");
assert_num(&get_var(&interp, "x"), 1.0);
}
// =========================================================================
// Const
// =========================================================================
#[test]
fn eval_const_definition() {
let interp = run("const x = 42;");
assert_num(&get_var(&interp, "x"), 42.0);
}
#[test]
fn error_const_reassignment() {
let result = std::panic::catch_unwind(|| {
run("const x = 5; x = 10;");
});
assert!(result.is_err(), "Expected error for reassigning const");
}
#[test]
fn error_const_compound_assignment() {
let result = std::panic::catch_unwind(|| {
run("const x = 5; x += 1;");
});
assert!(result.is_err(), "Expected error for compound assignment to const");
}
#[test]
fn eval_const_shadowing() {
// Inner block can shadow outer const with its own binding
let interp = run("const x = 10; { let x = 20; }");
assert_num(&get_var(&interp, "x"), 10.0); // Outer unchanged
}
#[test]
fn eval_const_object_mutation() {
// const prevents rebinding, not property mutation
let interp = run("const obj = { a: 1 }; obj.a = 2;");
match get_var(&interp, "obj") {
Value::Object(obj) => {
let obj = obj.borrow();
assert_num(obj.get("a").unwrap(), 2.0);
}
v => panic!("Expected Object, got {:?}", v),
}
}
#[test]
fn eval_const_array_mutation() {
// const prevents rebinding, not index mutation
let interp = run("const arr = [1, 2]; arr[0] = 99;");
match get_var(&interp, "arr") {
Value::Array(arr) => {
assert_num(&arr.borrow()[0], 99.0);
}
v => panic!("Expected Array, got {:?}", v),
}
}
// =========================================================================
// Scope
// =========================================================================
#[test]
fn eval_block_scope_isolation() {
let interp = run("{ let x = 42; }");
let val = get_var(&interp, "x");
assert_nil(&val); // x should not exist outside the block
}
#[test]
fn eval_block_can_access_outer() {
let interp = run("let x = 10; { let y = x + 1; }");
assert_num(&get_var(&interp, "x"), 10.0);
}
#[test]
fn eval_shadowing() {
let interp = run("let x = 10; { let x = 20; }");
assert_num(&get_var(&interp, "x"), 10.0); // Outer x unchanged
}
#[test]
fn eval_assign_outer_from_inner() {
let interp = run("let x = 10; { x = 20; }");
assert_num(&get_var(&interp, "x"), 20.0);
}
// =========================================================================
// Control flow: if
// =========================================================================
#[test]
fn eval_if_true_branch() {
let interp = run("let x = 0; if (true) { x = 1; }");
assert_num(&get_var(&interp, "x"), 1.0);
}
#[test]
fn eval_if_false_branch_skipped() {
let interp = run("let x = 0; if (false) { x = 1; }");
assert_num(&get_var(&interp, "x"), 0.0);
}
#[test]
fn eval_if_else_true() {
let interp = run("let x = 0; if (true) { x = 1; } else { x = 2; }");
assert_num(&get_var(&interp, "x"), 1.0);
}
#[test]
fn eval_if_else_false() {
let interp = run("let x = 0; if (false) { x = 1; } else { x = 2; }");
assert_num(&get_var(&interp, "x"), 2.0);
}
#[test]
fn eval_if_truthy() {
// Non-nil, non-false values are truthy
let interp = run("let x = 0; if (1) { x = 42; }");
assert_num(&get_var(&interp, "x"), 42.0);
}
// =========================================================================
// Control flow: while
// =========================================================================
#[test]
fn eval_while_loop() {
let interp = run("let i = 0; while (i < 5) { i = i + 1; }");
assert_num(&get_var(&interp, "i"), 5.0);
}
#[test]
fn eval_while_never_runs() {
let interp = run("let x = 0; while (false) { x = 1; }");
assert_num(&get_var(&interp, "x"), 0.0);
}
// =========================================================================
// Control flow: for
// =========================================================================
#[test]
fn eval_for_loop() {
let interp = run("let sum = 0; for (let i = 0; i < 5; i = i + 1) { sum = sum + i; }");
assert_num(&get_var(&interp, "sum"), 10.0); // 0+1+2+3+4
}
#[test]
fn eval_for_empty_clauses() {
// Infinite loop with break — just test it parses and doesn't crash on one iter
let interp = run("let x = 0; for (;;) { x = 1; break; }");
assert_num(&get_var(&interp, "x"), 1.0);
}
// =========================================================================
// Control flow: break / continue
// =========================================================================
#[test]
fn eval_break() {
let interp = run("let i = 0; while (i < 100) { if (i == 5) { break; } i = i + 1; }");
assert_num(&get_var(&interp, "i"), 5.0);
}
#[test]
fn eval_continue() {
let interp = run("let sum = 0; let j = 0; while (j < 5) { j = j + 1; if (j == 3) { continue; } sum = sum + j; }");
assert_num(&get_var(&interp, "sum"), 12.0); // 1+2+4+5 = 12 (skips 3)
}
#[test]
fn eval_break_in_for() {
let interp = run("let found = -1; for (let k = 0; k < 100; k = k + 1) { if (k * k > 50) { found = k; break; } }");
assert_num(&get_var(&interp, "found"), 8.0); // 8*8=64 > 50
}
// =========================================================================
// Functions
// =========================================================================
#[test]
fn eval_function_call() {
let interp = run("fn add(a, b) { return a + b; } let result = add(3, 4);");
assert_num(&get_var(&interp, "result"), 7.0);
}
#[test]
fn eval_function_no_return() {
let interp = run("fn foo() { 42; } let result = foo();");
assert_nil(&get_var(&interp, "result")); // No explicit return → nil
}
#[test]
fn eval_recursion() {
let interp = run("fn fib(n) { if (n <= 1) { return n; } return fib(n-1) + fib(n-2); } let f = fib(10);");
assert_num(&get_var(&interp, "f"), 55.0);
}
#[test]
fn eval_nested_function_calls() {
let interp = run("fn double(x) { return x * 2; } let r = double(double(3));");
assert_num(&get_var(&interp, "r"), 12.0);
}
// =========================================================================
// Closures
// =========================================================================
#[test]
fn eval_simple_closure() {
let interp = run("
fn make_adder(x) { return fn(y) { return x + y; }; }
let add10 = make_adder(10);
let r = add10(5);
");
assert_num(&get_var(&interp, "r"), 15.0);
}
#[test]
fn eval_closure_captures_by_value_semantics() {
// Each call to make_adder creates an independent closure
let interp = run("
fn make_adder(x) { return fn(y) { return x + y; }; }
let add5 = make_adder(5);
let add10 = make_adder(10);
let r1 = add5(0);
let r2 = add10(0);
");
assert_num(&get_var(&interp, "r1"), 5.0);
assert_num(&get_var(&interp, "r2"), 10.0);
}
// =========================================================================
// Arrays
// =========================================================================
#[test]
fn eval_array_creation() {
match eval_expr("[1, 2, 3]") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 3);
assert_num(&arr[0], 1.0);
assert_num(&arr[1], 2.0);
assert_num(&arr[2], 3.0);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_empty_array() {
match eval_expr("[]") {
Value::Array(arr) => assert_eq!(arr.borrow().len(), 0),
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_array_index_access() {
let interp = run("let arr = [10, 20, 30]; let x = arr[0]; let y = arr[2];");
assert_num(&get_var(&interp, "x"), 10.0);
assert_num(&get_var(&interp, "y"), 30.0);
}
#[test]
fn eval_array_index_assignment() {
let interp = run("let arr = [1, 2, 3]; arr[0] = 99;");
match get_var(&interp, "arr") {
Value::Array(arr) => assert_num(&arr.borrow()[0], 99.0),
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_array_compound_index_assignment() {
let interp = run("let arr = [1, 2, 3]; arr[0] += 10;");
match get_var(&interp, "arr") {
Value::Array(arr) => assert_num(&arr.borrow()[0], 11.0),
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_nested_arrays() {
let interp = run("let arr = [[1, 2], [3, 4]]; let x = arr[1][0];");
assert_num(&get_var(&interp, "x"), 3.0);
}
#[test]
fn eval_array_equality() {
assert_bool(&eval_expr("[1, 2, 3] == [1, 2, 3]"), true);
assert_bool(&eval_expr("[1, 2] == [3, 4]"), false);
assert_bool(&eval_expr("[1] == [1, 2]"), false);
}
// =========================================================================
// Object equality
// =========================================================================
#[test]
fn eval_object_equality_same() {
let interp = run("let a = {x: 1, y: 2}; let b = {x: 1, y: 2}; let r = a == b;");
assert_bool(&get_var(&interp, "r"), true);
}
#[test]
fn eval_object_equality_different_values() {
let interp = run("let a = {x: 1}; let b = {x: 2}; let r = a == b;");
assert_bool(&get_var(&interp, "r"), false);
}
#[test]
fn eval_object_equality_different_keys() {
let interp = run("let a = {x: 1}; let b = {y: 1}; let r = a == b;");
assert_bool(&get_var(&interp, "r"), false);
}
#[test]
fn eval_object_equality_different_size() {
let interp = run("let a = {x: 1}; let b = {x: 1, y: 2}; let r = a == b;");
assert_bool(&get_var(&interp, "r"), false);
}
#[test]
fn eval_object_equality_empty() {
let interp = run("let a = {}; let b = {}; let r = a == b;");
assert_bool(&get_var(&interp, "r"), true);
}
// =========================================================================
// Strings as indexable
// =========================================================================
#[test]
fn eval_string_index() {
match eval_expr("\"hello\"[0]") {
Value::String(s) => assert_eq!(s, "h"),
v => panic!("Expected String for 'h', got {:?}", v),
}
}
#[test]
fn eval_string_index_last() {
match eval_expr("\"abc\"[2]") {
Value::String(s) => assert_eq!(s, "c"),
v => panic!("Expected String for 'c', got {:?}", v),
}
}
// =========================================================================
// Objects
// =========================================================================
#[test]
fn eval_object_creation() {
match eval_expr("{ name: \"aster\", version: 1 }") {
Value::Object(obj) => {
let obj = obj.borrow();
match obj.get("name") {
Some(Value::String(s)) => assert_eq!(s, "aster"),
v => panic!("Expected String 'aster', got {:?}", v),
}
match obj.get("version") {
Some(Value::Number(n)) => assert!((n - 1.0).abs() < f64::EPSILON),
v => panic!("Expected Number 1, got {:?}", v),
}
}
v => panic!("Expected Object, got {:?}", v),
}
}
#[test]
fn eval_object_property_access() {
let interp = run("let obj = { name: \"aster\" }; let n = obj.name;");
match get_var(&interp, "n") {
Value::String(s) => assert_eq!(s, "aster"),
v => panic!("Expected String 'aster', got {:?}", v),
}
}
#[test]
fn eval_object_property_assignment() {
let interp = run("let obj = { name: \"old\" }; obj.name = \"new\";");
match get_var(&interp, "obj") {
Value::Object(obj) => {
match obj.borrow().get("name") {
Some(Value::String(s)) => assert_eq!(s, "new"),
v => panic!("Expected String 'new', got {:?}", v),
}
}
v => panic!("Expected Object, got {:?}", v),
}
}
#[test]
fn eval_object_compound_property_assignment() {
let interp = run("let obj = { age: 26 }; obj.age -= 2;");
match get_var(&interp, "obj") {
Value::Object(obj) => {
assert_num(obj.borrow().get("age").unwrap(), 24.0);
}
v => panic!("Expected Object, got {:?}", v),
}
}
// =========================================================================
// Built-in functions
// =========================================================================
#[test]
fn eval_builtin_clock() {
// clock() returns a number (Unix timestamp in seconds)
match eval_expr("clock()") {
Value::Number(n) => assert!(n > 0.0, "clock should return positive number"),
v => panic!("Expected Number from clock(), got {:?}", v),
}
}
#[test]
fn eval_builtin_os_clock() {
match eval_expr("os.clock()") {
Value::Number(n) => assert!(n > 0.0),
v => panic!("Expected Number from os.clock(), got {:?}", v),
}
}
#[test]
fn eval_print_returns_nil() {
// print() returns nil
assert_nil(&eval_expr("print(\"test\")"));
}
// =========================================================================
// Runtime errors
// =========================================================================
#[test]
fn error_undefined_variable() {
let result = std::panic::catch_unwind(|| {
run("let x = y;");
});
// run() asserts no errors, so this should panic
assert!(result.is_err(), "Expected error for undefined variable");
}
#[test]
fn error_division_by_zero() {
let result = std::panic::catch_unwind(|| {
eval_expr("1 / 0");
});
assert!(result.is_err(), "Expected error for division by zero");
}
#[test]
fn error_modulo_by_zero() {
let result = std::panic::catch_unwind(|| {
eval_expr("1 % 0");
});
assert!(result.is_err(), "Expected error for modulo by zero");
}
#[test]
fn error_type_mismatch_negate_string() {
let result = std::panic::catch_unwind(|| {
eval_expr("-\"hello\"");
});
assert!(result.is_err(), "Expected error for negating a string");
}
#[test]
fn eval_number_plus_string_concatenates() {
match eval_expr("1 + \"hello\"") {
Value::String(s) => assert_eq!(s, "1hello"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn error_call_non_function() {
let result = std::panic::catch_unwind(|| {
eval_expr("42()");
});
assert!(result.is_err(), "Expected error for calling non-function");
}
#[test]
fn error_index_out_of_bounds() {
let result = std::panic::catch_unwind(|| {
eval_expr("[1, 2][5]");
});
assert!(result.is_err(), "Expected error for index out of bounds");
}
#[test]
fn error_property_on_non_object() {
let result = std::panic::catch_unwind(|| {
eval_expr("42.name");
});
assert!(result.is_err(), "Expected error for property on non-object");
}
#[test]
fn error_index_on_non_array() {
let result = std::panic::catch_unwind(|| {
eval_expr("42[0]");
});
assert!(result.is_err(), "Expected error for index on non-array");
}
// =========================================================================
// Object index access: obj[key]
// =========================================================================
#[test]
fn eval_object_index_get() {
assert_num(&eval_expr("{a: 42}[\"a\"]"), 42.0);
}
#[test]
fn eval_object_index_get_nested() {
assert_num(&eval_expr("{inner: {val: 7}}[\"inner\"][\"val\"]"), 7.0);
}
#[test]
fn eval_object_index_get_variable_key() {
let interp = run("let obj = {x: 10, y: 20}; let k = \"y\"; let result = obj[k];");
assert_num(&get_var(&interp, "result"), 20.0);
}
#[test]
fn eval_object_index_get_undefined() {
let result = std::panic::catch_unwind(|| {
eval_expr("{}[\"x\"]");
});
assert!(result.is_err(), "Expected error for undefined property");
}
#[test]
fn eval_object_index_set_existing() {
let interp = run("let obj = {a: 1}; obj[\"a\"] = 99;");
match get_var(&interp, "obj") {
Value::Object(map) => {
let map = map.borrow();
assert_num(map.get("a").unwrap(), 99.0);
}
v => panic!("Expected Object, got {:?}", v),
}
}
#[test]
fn eval_object_index_set_new_key() {
let interp = run("let obj = {}; obj[\"name\"] = \"Aster\";");
match get_var(&interp, "obj") {
Value::Object(map) => {
let map = map.borrow();
match map.get("name").unwrap() {
Value::String(s) => assert_eq!(s, "Aster"),
v => panic!("Expected String, got {:?}", v),
}
}
v => panic!("Expected Object, got {:?}", v),
}
}
#[test]
fn eval_object_index_compound_assign() {
let interp = run("let obj = {count: 10}; obj[\"count\"] += 5;");
match get_var(&interp, "obj") {
Value::Object(map) => {
let map = map.borrow();
assert_num(map.get("count").unwrap(), 15.0);
}
v => panic!("Expected Object, got {:?}", v),
}
}
#[test]
fn error_object_index_non_string() {
let result = std::panic::catch_unwind(|| {
eval_expr("{a: 1}[42]");
});
assert!(result.is_err(), "Expected error for non-string object index");
}
#[test]
fn error_object_index_set_non_string() {
let result = std::panic::catch_unwind(|| {
eval_expr("let obj = {a: 1}; obj[true] = 2;");
});
assert!(result.is_err(), "Expected error for non-string object index set");
}
// =========================================================================
// Builtins: len
// =========================================================================
#[test]
fn eval_len_string() {
assert_num(&eval_expr("len(\"hello\")"), 5.0);
}
#[test]
fn eval_len_empty_string() {
assert_num(&eval_expr("len(\"\")"), 0.0);
}
#[test]
fn eval_len_array() {
assert_num(&eval_expr("len([1, 2, 3])"), 3.0);
}
#[test]
fn eval_len_empty_array() {
assert_num(&eval_expr("len([])"), 0.0);
}
#[test]
fn eval_len_object() {
assert_num(&eval_expr("len({a: 1, b: 2})"), 2.0);
}
#[test]
fn eval_len_empty_object() {
assert_num(&eval_expr("len({})"), 0.0);
}
#[test]
fn error_len_no_args() {
let result = std::panic::catch_unwind(|| {
eval_expr("len()");
});
assert!(result.is_err(), "Expected error for len() with no args");
}
#[test]
fn error_len_number() {
let result = std::panic::catch_unwind(|| {
eval_expr("len(42)");
});
assert!(result.is_err(), "Expected error for len(42)");
}
// =========================================================================
// Builtins: typeof
// =========================================================================
#[test]
fn eval_typeof_number() {
match eval_expr("typeof(42)") {
Value::String(s) => assert_eq!(s, "number"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_typeof_string() {
match eval_expr("typeof(\"hello\")") {
Value::String(s) => assert_eq!(s, "string"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_typeof_bool() {
match eval_expr("typeof(true)") {
Value::String(s) => assert_eq!(s, "bool"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_typeof_nil() {
match eval_expr("typeof(nil)") {
Value::String(s) => assert_eq!(s, "nil"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_typeof_object() {
match eval_expr("typeof({a: 1})") {
Value::String(s) => assert_eq!(s, "object"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_typeof_array() {
match eval_expr("typeof([1, 2])") {
Value::String(s) => assert_eq!(s, "array"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_typeof_function() {
let interp = run("fn f() {}");
// f is a variable, not an expression — use get_var
match get_var(&interp, "f") {
Value::Function(_) => {} // just verify it's a function
v => panic!("Expected Function, got {:?}", v),
}
}
// =========================================================================
// Builtins: push / pop
// =========================================================================
#[test]
fn eval_push_returns_value() {
assert_num(&eval_expr("push([1, 2], 3)"), 3.0);
}
#[test]
fn eval_push_modifies_array() {
let interp = run("let arr = [1, 2]; push(arr, 3);");
match get_var(&interp, "arr") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 3);
assert_num(&arr[0], 1.0);
assert_num(&arr[1], 2.0);
assert_num(&arr[2], 3.0);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_push_string_to_array() {
let interp = run("let arr = [\"a\"]; push(arr, \"b\");");
match get_var(&interp, "arr") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 2);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_pop_returns_last() {
assert_num(&eval_expr("pop([1, 2, 3])"), 3.0);
}
#[test]
fn eval_pop_modifies_array() {
let interp = run("let arr = [1, 2, 3]; pop(arr);");
match get_var(&interp, "arr") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 2);
assert_num(&arr[0], 1.0);
assert_num(&arr[1], 2.0);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn error_pop_empty_array() {
let result = std::panic::catch_unwind(|| {
eval_expr("pop([])");
});
assert!(result.is_err(), "Expected error for pop([])");
}
#[test]
fn error_push_non_array() {
let result = std::panic::catch_unwind(|| {
eval_expr("push(42, 1)");
});
assert!(result.is_err(), "Expected error for push on non-array");
}
#[test]
fn error_pop_non_array() {
let result = std::panic::catch_unwind(|| {
eval_expr("pop(42)");
});
assert!(result.is_err(), "Expected error for pop on non-array");
}
// =========================================================================
// For-In loops
// =========================================================================
#[test]
fn eval_forin_array_elements() {
// Sum elements via for-in
let interp = run("let sum = 0; let arr = [1, 2, 3]; for (x in arr) { sum = sum + x; }");
assert_num(&get_var(&interp, "sum"), 6.0);
}
#[test]
fn eval_forin_empty_array() {
let interp = run("let count = 0; let arr = []; for (x in arr) { count = count + 1; }");
assert_num(&get_var(&interp, "count"), 0.0);
}
#[test]
fn eval_forin_object_keys() {
let interp = run("let keys = \"\"; let obj = {a: 1, b: 2}; for (k in obj) { keys = keys + k; }");
match get_var(&interp, "keys") {
Value::String(s) => {
// Object iteration order is not guaranteed, so check length
assert_eq!(s.len(), 2);
assert!(s.contains('a'));
assert!(s.contains('b'));
}
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_forin_string_chars() {
let interp = run("let result = \"\"; for (c in \"ab\") { result = result + c; }");
match get_var(&interp, "result") {
Value::String(s) => assert_eq!(s, "ab"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_forin_break() {
let interp = run("let sum = 0; for (x in [1, 2, 3, 4]) { if (x == 3) { break; } sum = sum + x; }");
assert_num(&get_var(&interp, "sum"), 3.0); // 1 + 2
}
#[test]
fn eval_forin_continue() {
let interp = run("let sum = 0; for (x in [1, 2, 3]) { if (x == 2) { continue; } sum = sum + x; }");
assert_num(&get_var(&interp, "sum"), 4.0); // 1 + 3
}
#[test]
fn eval_forin_loop_var_doesnt_leak() {
// Loop variable should not be accessible outside the loop
let interp = run("for (x in [1]) { let _ = x; }");
// x should not exist in the outer scope
match get_var(&interp, "x") {
Value::Nil => {} // Expected: x is not defined
v => panic!("Expected Nil (undefined), got {:?}", v),
}
}
#[test]
fn eval_forin_with_object_array() {
// Iterate over an array of objects
let interp = run("
let arr = [{v: 10}, {v: 20}];
let total = 0;
for (obj in arr) { total = total + obj.v; }
");
assert_num(&get_var(&interp, "total"), 30.0);
}
#[test]
fn error_forin_non_iterable() {
let result = std::panic::catch_unwind(|| {
run("for (x in 42) { let _ = x; }"); // Can't iterate a number
});
assert!(result.is_err(), "Expected error for for-in on non-iterable");
}
// =========================================================================
// String builtins
// =========================================================================
#[test]
fn eval_split_by_comma() {
match eval_expr("split(\"a,b,c\", \",\")") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 3);
match &arr[0] { Value::String(s) => assert_eq!(s, "a"), v => panic!("{:?}", v) }
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_split_by_empty_delim_returns_chars() {
match eval_expr("split(\"ab\", \"\")") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 2);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_split_no_match_returns_whole_string() {
match eval_expr("split(\"hello\", \",\")") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 1);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_trim_spaces() {
match eval_expr("trim(\" hello \")") {
Value::String(s) => assert_eq!(s, "hello"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_trim_no_whitespace() {
match eval_expr("trim(\"hello\")") {
Value::String(s) => assert_eq!(s, "hello"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_substring_start() {
match eval_expr("substring(\"hello\", 1)") {
Value::String(s) => assert_eq!(s, "ello"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_substring_start_and_length() {
match eval_expr("substring(\"hello\", 1, 3)") {
Value::String(s) => assert_eq!(s, "ell"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_substring_start_beyond_length() {
match eval_expr("substring(\"hi\", 10)") {
Value::String(s) => assert_eq!(s, ""),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_replace_single() {
match eval_expr("replace(\"hello world\", \"world\", \"aster\")") {
Value::String(s) => assert_eq!(s, "hello aster"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_replace_multiple() {
match eval_expr("replace(\"a,a,a\", \"a\", \"b\")") {
Value::String(s) => assert_eq!(s, "b,b,b"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_replace_no_match() {
match eval_expr("replace(\"hello\", \"x\", \"y\")") {
Value::String(s) => assert_eq!(s, "hello"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_contains_true() {
assert_bool(&eval_expr("contains(\"hello\", \"ell\")"), true);
}
#[test]
fn eval_contains_false() {
assert_bool(&eval_expr("contains(\"hello\", \"xyz\")"), false);
}
#[test]
fn eval_upper() {
match eval_expr("upper(\"hello\")") {
Value::String(s) => assert_eq!(s, "HELLO"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_lower() {
match eval_expr("lower(\"HELLO\")") {
Value::String(s) => assert_eq!(s, "hello"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_starts_with_true() {
assert_bool(&eval_expr("starts_with(\"hello\", \"he\")"), true);
}
#[test]
fn eval_starts_with_false() {
assert_bool(&eval_expr("starts_with(\"hello\", \"lo\")"), false);
}
#[test]
fn eval_ends_with_true() {
assert_bool(&eval_expr("ends_with(\"hello\", \"lo\")"), true);
}
#[test]
fn eval_ends_with_false() {
assert_bool(&eval_expr("ends_with(\"hello\", \"he\")"), false);
}
#[test]
fn error_split_wrong_arg_count() {
let result = std::panic::catch_unwind(|| { eval_expr("split(\"a\")"); });
assert!(result.is_err(), "Expected error for split with 1 arg");
}
#[test]
fn error_trim_non_string() {
let result = std::panic::catch_unwind(|| { eval_expr("trim(42)"); });
assert!(result.is_err(), "Expected error for trim(42)");
}
// =========================================================================
// Array properties & methods (dot notation)
// =========================================================================
#[test]
fn eval_array_length_property() {
assert_num(&eval_expr("[1, 2, 3].length"), 3.0);
}
#[test]
fn eval_empty_array_length() {
assert_num(&eval_expr("[].length"), 0.0);
}
#[test]
fn eval_array_length_via_variable() {
let interp = run("let arr = [10, 20, 30, 40]; let len = arr.length;");
assert_num(&get_var(&interp, "len"), 4.0);
}
#[test]
fn eval_array_push_method_returns_value() {
let interp = run("let arr = [1, 2]; let result = arr.push(3);");
assert_num(&get_var(&interp, "result"), 3.0);
}
#[test]
fn eval_array_push_method_modifies_array() {
let interp = run("let arr = [1, 2]; arr.push(3);");
match get_var(&interp, "arr") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 3);
assert_num(&arr[2], 3.0);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_array_push_method_chained() {
// push returns the pushed value, so chaining .push().push() won't work
// but push returns the pushed value, verify that independently
let interp = run("let arr = []; let a = arr.push(1); let b = arr.push(2); let c = arr.push(3);");
assert_num(&get_var(&interp, "a"), 1.0);
assert_num(&get_var(&interp, "b"), 2.0);
assert_num(&get_var(&interp, "c"), 3.0);
match get_var(&interp, "arr") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 3);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_array_pop_method_returns_last() {
assert_num(&eval_expr("[1, 2, 3].pop()"), 3.0);
}
#[test]
fn eval_array_pop_method_modifies_array() {
let interp = run("let arr = [1, 2, 3]; let popped = arr.pop();");
assert_num(&get_var(&interp, "popped"), 3.0);
match get_var(&interp, "arr") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 2);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn error_array_pop_empty() {
let result = std::panic::catch_unwind(|| {
eval_expr("[].pop()");
});
assert!(result.is_err(), "Expected error for pop() on empty array");
}
#[test]
fn error_array_unknown_property() {
let result = std::panic::catch_unwind(|| {
eval_expr("[1, 2].foo");
});
assert!(result.is_err(), "Expected error for unknown array property");
}
#[test]
fn error_non_array_push_method() {
let result = std::panic::catch_unwind(|| {
eval_expr("42.push(1)");
});
assert!(result.is_err(), "Expected error for .push() on non-array");
}
// =========================================================================
// String properties & methods (dot notation)
// =========================================================================
#[test]
fn eval_string_length_property() {
assert_num(&eval_expr("\"hello\".length"), 5.0);
}
#[test]
fn eval_empty_string_length() {
assert_num(&eval_expr("\"\".length"), 0.0);
}
#[test]
fn eval_string_length_via_variable() {
let interp = run("let s = \"hi there\"; let len = s.length;");
assert_num(&get_var(&interp, "len"), 8.0);
}
#[test]
fn eval_string_upper_method() {
match eval_expr("\"hello\".upper()") {
Value::String(s) => assert_eq!(s, "HELLO"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_string_lower_method() {
match eval_expr("\"HELLO\".lower()") {
Value::String(s) => assert_eq!(s, "hello"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_string_trim_method() {
match eval_expr("\" hi \".trim()") {
Value::String(s) => assert_eq!(s, "hi"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_string_contains_method_true() {
assert_bool(&eval_expr("\"hello\".contains(\"ell\")"), true);
}
#[test]
fn eval_string_contains_method_false() {
assert_bool(&eval_expr("\"hello\".contains(\"xyz\")"), false);
}
#[test]
fn eval_string_starts_with_method() {
assert_bool(&eval_expr("\"hello\".starts_with(\"he\")"), true);
assert_bool(&eval_expr("\"hello\".starts_with(\"lo\")"), false);
}
#[test]
fn eval_string_ends_with_method() {
assert_bool(&eval_expr("\"hello\".ends_with(\"lo\")"), true);
assert_bool(&eval_expr("\"hello\".ends_with(\"he\")"), false);
}
#[test]
fn eval_string_substring_method() {
match eval_expr("\"hello\".substring(1, 3)") {
Value::String(s) => assert_eq!(s, "ell"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_string_replace_method() {
match eval_expr("\"hello world\".replace(\"world\", \"aster\")") {
Value::String(s) => assert_eq!(s, "hello aster"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_string_split_method() {
match eval_expr("\"a,b,c\".split(\",\")") {
Value::Array(arr) => {
let arr = arr.borrow();
assert_eq!(arr.len(), 3);
}
v => panic!("Expected Array, got {:?}", v),
}
}
#[test]
fn eval_string_method_chaining() {
match eval_expr("\" hello \".trim().upper()") {
Value::String(s) => assert_eq!(s, "HELLO"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn error_string_unknown_property() {
let result = std::panic::catch_unwind(|| {
eval_expr("\"hi\".foo");
});
assert!(result.is_err(), "Expected error for unknown string property");
}
// =========================================================================
// Module system (require)
// =========================================================================
#[test]
fn eval_require_math_module() {
let interp = run("let math = require(\"tests/fixtures/math.ast\");");
let math = get_var(&interp, "math");
match math {
Value::Object(obj) => {
let obj = obj.borrow();
assert!(obj.contains_key("add"), "math should contain 'add'");
assert!(obj.contains_key("mul"), "math should contain 'mul'");
assert!(obj.contains_key("version"), "math should contain 'version'");
}
v => panic!("Expected Object, got {:?}", v),
}
}
#[test]
fn eval_require_call_exported_function() {
let interp = run(r#"
let math = require("tests/fixtures/math.ast");
let result = math.add(10, 32);
"#);
assert_num(&get_var(&interp, "result"), 42.0);
}
#[test]
fn eval_require_cache_returns_same_object() {
let interp = run(r#"
let a = require("tests/fixtures/math.ast");
let b = require("tests/fixtures/math.ast");
let same = a == b;
"#);
assert_bool(&get_var(&interp, "same"), true);
}
#[test]
fn eval_require_module_isolation() {
// 模块看不到调用者的变量,但模块内部定义可以正常工作
let interp = run(r#"
let secret = 99;
let g = require("tests/fixtures/greet.ast");
let msg = g.greet("Aster");
"#);
match get_var(&interp, "msg") {
Value::String(s) => assert_eq!(s, "Hello!"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_require_module_with_builtins() {
// 模块内部可以使用内置函数(因为 builtins_env 是模块 env 的祖先)
let interp = run(r#"
let m = require("tests/fixtures/math.ast");
let r = m.add(5, 7);
"#);
assert_num(&get_var(&interp, "r"), 12.0);
}
#[test]
fn eval_require_error_file_not_found() {
let result = std::panic::catch_unwind(|| {
run("let x = require(\"nonexistent_file_xyz.ast\");");
});
assert!(result.is_err(), "Expected error for missing module file");
}
#[test]
fn eval_require_error_no_args() {
let result = std::panic::catch_unwind(|| {
run("let x = require();");
});
assert!(result.is_err(), "Expected error for require() with no args");
}
#[test]
fn eval_require_error_non_string_arg() {
let result = std::panic::catch_unwind(|| {
run("let x = require(42);");
});
assert!(result.is_err(), "Expected error for require() with non-string arg");
}
#[test]
fn eval_require_error_broken_syntax() {
let result = std::panic::catch_unwind(|| {
run("let x = require(\"tests/fixtures/broken_syntax.ast\");");
});
assert!(result.is_err(), "Expected error for module with syntax error");
}
#[test]
fn eval_require_empty_module() {
// 空模块应该返回空对象
let interp = run("let empty = require(\"tests/fixtures/sub/mod.ast\");");
match get_var(&interp, "empty") {
Value::Object(obj) => {
let obj = obj.borrow();
assert!(obj.contains_key("sub"), "empty module should contain 'sub'");
}
v => panic!("Expected Object, got {:?}", v),
}
}