24d36a08a6
This change replaces a manual type check for the `again` function with a call to the generic `unify` function. This improves consistency and leverages existing type-checking logic. A new test case `test_again_type_mismatch` has been added to specifically verify that type mismatches in `again` calls are caught at compile time, preventing potential infinite loops.
177 lines
4.7 KiB
Rust
177 lines
4.7 KiB
Rust
use myc::ast::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("no matching overload")
|
|
);
|
|
}
|
|
|
|
#[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("no matching overload")),
|
|
"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("no matching overload"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_again_type_mismatch() {
|
|
let env = Environment::new();
|
|
// (again "text") should fail: parameter n is int (from call site), not string.
|
|
// Without this check, the program would loop forever.
|
|
let result = env.run_script(
|
|
r#"(do (def f (fn [n] (if (= n 0) n (again "text")))) (f 3))"#,
|
|
);
|
|
assert!(
|
|
result.is_err(),
|
|
"again with wrong type should be a compile error, not an infinite loop"
|
|
);
|
|
}
|