Refactor: Remove integration tests and organize into modules
This commit refactors the test suite by removing the monolithic `integration_test.rs` file. The tests have been categorized and moved into dedicated modules within the `tests/` directory: - `tests/parser.rs`: Contains tests for parsing various literal values. - `tests/destructuring.rs`: Houses tests related to destructuring syntax. - `tests/closures.rs`: Includes tests for closure behavior and related optimizations. - `tests/optimizer.rs`: Contains tests specifically targeting optimizer logic and regressions. - `tests/records.rs`: Holds tests for record syntax and associated optimizations. - `tests/rtl.rs`: Contains tests for runtime library operators and functions. - `tests/pipeline.rs`: Tests for pipeline functionality. - `tests/macros.rs`: Tests for macro expansion and related issues. - `tests/error_recovery.rs`: Includes tests for compiler error handling and recovery mechanisms. - `tests/examples.rs`: Verifies the execution of example scripts. This reorganization improves test discoverability and maintainability. The `lib.rs` file has also been updated to reflect these changes by removing the reference to the old `integration_test` module. The `type_checker.rs` file has had some tests removed, as they are now covered by the more specific tests in `tests/error_recovery.rs`.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
use myc::ast::environment::Environment;
|
||||
use myc::ast::types::Value;
|
||||
|
||||
#[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)
|
||||
.into_result()
|
||||
.expect("Failed to compile");
|
||||
let linked = env.link(compiled);
|
||||
let func = env.instantiate(linked);
|
||||
let result: Result<Value, String> = Ok((func.func)(&[]));
|
||||
|
||||
match result {
|
||||
Ok(Value::Int(20)) => (),
|
||||
Ok(val) => panic!("Expected Int(20), got {:?}", val),
|
||||
Err(e) => panic!("VM Error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_closure_reassignment_optimization_bug() {
|
||||
let env = Environment::new();
|
||||
// Regression: optimizer inlined a function whose parameters were assigned to
|
||||
// by an inner lambda. Such functions must NOT be 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_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]
|
||||
#[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))";
|
||||
let _ = env.run_script(source);
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
match val {
|
||||
Ok(Value::Int(30)) => (),
|
||||
_ => panic!("Expected Int(30), got {:?}", val),
|
||||
}
|
||||
|
||||
assert!(!logs.is_empty(), "Logs should not be empty");
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
use myc::ast::environment::Environment;
|
||||
use myc::ast::types::Value;
|
||||
|
||||
#[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();
|
||||
|
||||
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 destructuring result manually
|
||||
let source_check = "(do (def [x y] [7 8]) [x y])";
|
||||
let res = env.run_script(source_check).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 from [x y], 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 value check
|
||||
{
|
||||
let env = Environment::new();
|
||||
let source_return = "(do (def a 0) (def b 0) (assign [a b] [5 6]) [a b])";
|
||||
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 from [a b], got {:?}", res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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_captured_slot_destructuring() {
|
||||
let env = Environment::new();
|
||||
// A closure capturing both slots from a destructuring def must work correctly.
|
||||
let source = r#"
|
||||
(do
|
||||
(def [x y] [10 20])
|
||||
(def get-sum (fn [] (+ x y)))
|
||||
(get-sum))
|
||||
"#;
|
||||
assert_eq!(format!("{}", env.run_script(source).unwrap()), "30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_destructuring_capture_mutation() {
|
||||
let env = Environment::new();
|
||||
// A closure mutating a slot bound via destructuring must see the updated value.
|
||||
let source = r#"
|
||||
(do
|
||||
(def [a b] [1 2])
|
||||
(def inc! (fn [] (assign a (+ a b))))
|
||||
(inc!)
|
||||
(inc!)
|
||||
a)
|
||||
"#;
|
||||
// 1+2=3, 3+2=5
|
||||
assert_eq!(format!("{}", env.run_script(source).unwrap()), "5");
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use myc::ast::compiler::bound_nodes::TypedNode;
|
||||
use myc::ast::environment::Environment;
|
||||
use myc::ast::types::StaticType;
|
||||
|
||||
// Helper: extracts the return type from a compiled node.
|
||||
// For a function type, returns the return type; otherwise returns the node type directly.
|
||||
fn get_ret_type(node: &TypedNode) -> StaticType {
|
||||
if let StaticType::Function(sig) = &node.ty {
|
||||
sig.ret.clone()
|
||||
} else {
|
||||
node.ty.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Type checker errors ---
|
||||
|
||||
#[test]
|
||||
fn test_destructuring_scalar_error() {
|
||||
let env = Environment::new();
|
||||
let result = env.compile("(def [x] 5)").into_result();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err(),
|
||||
"Statement 'def' cannot be used as an expression.\nCannot destructure type int as a tuple/vector"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_argument_mismatch() {
|
||||
let env = Environment::new();
|
||||
// fn takes 1 arg, called with 0
|
||||
let result = env.compile("(do (def f (fn [x] x)) (f))").into_result();
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.contains("Invalid arguments for function call")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_destructuring_vector_args() {
|
||||
let env = Environment::new();
|
||||
// ((fn [[x y]] (+ x y)) [10 20]) -> 30
|
||||
let result = env.compile("((fn [[x y]] (+ x y)) [10 20])").into_result();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(get_ret_type(&result.unwrap()), StaticType::Int);
|
||||
}
|
||||
|
||||
// --- Error recovery (multi-pass diagnostics) ---
|
||||
|
||||
#[test]
|
||||
fn test_error_recovery_parser() {
|
||||
let env = Environment::new();
|
||||
// Syntax error: mismatched bracket/paren
|
||||
let source = "(do (def a [1 2 ) )";
|
||||
let result = env.compile(source);
|
||||
|
||||
assert!(result.diagnostics.has_errors(), "Expected parser errors");
|
||||
let error_msgs: Vec<_> = result
|
||||
.diagnostics
|
||||
.items
|
||||
.iter()
|
||||
.map(|d| d.message.as_str())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
error_msgs.iter().any(|m| m.contains("RightParen")),
|
||||
"Expected error about RightParen"
|
||||
);
|
||||
assert!(
|
||||
result.ast.is_some(),
|
||||
"AST should be partially built despite parser errors"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_recovery_binder() {
|
||||
let env = Environment::new();
|
||||
// Semantic error: undefined variable
|
||||
let source = "(do (def a 10) (def b unknown_var) (+ a 5))";
|
||||
let result = env.compile(source);
|
||||
|
||||
assert!(result.diagnostics.has_errors(), "Expected binder errors");
|
||||
let error_msgs: Vec<_> = result
|
||||
.diagnostics
|
||||
.items
|
||||
.iter()
|
||||
.map(|d| d.message.as_str())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
error_msgs
|
||||
.iter()
|
||||
.any(|m| m.contains("Undefined variable 'unknown_var'")),
|
||||
"Expected undefined variable error"
|
||||
);
|
||||
assert!(result.ast.is_some(), "AST should be built with Error nodes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_recovery_type_checker() {
|
||||
let env = Environment::new();
|
||||
// Semantic error: Type mismatch in function call
|
||||
let source = "(do (def a 10) (def b (not \"text\")) (- a 2))";
|
||||
let result = env.compile(source);
|
||||
|
||||
assert!(
|
||||
result.diagnostics.has_errors(),
|
||||
"Expected type checker errors"
|
||||
);
|
||||
let error_msgs: Vec<_> = result
|
||||
.diagnostics
|
||||
.items
|
||||
.iter()
|
||||
.map(|d| d.message.as_str())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
error_msgs
|
||||
.iter()
|
||||
.any(|m| m.contains("Invalid arguments for function call")),
|
||||
"Expected invalid arguments error"
|
||||
);
|
||||
assert!(result.ast.is_some(), "AST should be built with Error nodes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_recovery_multiple_errors() {
|
||||
let env = Environment::new();
|
||||
// A script with multiple errors that should all be collected
|
||||
let source = r#"
|
||||
(do
|
||||
(def a undefined_var)
|
||||
(def b (not "text"))
|
||||
)
|
||||
"#;
|
||||
let result = env.compile(source);
|
||||
|
||||
assert!(result.diagnostics.has_errors());
|
||||
assert!(
|
||||
result.diagnostics.items.len() >= 2,
|
||||
"Expected multiple errors to be collected"
|
||||
);
|
||||
|
||||
let error_msgs: Vec<_> = result
|
||||
.diagnostics
|
||||
.items
|
||||
.iter()
|
||||
.map(|d| d.message.as_str())
|
||||
.collect();
|
||||
assert!(
|
||||
error_msgs
|
||||
.iter()
|
||||
.any(|m| m.contains("Undefined variable 'undefined_var'"))
|
||||
);
|
||||
assert!(
|
||||
error_msgs
|
||||
.iter()
|
||||
.any(|m| m.contains("Invalid arguments for function call"))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use myc::utils::tester::run_functional_tests_with_optimization;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
for opt in [false, true] {
|
||||
let results = run_functional_tests_with_optimization(opt);
|
||||
for res in results {
|
||||
assert!(
|
||||
res.success,
|
||||
"Example {} failed at opt {}: {}",
|
||||
res.name, opt, res.message
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use myc::ast::environment::Environment;
|
||||
|
||||
#[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") || dump.contains("Call"),
|
||||
"Macro-wrapped calls should be properly handled. Dump:\n{}",
|
||||
dump
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repro_placeholder_in_template_failure() {
|
||||
let env = Environment::new();
|
||||
let source = r#"
|
||||
(do
|
||||
(macro repeat [var limit body]
|
||||
`((fn [~var __limit]
|
||||
(if (< ~var __limit)
|
||||
(do ~body (again (+ ~var 1) __limit))))
|
||||
0 ~limit))
|
||||
(repeat n 10 42))
|
||||
"#;
|
||||
let res = env.run_script(source);
|
||||
assert!(res.is_ok(), "Expansion failed to strip placeholders: {:?}", res.err());
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
use myc::ast::environment::Environment;
|
||||
|
||||
#[test]
|
||||
fn test_optimizer_destructuring_inlining_and_mutation() {
|
||||
let env = Environment::new();
|
||||
|
||||
// 1. 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. 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_inline_two_lambdas_same_slots() {
|
||||
let env = Environment::new();
|
||||
// Two lambdas each binding a local at slot 0 get inlined into the same scope.
|
||||
// Slot remapping must prevent collision between them.
|
||||
let source = r#"
|
||||
(do
|
||||
(def add1 (fn [x] (+ x 1)))
|
||||
(def add2 (fn [x] (+ x 2)))
|
||||
(+ (add1 10) (add2 20)))
|
||||
"#;
|
||||
assert_eq!(format!("{}", env.run_script(source).unwrap()), "33");
|
||||
let dump = env.dump_ast(source).unwrap();
|
||||
assert!(
|
||||
dump.contains("Constant: 33"),
|
||||
"Should be fully folded to Constant 33. Dump:\n{}",
|
||||
dump
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multipass_const_propagation() {
|
||||
let env = Environment::new();
|
||||
// A constant chain where each step is only resolvable after the previous has been folded.
|
||||
// Requires multiple optimizer passes.
|
||||
let source = r#"
|
||||
(do
|
||||
(def a 1)
|
||||
(def b (+ a 1))
|
||||
(def c (+ b 1))
|
||||
(+ c 1))
|
||||
"#;
|
||||
assert_eq!(format!("{}", env.run_script(source).unwrap()), "4");
|
||||
let dump = env.dump_ast(source).unwrap();
|
||||
assert!(
|
||||
dump.contains("Constant: 4"),
|
||||
"Multi-pass chain should fold to Constant 4. Dump:\n{}",
|
||||
dump
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dead_def_destructuring_correctness() {
|
||||
// A dead destructuring def must not change the result — with or without optimization.
|
||||
let source = "(do (def [x y] [1 2]) 42)";
|
||||
let mut env_opt = Environment::new();
|
||||
env_opt.optimization = true;
|
||||
let mut env_no = Environment::new();
|
||||
env_no.optimization = false;
|
||||
assert_eq!(
|
||||
format!("{}", env_opt.run_script(source).unwrap()),
|
||||
format!("{}", env_no.run_script(source).unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dead_destructuring_def_eliminated() {
|
||||
let env = Environment::new();
|
||||
// A dead destructuring def with a pure RHS should be eliminated by the optimizer.
|
||||
let dump = env.dump_ast("(do (def [x y] [1 2]) 42)").unwrap();
|
||||
assert!(
|
||||
!dump.contains("Def"),
|
||||
"Dead destructuring def should be eliminated from AST. Dump:\n{}",
|
||||
dump
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optimizer_inlining_slot_clash_repro() {
|
||||
let mut env = Environment::new();
|
||||
env.optimization = true;
|
||||
|
||||
let source = r#"
|
||||
(do
|
||||
(def outer (fn [length]
|
||||
(do
|
||||
(def history (series 100 :float))
|
||||
(fn [val] length)
|
||||
)
|
||||
))
|
||||
((outer 20) 5)
|
||||
)
|
||||
"#;
|
||||
|
||||
let res = env.run_script(source).unwrap();
|
||||
assert_eq!(format!("{}", res), "20");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reproduce_inlining_slot_clash_crash() {
|
||||
let mut env = Environment::new();
|
||||
env.optimization = true;
|
||||
|
||||
let source = r#"
|
||||
(do
|
||||
(def MY_SMA
|
||||
(fn [length]
|
||||
(do
|
||||
(def history (series 100 :float))
|
||||
(fn [val]
|
||||
(do
|
||||
(push history val)
|
||||
(len history))))))
|
||||
|
||||
(def src (create-random-ohlc 42 100))
|
||||
(def s (.close src))
|
||||
|
||||
[(pipe [s] (MY_SMA 5))]
|
||||
[(pipe [s] (MY_SMA 20))])
|
||||
"#;
|
||||
|
||||
let res = env.run_script(source);
|
||||
assert!(res.is_ok(), "Inlining slot clash triggered: {:?}", res.err());
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use myc::ast::nodes::SyntaxKind;
|
||||
use myc::ast::parser::Parser;
|
||||
use myc::ast::types::Value;
|
||||
|
||||
#[test]
|
||||
fn test_parse_integer_constant() {
|
||||
let source = "123";
|
||||
let mut parser = Parser::new(source);
|
||||
let ast = parser.parse_expression();
|
||||
|
||||
if let SyntaxKind::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);
|
||||
let ast = parser.parse_expression();
|
||||
|
||||
if let SyntaxKind::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);
|
||||
let ast = parser.parse_expression();
|
||||
|
||||
if let SyntaxKind::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);
|
||||
let ast = parser.parse_expression();
|
||||
|
||||
if let SyntaxKind::Constant(Value::Float(val)) = ast.kind {
|
||||
assert_eq!(val, -10.5);
|
||||
} else {
|
||||
panic!("Expected Float constant, got {:?}", ast.kind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use myc::ast::environment::Environment;
|
||||
use myc::ast::types::Value;
|
||||
|
||||
#[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 Value::Object(obj) = val {
|
||||
assert_eq!(obj.type_name(), "StreamNode");
|
||||
} else {
|
||||
panic!("Expected an Object(StreamNode)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
use myc::ast::environment::Environment;
|
||||
|
||||
#[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})";
|
||||
|
||||
let res = env.run_script(source_eval).unwrap();
|
||||
assert_eq!(format!("{}", res), "99.5");
|
||||
|
||||
// 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)
|
||||
(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();
|
||||
|
||||
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();
|
||||
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();
|
||||
|
||||
assert!(
|
||||
dump.contains("Constant: 10") || dump.contains("GetField: .limit"),
|
||||
"The limit should be resolved to a constant 10 or kept as GetField."
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
// 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})
|
||||
(= r1 r2))
|
||||
"#;
|
||||
let res = env.run_script(source).unwrap();
|
||||
assert_eq!(format!("{}", res), "false");
|
||||
}
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
use myc::ast::environment::Environment;
|
||||
use myc::ast::types::Value;
|
||||
|
||||
#[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");
|
||||
assert_eq!(format!("{}", env.run_script("(% 20 3)").unwrap()), "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");
|
||||
assert_eq!(format!("{}", env.run_script("(>> 4 1)").unwrap()), "2");
|
||||
assert_eq!(format!("{}", env.run_script("(and 3 1)").unwrap()), "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();
|
||||
|
||||
let val1_a = env1.run_script("(do (def rand (make-random 123)) (rand))").unwrap();
|
||||
let val2_a = env2.run_script("(do (def rand (make-random)) (rand))").unwrap();
|
||||
|
||||
assert_ne!(
|
||||
val1_a, val2_a,
|
||||
"Environments must have isolated PRNG states"
|
||||
);
|
||||
|
||||
let val2_b = env2.run_script("(do (def rand-same (make-random 123)) (rand-same))").unwrap();
|
||||
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();
|
||||
|
||||
let val1 = env.run_script("(do (def rand1 (make-random 42)) (rand1))").unwrap();
|
||||
let val2 = env.run_script("(do (def rand2 (make-random 42)) (rand2))").unwrap();
|
||||
|
||||
assert_eq!(val1, val2, "Random results must be identical for the same seed");
|
||||
|
||||
let val3 = env.run_script("(do (def rand3 (make-random 123)) (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)";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user