Files
RustAst/tests/error_recovery.rs
Brummel 02ea2f0d80 Add Program node kind
The `Program` node kind is introduced to represent top-level code
blocks, distinguishing them from `Block` nodes which introduce new
scopes. This commit updates various compiler passes to handle the
`Program` node, ensuring correct AST traversal and processing.

The `Program` node is similar to `Block`, but its definitions propagate
to the enclosing scope, unlike `Block` which creates a new, isolated
scope. This distinction is important for how variables and functions are
resolved within the compiled code.
2026-03-31 13:53:51 +02:00

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(),
"Cannot 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"
);
}