Files
RustAst/src/integration_test.rs
T
Michael Schimmel a4af142719 Refactor random number generation to factory
- Use a `make-random` factory to create isolated random number
  generators.
- Remove the global `prng` from the `Environment`.
- Ensure `make-random` can be called with or without a seed.
2026-03-02 14:01:36 +01:00

622 lines
22 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. Create a seeded generator in env1
env1.run_script("(def rand (make-random 123))").unwrap();
let val1_a = env1.run_script("(rand)").unwrap();
// 2. env2 should have its own default seed state for its generators
env2.run_script("(def rand (make-random))").unwrap();
let val2_a = env2.run_script("(rand)").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. Create another generator in env2 with the same seed
env2.run_script("(def rand-same (make-random 123))").unwrap();
let val2_b = env2.run_script("(rand-same)").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("(def rand1 (make-random 42))").unwrap();
let val1 = env.run_script("(rand1)").unwrap();
// 2. Second run with same seed 42
env.run_script("(def rand2 (make-random 42))").unwrap();
let val2 = env.run_script("(rand2)").unwrap();
assert_eq!(
val1, val2,
"Random results must be identical for the same seed"
);
// 3. Third run with different seed
env.run_script("(def rand3 (make-random 123))").unwrap();
let val3 = env.run_script("(rand3)").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
);
}
#[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);
}
}
#[test]
fn test_assign_destructuring() {
// 1. Simple assignment destructuring
{
let env = Environment::new();
let source_simple = "(do (def a 0) (def b 0) (assign [a b] [10 20]) (+ a b))";
assert_eq!(format!("{}", env.run_script(source_simple).unwrap()), "30");
}
// 2. Nested assignment destructuring
{
let env = Environment::new();
let source_nested =
"(do (def a 0) (def b 0) (def c 0) (assign [a [b c]] [1 [2 3]]) (+ a (+ b c)))";
assert_eq!(format!("{}", env.run_script(source_nested).unwrap()), "6");
}
// 3. Assignment returns the assigned value
{
let env = Environment::new();
let source_return = "(do (def a 0) (def b 0) (assign [a b] [5 6]))";
let res = env.run_script(source_return).unwrap();
if let Value::Tuple(vals) = res {
assert_eq!(vals.len(), 2);
assert_eq!(format!("{}", vals[0]), "5");
assert_eq!(format!("{}", vals[1]), "6");
} else {
panic!("Expected tuple return from assign, got {:?}", res);
}
}
}
#[test]
fn test_pipeline_optional_type() {
let env = Environment::new();
// The lambda uses an `if` without an `else` returning a float constant.
// The TypeChecker should deduce `Optional(Float)` for the lambda body,
// and correctly unwrap it to `Series(Float)` for the pipeline output.
let source = "(do
(def src (create-random-ohlc 42 10))
(def filtered
(pipe [src]
(fn [tick]
(if true
42.0
)
)
)
)
filtered
)";
let res = env.run_script(source);
if let Err(e) = &res {
panic!("Script failed to compile/run: {:?}", e);
}
let val = res.unwrap();
if let crate::ast::types::Value::Object(obj) = val {
assert_eq!(obj.type_name(), "PipelineNode");
} else {
panic!("Expected an Object(PipelineNode)");
}
}
#[test]
fn test_multi_level_destructuring() {
let env = Environment::new();
let source = "(do
(def process_data (fn [conf]
(do
(def [str s] conf)
(def [f ss] s)
[\"Symbol:\" str \"field:\" f \"id:\" ss]
)
)
)
(process_data [\"btc\" [:close \"cls\"]]))";
let res = env.run_script(source).unwrap();
assert_eq!(
format!("{}", res),
"[\"Symbol:\" \"btc\" \"field:\" :close \"id:\" \"cls\"]"
);
}
#[test]
fn test_closure_reassignment_optimization_bug() {
let env = Environment::new();
// This test case reproduces a bug where the optimizer aggressively inlined a function
// ('f') even though its parameters ('x') were being assigned to in the body (by inner lambda).
// The fix ensures that such functions are NOT inlined.
let source = "(do (def f (fn [[x y]] (fn [] (assign x (+ x y))))) ((f [1 2])))";
let res = env.run_script(source);
assert_eq!(format!("{}", res.unwrap()), "3");
}
#[test]
fn test_macro_inlining_identity_collision() {
let source = r#"
(do
(macro wrap [f] `(fn [x] (~f x)))
(def add1 (fn [x] (+ x 1)))
(def add2 (fn [x] (+ x 2)))
(def w1 (wrap add1))
(def w2 (wrap add2))
(w1 (w2 10)))
"#;
// 1. Verify the result is correct
let env_run = Environment::new();
let res = env_run.run_script(source).expect("Failed to run script");
assert_eq!(format!("{}", res), "13");
// 2. Verify that it was actually folded into a constant by the optimizer
let env_dump = Environment::new();
let dump = env_dump.dump_ast(source).expect("Failed to dump AST");
assert!(
dump.contains("Constant: 13"),
"Macro-wrapped calls should be fully folded to 13. Dump:\n{}",
dump
);
// The definitions add1, add2, w1, w2 should be gone after dead code elimination
assert!(
!dump.contains("Define Variable"),
"Definitions should be removed by DCE"
);
}
#[test]
fn test_optimizer_upvalue_inlining_bug_repro() {
let env = Environment::new();
let source = r#"
(do
(def make-counter (fn [init]
(do
(def val init)
{
:inc (fn [] (assign val (+ val 1)))
:get (fn [] val)
})))
(def c (make-counter 10))
((.inc c))
((.get c)))
"#;
let res = env.run_script(source);
assert!(res.is_ok(), "Optimizer bug triggered: {:?}", res.err());
assert_eq!(format!("{}", res.unwrap()), "11");
}
#[test]
fn test_optimizer_destructuring_inlining_and_mutation() {
let env = Environment::new();
// 1. Test: Destructuring definition should allow inlining if not mutated
let source_inline = "(do (def [x y] [10 20]) (+ x y))";
let res_inline = env.run_script(source_inline).unwrap();
assert_eq!(format!("{}", res_inline), "30");
// 2. Test: Destructuring definition should NOT be inlined if mutated (Regression Test)
let source_mutation = r#"
(do
(def [a b] [1 2])
(def f (fn [] (assign a (+ a b))))
(f)
a)
"#;
let res_mutation = env.run_script(source_mutation).unwrap();
assert_eq!(format!("{}", res_mutation), "3");
}
#[test]
fn test_record_basics() {
let env = Environment::new();
let source = r#"
((fn [user] [(.name user) (.age user)])
{:name "Alice" :age 30})
"#;
let res = env.run_script(source).unwrap();
assert_eq!(format!("{}", res), "[\"Alice\" 30]");
}
#[test]
fn test_record_optimized_access() {
let env = Environment::new();
let source_eval = "(.price {:id 1 :price 99.5})";
// 1. Check result (will be fully folded to a constant by the new optimization)
let res = env.run_script(source_eval).unwrap();
assert_eq!(format!("{}", res), "99.5");
// 2. Verify optimization to GET_FIELD when the record contains non-constants
let source_ast = "(fn [id] (.price {:id id :price 99.5}))";
let dump = env.dump_ast(source_ast).unwrap();
assert!(dump.contains("GetField: .price"), "Should be optimized to GetField. Dump:\n{}", dump);
}
#[test]
fn test_first_class_field_accessor() {
let env = Environment::new();
let source = r#"
(do
(def get-name .name)
; Dynamic call to field accessor
(get-name {:name "Alice"}))
"#;
let res = env.run_script(source).unwrap();
assert_eq!(format!("{}", res), "\"Alice\"");
}
#[test]
fn test_record_constant_folding() {
let env = Environment::new();
// Optimizer should fold (.x {:x 10}) into 10
let source = "(.x {:x 10 :y 20})";
let dump = env.dump_ast(source).unwrap();
assert!(dump.contains("Constant: 10"), "Should evaluate GetField at compile time.");
}
#[test]
fn test_record_literal_constant_folding() {
let env = Environment::new();
let source = " {:a 1 :b 2} ";
let dump = env.dump_ast(source).unwrap();
// Ensure the record definition itself is folded into a Constant.
assert!(dump.contains("Constant: {:a 1, :b 2}"), "Should transform a pure record literal into a constant value.");
assert!(!dump.contains("Record {"), "Should not leave a runtime Record node in the AST.");
}
#[test]
fn test_record_inlining_in_while_loop() {
let env_ast = Environment::new();
// Ensure that constants (like the config record) are inherited into inner lambda scopes (like the body of a while loop).
let source = r#"
(do
(def loop-config {:start 0 :limit 10})
(def loop-idx 0)
(while (< loop-idx (.limit loop-config))
(assign loop-idx (+ loop-idx 1)))
loop-idx
)
"#;
let dump = env_ast.dump_ast(source).unwrap();
// The optimizer should inline `loop-config`, resolving `(.limit loop-config)` to `10`.
// The GetField and the Get for 'loop-config' should vanish inside the condition.
assert!(!dump.contains("GetField: .limit"), "The record field should be completely inlined.");
assert!(dump.contains("Constant: 10"), "The limit should be resolved to a constant 10.");
// The result of running it should obviously still be correct.
let env_run = Environment::new();
let res = env_run.run_script(source).unwrap();
assert_eq!(format!("{}", res), "10");
}
#[test]
fn test_record_errors() {
let env = Environment::new();
// Both cases are reported by the TypeChecker since it can't resolve the call
// for a missing field or a non-record argument.
// 1. Missing field
let res_missing = env.run_script("(.missing {:a 1})");
assert!(res_missing.is_err());
assert!(res_missing.unwrap_err().contains("Invalid arguments"));
// 2. Not a record
let res_not_rec = env.run_script("(.name 123)");
assert!(res_not_rec.is_err());
assert!(res_not_rec.unwrap_err().contains("Invalid arguments"));
}
#[test]
fn test_record_layout_interning() {
let env = Environment::new();
let source = r#"
(do
(def r1 {:a 1 :b 2})
(def r2 {:a 10 :b 20})
; Identical layouts result in identical types
(= r1 r2))
"#;
// This will be false because values differ, but let's just check if it compiles and runs.
// To really test interning, we'd need a way to check if layouts are the same Arc.
let res = env.run_script(source).unwrap();
assert_eq!(format!("{}", res), "false");
}
}