252b725677
This commit introduces the `DefDestructure` bound kind and modifies the binder, analyzer, type checker, and VM to support destructuring in `def` statements. This allows for pattern matching on the right-hand side of a `def` to bind multiple variables. The parser has been updated to accept patterns in `def` statements. The binder now handles `UntypedKind::Def` with a `target` pattern, rather than a simple `name`. This enables destructuring. The `Gemini.md` documentation has been updated to include a new rule for incremental development.
340 lines
12 KiB
Rust
340 lines
12 KiB
Rust
#[cfg(test)]
|
|
mod tests {
|
|
use crate::ast::environment::Environment;
|
|
use crate::ast::nodes::UntypedKind;
|
|
use crate::ast::parser::Parser;
|
|
use crate::ast::types::Value;
|
|
|
|
#[test]
|
|
fn test_parse_integer_constant() {
|
|
let source = "123";
|
|
let mut parser = Parser::new(source).expect("Failed to create parser");
|
|
let ast = parser.parse_expression().expect("Failed to parse");
|
|
|
|
if let UntypedKind::Constant(Value::Int(val)) = ast.kind {
|
|
assert_eq!(val, 123);
|
|
} else {
|
|
panic!("Expected Integer constant, got {:?}", ast.kind);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_negative_integer() {
|
|
let source = "-42";
|
|
let mut parser = Parser::new(source).expect("Failed to create parser");
|
|
let ast = parser.parse_expression().expect("Failed to parse");
|
|
|
|
if let UntypedKind::Constant(Value::Int(val)) = ast.kind {
|
|
assert_eq!(val, -42);
|
|
} else {
|
|
panic!("Expected Integer constant, got {:?}", ast.kind);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_float_constant() {
|
|
let source = "123.45";
|
|
let mut parser = Parser::new(source).expect("Failed to create parser");
|
|
let ast = parser.parse_expression().expect("Failed to parse");
|
|
|
|
if let UntypedKind::Constant(Value::Float(val)) = ast.kind {
|
|
assert_eq!(val, 123.45);
|
|
} else {
|
|
panic!("Expected Float constant, got {:?}", ast.kind);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_negative_float() {
|
|
let source = "-10.5";
|
|
let mut parser = Parser::new(source).expect("Failed to create parser");
|
|
let ast = parser.parse_expression().expect("Failed to parse");
|
|
|
|
if let UntypedKind::Constant(Value::Float(val)) = ast.kind {
|
|
assert_eq!(val, -10.5);
|
|
} else {
|
|
panic!("Expected Float constant, got {:?}", ast.kind);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_closure_modification_from_source() {
|
|
let source = r#"
|
|
(do
|
|
(def x 10)
|
|
(def f (fn [] (assign x 20)))
|
|
(f)
|
|
x
|
|
)
|
|
"#;
|
|
|
|
let env = Environment::new();
|
|
let compiled = env.compile(source).expect("Failed to compile");
|
|
let linked = env.link(compiled);
|
|
let func = env.instantiate(linked);
|
|
let result: Result<Value, String> = Ok((func.func)(vec![]));
|
|
|
|
match result {
|
|
Ok(Value::Int(20)) => (),
|
|
Ok(val) => panic!("Expected Int(20), got {:?}", val),
|
|
Err(e) => panic!("VM Error: {}", e),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_examples() {
|
|
for opt in [false, true] {
|
|
let results = crate::utils::tester::run_functional_tests_with_optimization(opt);
|
|
for res in results {
|
|
assert!(
|
|
res.success,
|
|
"Example {} failed at opt {}: {}",
|
|
res.name, opt, res.message
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_debug_mode_logging() {
|
|
let mut env = Environment::new();
|
|
env.optimization = false;
|
|
let source = "(+ 10 20)";
|
|
let result = env.run_debug(source).expect("Failed to run debug");
|
|
|
|
let (val, logs) = result;
|
|
|
|
// 1. Check value
|
|
match val {
|
|
Ok(Value::Int(30)) => (),
|
|
_ => panic!("Expected Int(30), got {:?}", val),
|
|
}
|
|
|
|
// 2. Check logs (should have entries for + and constants)
|
|
assert!(!logs.is_empty(), "Logs should not be empty");
|
|
|
|
// Look for typical trace patterns
|
|
let has_call = logs.iter().any(|l| l.contains("CALL"));
|
|
let has_const = logs.iter().any(|l| l.contains("CONST(10)"));
|
|
let has_result = logs.iter().any(|l| l.contains("} -> 30"));
|
|
|
|
assert!(has_call, "Logs should contain CALL");
|
|
assert!(has_const, "Logs should contain CONST(10)");
|
|
assert!(has_result, "Logs should contain result 30");
|
|
}
|
|
|
|
#[test]
|
|
fn test_rtl_operators() {
|
|
let env = Environment::new();
|
|
|
|
// --- Arithmetic ---
|
|
assert_eq!(format!("{}", env.run_script("(+ 10 20)").unwrap()), "30");
|
|
assert_eq!(format!("{}", env.run_script("(- 20 10)").unwrap()), "10");
|
|
assert_eq!(format!("{}", env.run_script("(* 10 20)").unwrap()), "200");
|
|
assert_eq!(format!("{}", env.run_script("(/ 20 10)").unwrap()), "2");
|
|
assert_eq!(format!("{}", env.run_script("(// 20 3)").unwrap()), "6"); // 20 // 3 = 6
|
|
assert_eq!(format!("{}", env.run_script("(% 20 3)").unwrap()), "2"); // 20 % 3 = 2
|
|
|
|
// --- Logic / Bitwise ---
|
|
assert_eq!(
|
|
format!("{}", env.run_script("(and true false)").unwrap()),
|
|
"false"
|
|
);
|
|
assert_eq!(
|
|
format!("{}", env.run_script("(or true false)").unwrap()),
|
|
"true"
|
|
);
|
|
assert_eq!(
|
|
format!("{}", env.run_script("(xor true false)").unwrap()),
|
|
"true"
|
|
);
|
|
assert_eq!(
|
|
format!("{}", env.run_script("(not true)").unwrap()),
|
|
"false"
|
|
);
|
|
|
|
assert_eq!(format!("{}", env.run_script("(<< 1 2)").unwrap()), "4"); // 1 << 2 = 4
|
|
assert_eq!(format!("{}", env.run_script("(>> 4 1)").unwrap()), "2"); // 4 >> 1 = 2
|
|
assert_eq!(format!("{}", env.run_script("(and 3 1)").unwrap()), "1"); // 3 & 1 = 1
|
|
|
|
// --- Comparison ---
|
|
assert_eq!(format!("{}", env.run_script("(= 10 10)").unwrap()), "true");
|
|
assert_eq!(format!("{}", env.run_script("(= 10 20)").unwrap()), "false");
|
|
assert_eq!(format!("{}", env.run_script("(<> 10 20)").unwrap()), "true");
|
|
assert_eq!(format!("{}", env.run_script("(< 10 20)").unwrap()), "true");
|
|
assert_eq!(format!("{}", env.run_script("(> 10 20)").unwrap()), "false");
|
|
assert_eq!(format!("{}", env.run_script("(<= 10 10)").unwrap()), "true");
|
|
assert_eq!(format!("{}", env.run_script("(>= 10 10)").unwrap()), "true");
|
|
|
|
// --- NaN ---
|
|
assert_eq!(format!("{}", env.run_script("NaN").unwrap()), "NaN");
|
|
}
|
|
|
|
#[test]
|
|
fn test_random_isolation_between_environments() {
|
|
let env1 = Environment::new();
|
|
let env2 = Environment::new();
|
|
|
|
// 1. Seed env1
|
|
env1.run_script("(seed! 123)").unwrap();
|
|
let val1_a = env1.run_script("(random)").unwrap();
|
|
|
|
// 2. env2 should have its own default seed state
|
|
let val2_a = env2.run_script("(random)").unwrap();
|
|
|
|
// They are highly unlikely to be equal by default,
|
|
// and seeding env1 MUST not have seeded env2.
|
|
assert_ne!(
|
|
val1_a, val2_a,
|
|
"Environments must have isolated PRNG states"
|
|
);
|
|
|
|
// 3. Seed env2 differently
|
|
env2.run_script("(seed! 123)").unwrap();
|
|
let val2_b = env2.run_script("(random)").unwrap();
|
|
|
|
// After same seeding, they should match (isolated but identical seed)
|
|
assert_eq!(
|
|
val1_a, val2_b,
|
|
"Different environments with the same seed must produce the same sequence"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_random_seeding_determinism() {
|
|
let env = Environment::new();
|
|
|
|
// 1. First run with seed 42
|
|
env.run_script("(seed! 42)").unwrap();
|
|
let val1 = env.run_script("(random)").unwrap();
|
|
|
|
// 2. Second run with same seed 42
|
|
env.run_script("(seed! 42)").unwrap();
|
|
let val2 = env.run_script("(random)").unwrap();
|
|
|
|
assert_eq!(
|
|
val1, val2,
|
|
"Random results must be identical for the same seed"
|
|
);
|
|
|
|
// 3. Third run with different seed
|
|
env.run_script("(seed! 123)").unwrap();
|
|
let val3 = env.run_script("(random)").unwrap();
|
|
assert_ne!(val1, val3, "Random results must differ for different seeds");
|
|
}
|
|
|
|
#[test]
|
|
fn test_now_function_not_folded() {
|
|
let env = Environment::new();
|
|
let source = "(now)";
|
|
|
|
// 1. Check result type and value plausibility
|
|
let result = env.run_script(source).expect("Failed to run script");
|
|
if let Value::DateTime(ts) = result {
|
|
let current = chrono::Utc::now().timestamp_millis();
|
|
assert!(ts > 0);
|
|
assert!(ts <= current);
|
|
} else {
|
|
panic!("Expected DateTime, got {:?}", result);
|
|
}
|
|
|
|
// 2. Verify it's NOT constant folded in the AST dump
|
|
let dump = env.dump_ast(source).expect("Failed to dump AST");
|
|
assert!(
|
|
dump.contains("Call"),
|
|
"now() should remain a Call, not a Constant. Dump: \n{}",
|
|
dump
|
|
);
|
|
assert!(
|
|
!dump.contains("Constant: #"),
|
|
"now() should NOT be folded into a specific timestamp constant. Dump: \n{}",
|
|
dump
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_date_parsing() {
|
|
let env = Environment::new();
|
|
let res = env.run_script("(date \"2023-01-01\")").unwrap();
|
|
if let Value::DateTime(_) = res {
|
|
// OK
|
|
} else {
|
|
panic!("Expected DateTime, got {:?}", res);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "'again' is only allowed in tail position to avoid dead code.")]
|
|
fn test_again_non_tail_panic() {
|
|
let env = Environment::new();
|
|
let source = "(do (def f (fn [x] (do (again [(- x 1)]) x))) (f 5))";
|
|
// This will trigger the TCO pass which contains the validation logic
|
|
let _ = env.run_script(source);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dynamic_call_destructuring_underflow() {
|
|
let env = Environment::new();
|
|
let source = "(do
|
|
(def call-dynamic (fn [f data] (f data)))
|
|
(def data [10 [20 30]])
|
|
(def x (fn [[a [b c]]] (+ a (+ b c))))
|
|
(call-dynamic x data))";
|
|
|
|
let result = env.run_script(source);
|
|
if let Err(e) = &result {
|
|
panic!("Failed: {}", e);
|
|
}
|
|
assert_eq!(format!("{}", result.unwrap()), "60");
|
|
}
|
|
|
|
#[test]
|
|
fn test_nested_destructuring_optimization() {
|
|
let env = Environment::new();
|
|
|
|
// 1. Tuple-to-Tuple
|
|
let source_tuple = "((fn [[x y]] (+ x y)) [10 20])";
|
|
assert_eq!(format!("{}", env.run_script(source_tuple).unwrap()), "30");
|
|
let dump_tuple = env.dump_ast(source_tuple).unwrap();
|
|
assert!(
|
|
dump_tuple.contains("Constant: 30"),
|
|
"Nested tuple should be folded to 30. Dump:\n{}",
|
|
dump_tuple
|
|
);
|
|
|
|
// 2. Record-to-Tuple
|
|
let source_record = "((fn [[x y]] (+ x y)) {:a 5 :b 7})";
|
|
assert_eq!(format!("{}", env.run_script(source_record).unwrap()), "12");
|
|
let dump_record = env.dump_ast(source_record).unwrap();
|
|
assert!(
|
|
dump_record.contains("Constant: 12"),
|
|
"Record-to-Tuple destructuring should be folded to 12. Dump:\n{}",
|
|
dump_record
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_def_destructuring() {
|
|
let env = Environment::new();
|
|
|
|
// 1. Global destructuring
|
|
let source_global = "(do (def [a b] [1 2]) (+ a b))";
|
|
assert_eq!(format!("{}", env.run_script(source_global).unwrap()), "3");
|
|
|
|
// 2. Local nested destructuring inside a function
|
|
let source_local = "((fn [x] (do (def [a [[b c] d]] x) (+ a (+ b (+ c d))))) [1 [[2 3] 4]])";
|
|
assert_eq!(format!("{}", env.run_script(source_local).unwrap()), "10");
|
|
|
|
// 3. Verify 'def' returns the assigned value
|
|
let source_return = "(def [x y] [7 8])";
|
|
let res = env.run_script(source_return).unwrap();
|
|
if let Value::Tuple(vals) = res {
|
|
assert_eq!(vals.len(), 2);
|
|
assert_eq!(format!("{}", vals[0]), "7");
|
|
assert_eq!(format!("{}", vals[1]), "8");
|
|
} else {
|
|
panic!("Expected tuple return from def, got {:?}", res);
|
|
}
|
|
}
|
|
}
|