Files
lux/src/main.rs
Brandon Lucas d37f0fb096 Add Elm-style error diagnostics
Implement beautiful, informative error messages inspired by Elm:
- Rich diagnostic rendering with source code snippets
- Colored output with proper underlines showing error locations
- Categorized error titles (Type Mismatch, Unknown Name, etc.)
- Contextual hints and suggestions for common errors
- Support for type errors, runtime errors, and parse errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-13 04:01:49 -05:00

935 lines
27 KiB
Rust

//! Lux - A functional programming language with first-class effects
mod ast;
mod diagnostics;
mod interpreter;
mod lexer;
mod modules;
mod parser;
mod schema;
mod typechecker;
mod types;
use diagnostics::render;
use interpreter::Interpreter;
use parser::Parser;
use std::io::{self, Write};
use typechecker::TypeChecker;
const VERSION: &str = "0.1.0";
const HELP: &str = r#"
Lux - A functional language with first-class effects
Commands:
:help, :h Show this help
:quit, :q Exit the REPL
:type <expr> Show the type of an expression
:clear Clear the environment
:load <file> Load and execute a file
:trace on/off Enable/disable effect tracing
:traces Show recorded effect traces
Examples:
> let x = 42
> x + 1
43
> fn double(n: Int): Int = n * 2
> double(21)
42
> Console.print("Hello, world!")
Hello, world!
Debugging:
> :trace on
> Console.print("test")
> :traces
[ 0.123ms] Console.print("test") → ()
"#;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
// Run a file
run_file(&args[1]);
} else {
// Start REPL
run_repl();
}
}
fn run_file(path: &str) {
use modules::ModuleLoader;
use std::path::Path;
let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file '{}': {}", path, e);
std::process::exit(1);
}
};
// Set up module loader with the file's directory as a search path
let mut loader = ModuleLoader::new();
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
// Load and parse the program (including any imports)
let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p,
Err(e) => {
eprintln!("Module error: {}", e);
std::process::exit(1);
}
};
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
for error in errors {
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
}
std::process::exit(1);
}
let mut interp = Interpreter::new();
match interp.run_with_modules(&program, &loader) {
Ok(value) => {
if !matches!(value, interpreter::Value::Unit) {
println!("{}", value);
}
}
Err(e) => {
let diagnostic = e.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
std::process::exit(1);
}
}
}
fn run_repl() {
println!("Lux v{}", VERSION);
println!("Type :help for help, :quit to exit\n");
let mut interp = Interpreter::new();
let mut checker = TypeChecker::new();
let mut buffer = String::new();
let mut continuation = false;
loop {
// Print prompt
let prompt = if continuation { "... " } else { "lux> " };
print!("{}", prompt);
io::stdout().flush().unwrap();
// Read input
let mut line = String::new();
match io::stdin().read_line(&mut line) {
Ok(0) => break, // EOF
Ok(_) => {}
Err(e) => {
eprintln!("Error reading input: {}", e);
continue;
}
}
let line = line.trim_end();
// Handle commands
if !continuation && line.starts_with(':') {
handle_command(line, &mut interp, &mut checker);
continue;
}
// Accumulate input
buffer.push_str(line);
buffer.push('\n');
// Check for continuation (simple heuristic: unbalanced braces)
let open_braces = buffer.chars().filter(|c| *c == '{').count();
let close_braces = buffer.chars().filter(|c| *c == '}').count();
let open_parens = buffer.chars().filter(|c| *c == '(').count();
let close_parens = buffer.chars().filter(|c| *c == ')').count();
if open_braces > close_braces || open_parens > close_parens {
continuation = true;
continue;
}
continuation = false;
let input = std::mem::take(&mut buffer);
if input.trim().is_empty() {
continue;
}
eval_input(&input, &mut interp, &mut checker);
}
println!("\nGoodbye!");
}
fn handle_command(line: &str, interp: &mut Interpreter, checker: &mut TypeChecker) {
let parts: Vec<&str> = line.splitn(2, ' ').collect();
let cmd = parts[0];
let arg = parts.get(1).map(|s| s.trim());
match cmd {
":help" | ":h" => {
println!("{}", HELP);
}
":quit" | ":q" => {
println!("Goodbye!");
std::process::exit(0);
}
":type" | ":t" => {
if let Some(expr_str) = arg {
show_type(expr_str, checker);
} else {
println!("Usage: :type <expression>");
}
}
":clear" => {
*interp = Interpreter::new();
*checker = TypeChecker::new();
println!("Environment cleared.");
}
":load" | ":l" => {
if let Some(path) = arg {
load_file(path, interp, checker);
} else {
println!("Usage: :load <filename>");
}
}
":trace" => match arg {
Some("on") => {
interp.enable_tracing();
println!("Effect tracing enabled.");
}
Some("off") => {
interp.trace_effects = false;
println!("Effect tracing disabled.");
}
_ => {
println!("Usage: :trace on|off");
}
},
":traces" => {
if interp.get_traces().is_empty() {
println!("No effect traces recorded. Use :trace on to enable tracing.");
} else {
interp.print_traces();
}
}
_ => {
println!("Unknown command: {}", cmd);
println!("Type :help for help");
}
}
}
fn show_type(expr_str: &str, checker: &mut TypeChecker) {
// Wrap expression in a let to parse it
let wrapped = format!("let _expr_ = {}", expr_str);
match Parser::parse_source(&wrapped) {
Ok(program) => {
if let Err(errors) = checker.check_program(&program) {
for error in errors {
println!("Type error: {}", error);
}
} else {
println!("(type checking passed)");
}
}
Err(e) => {
println!("Parse error: {}", e);
}
}
}
fn load_file(path: &str, interp: &mut Interpreter, checker: &mut TypeChecker) {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
println!("Error reading file '{}': {}", path, e);
return;
}
};
let program = match Parser::parse_source(&source) {
Ok(p) => p,
Err(e) => {
println!("Parse error: {}", e);
return;
}
};
if let Err(errors) = checker.check_program(&program) {
for error in errors {
println!("Type error: {}", error);
}
return;
}
match interp.run(&program) {
Ok(_) => println!("Loaded '{}'", path),
Err(e) => println!("Runtime error: {}", e),
}
}
fn eval_input(input: &str, interp: &mut Interpreter, checker: &mut TypeChecker) {
// Try to parse as a program (declarations)
match Parser::parse_source(input) {
Ok(program) => {
// Type check
if let Err(errors) = checker.check_program(&program) {
for error in errors {
println!("Type error: {}", error);
}
return;
}
// Execute
match interp.run(&program) {
Ok(value) => {
if !matches!(value, interpreter::Value::Unit) {
println!("{}", value);
}
}
Err(e) => {
println!("Runtime error: {}", e);
}
}
}
Err(parse_err) => {
// Try wrapping as an expression
let wrapped = format!("let _result_ = {}", input.trim());
match Parser::parse_source(&wrapped) {
Ok(program) => {
if let Err(errors) = checker.check_program(&program) {
for error in errors {
println!("Type error: {}", error);
}
return;
}
match interp.run(&program) {
Ok(value) => {
println!("{}", value);
}
Err(e) => {
println!("Runtime error: {}", e);
}
}
}
Err(_) => {
// Use original error
println!("Parse error: {}", parse_err);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn eval(source: &str) -> Result<String, String> {
let program = Parser::parse_source(source).map_err(|e| e.to_string())?;
let mut checker = TypeChecker::new();
checker.check_program(&program).map_err(|errors| {
errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n")
})?;
let mut interp = Interpreter::new();
let value = interp.run(&program).map_err(|e| e.to_string())?;
Ok(format!("{}", value))
}
#[test]
fn test_arithmetic() {
assert_eq!(eval("let x = 1 + 2").unwrap(), "3");
assert_eq!(eval("let x = 10 - 3").unwrap(), "7");
assert_eq!(eval("let x = 4 * 5").unwrap(), "20");
assert_eq!(eval("let x = 15 / 3").unwrap(), "5");
}
#[test]
fn test_function() {
let source = r#"
fn add(a: Int, b: Int): Int = a + b
let result = add(3, 4)
"#;
assert_eq!(eval(source).unwrap(), "7");
}
#[test]
fn test_if_expr() {
let source = r#"
fn max(a: Int, b: Int): Int = if a > b then a else b
let result = max(5, 3)
"#;
assert_eq!(eval(source).unwrap(), "5");
}
#[test]
fn test_recursion() {
let source = r#"
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
let result = factorial(5)
"#;
assert_eq!(eval(source).unwrap(), "120");
}
#[test]
fn test_lambda() {
let source = r#"
let double = fn(x: Int): Int => x * 2
let result = double(21)
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_records() {
let source = r#"
let person = { name: "Alice", age: 30 }
let result = person.age
"#;
assert_eq!(eval(source).unwrap(), "30");
}
#[test]
fn test_lists() {
let source = "let nums = [1, 2, 3]";
assert_eq!(eval(source).unwrap(), "[1, 2, 3]");
}
#[test]
fn test_tuples() {
let source = "let pair = (42, \"hello\")";
assert_eq!(eval(source).unwrap(), "(42, \"hello\")");
}
#[test]
fn test_block() {
let source = r#"
let result = {
let x = 10
let y = 20
x + y
}
"#;
assert_eq!(eval(source).unwrap(), "30");
}
#[test]
fn test_pipe() {
let source = r#"
fn double(x: Int): Int = x * 2
fn add_one(x: Int): Int = x + 1
let result = 5 |> double |> add_one
"#;
assert_eq!(eval(source).unwrap(), "11");
}
// ============ Standard Library Tests ============
// List tests
#[test]
fn test_list_length() {
assert_eq!(eval("let x = List.length([1, 2, 3])").unwrap(), "3");
assert_eq!(eval("let x = List.length([])").unwrap(), "0");
}
#[test]
fn test_list_reverse() {
assert_eq!(
eval("let x = List.reverse([1, 2, 3])").unwrap(),
"[3, 2, 1]"
);
assert_eq!(eval("let x = List.reverse([])").unwrap(), "[]");
}
#[test]
fn test_list_range() {
assert_eq!(eval("let x = List.range(0, 5)").unwrap(), "[0, 1, 2, 3, 4]");
assert_eq!(eval("let x = List.range(3, 3)").unwrap(), "[]");
assert_eq!(eval("let x = List.range(-2, 2)").unwrap(), "[-2, -1, 0, 1]");
}
#[test]
fn test_list_head() {
assert_eq!(eval("let x = List.head([1, 2, 3])").unwrap(), "Some(1)");
assert_eq!(eval("let x = List.head([])").unwrap(), "None");
}
#[test]
fn test_list_tail() {
assert_eq!(
eval("let x = List.tail([1, 2, 3])").unwrap(),
"Some([2, 3])"
);
assert_eq!(eval("let x = List.tail([1])").unwrap(), "Some([])");
assert_eq!(eval("let x = List.tail([])").unwrap(), "None");
}
#[test]
fn test_list_concat() {
assert_eq!(
eval("let x = List.concat([1, 2], [3, 4])").unwrap(),
"[1, 2, 3, 4]"
);
assert_eq!(eval("let x = List.concat([], [1])").unwrap(), "[1]");
assert_eq!(eval("let x = List.concat([1], [])").unwrap(), "[1]");
}
#[test]
fn test_list_get() {
assert_eq!(
eval("let x = List.get([10, 20, 30], 0)").unwrap(),
"Some(10)"
);
assert_eq!(
eval("let x = List.get([10, 20, 30], 2)").unwrap(),
"Some(30)"
);
assert_eq!(eval("let x = List.get([10, 20, 30], 5)").unwrap(), "None");
assert_eq!(eval("let x = List.get([10, 20, 30], -1)").unwrap(), "None");
}
#[test]
fn test_list_map() {
let source = r#"
fn double(x: Int): Int = x * 2
let result = List.map([1, 2, 3], double)
"#;
assert_eq!(eval(source).unwrap(), "[2, 4, 6]");
}
#[test]
fn test_list_map_lambda() {
let source = "let x = List.map([1, 2, 3], fn(x: Int): Int => x * x)";
assert_eq!(eval(source).unwrap(), "[1, 4, 9]");
}
#[test]
fn test_list_filter() {
let source = "let x = List.filter([1, 2, 3, 4, 5], fn(x: Int): Bool => x > 2)";
assert_eq!(eval(source).unwrap(), "[3, 4, 5]");
}
#[test]
fn test_list_filter_all() {
let source = "let x = List.filter([1, 2, 3], fn(x: Int): Bool => x > 10)";
assert_eq!(eval(source).unwrap(), "[]");
}
#[test]
fn test_list_fold() {
let source = "let x = List.fold([1, 2, 3, 4], 0, fn(acc: Int, x: Int): Int => acc + x)";
assert_eq!(eval(source).unwrap(), "10");
}
#[test]
fn test_list_fold_product() {
let source = "let x = List.fold([1, 2, 3, 4], 1, fn(acc: Int, x: Int): Int => acc * x)";
assert_eq!(eval(source).unwrap(), "24");
}
// String tests
#[test]
fn test_string_length() {
assert_eq!(eval(r#"let x = String.length("hello")"#).unwrap(), "5");
assert_eq!(eval(r#"let x = String.length("")"#).unwrap(), "0");
}
#[test]
fn test_string_split() {
assert_eq!(
eval(r#"let x = String.split("a,b,c", ",")"#).unwrap(),
r#"["a", "b", "c"]"#
);
assert_eq!(
eval(r#"let x = String.split("hello", ",")"#).unwrap(),
r#"["hello"]"#
);
}
#[test]
fn test_string_join() {
assert_eq!(
eval(r#"let x = String.join(["a", "b", "c"], "-")"#).unwrap(),
r#""a-b-c""#
);
assert_eq!(
eval(r#"let x = String.join(["hello"], ",")"#).unwrap(),
r#""hello""#
);
assert_eq!(eval(r#"let x = String.join([], ",")"#).unwrap(), r#""""#);
}
#[test]
fn test_string_trim() {
assert_eq!(
eval(r#"let x = String.trim(" hello ")"#).unwrap(),
r#""hello""#
);
assert_eq!(
eval(r#"let x = String.trim("hello")"#).unwrap(),
r#""hello""#
);
assert_eq!(eval(r#"let x = String.trim(" ")"#).unwrap(), r#""""#);
}
#[test]
fn test_string_contains() {
assert_eq!(
eval(r#"let x = String.contains("hello world", "world")"#).unwrap(),
"true"
);
assert_eq!(
eval(r#"let x = String.contains("hello", "xyz")"#).unwrap(),
"false"
);
assert_eq!(
eval(r#"let x = String.contains("hello", "")"#).unwrap(),
"true"
);
}
#[test]
fn test_string_replace() {
assert_eq!(
eval(r#"let x = String.replace("hello", "l", "L")"#).unwrap(),
r#""heLLo""#
);
assert_eq!(
eval(r#"let x = String.replace("aaa", "a", "b")"#).unwrap(),
r#""bbb""#
);
}
#[test]
fn test_string_chars() {
assert_eq!(eval(r#"let x = String.chars("hi")"#).unwrap(), "['h', 'i']");
assert_eq!(eval(r#"let x = String.chars("")"#).unwrap(), "[]");
}
#[test]
fn test_string_lines() {
// Note: Using actual newline in the string
let source = r#"let x = String.lines("a
b
c")"#;
assert_eq!(eval(source).unwrap(), r#"["a", "b", "c"]"#);
}
// Option tests
#[test]
fn test_option_constructors() {
assert_eq!(eval("let x = Some(42)").unwrap(), "Some(42)");
assert_eq!(eval("let x = None").unwrap(), "None");
}
#[test]
fn test_option_is_some() {
assert_eq!(eval("let x = Option.isSome(Some(42))").unwrap(), "true");
assert_eq!(eval("let x = Option.isSome(None)").unwrap(), "false");
}
#[test]
fn test_option_is_none() {
assert_eq!(eval("let x = Option.isNone(None)").unwrap(), "true");
assert_eq!(eval("let x = Option.isNone(Some(42))").unwrap(), "false");
}
#[test]
fn test_option_get_or_else() {
assert_eq!(eval("let x = Option.getOrElse(Some(42), 0)").unwrap(), "42");
assert_eq!(eval("let x = Option.getOrElse(None, 0)").unwrap(), "0");
}
#[test]
fn test_option_map() {
let source = "let x = Option.map(Some(5), fn(x: Int): Int => x * 2)";
assert_eq!(eval(source).unwrap(), "Some(10)");
}
#[test]
fn test_option_map_none() {
let source = "let x = Option.map(None, fn(x: Int): Int => x * 2)";
assert_eq!(eval(source).unwrap(), "None");
}
#[test]
fn test_option_flat_map() {
let source = "let x = Option.flatMap(Some(5), fn(x: Int): Option<Int> => Some(x * 2))";
assert_eq!(eval(source).unwrap(), "Some(10)");
}
#[test]
fn test_option_flat_map_to_none() {
let source = "let x = Option.flatMap(Some(5), fn(x: Int): Option<Int> => None)";
assert_eq!(eval(source).unwrap(), "None");
}
// Result tests
#[test]
fn test_result_constructors() {
assert_eq!(eval("let x = Ok(42)").unwrap(), "Ok(42)");
assert_eq!(eval(r#"let x = Err("error")"#).unwrap(), r#"Err("error")"#);
}
#[test]
fn test_result_is_ok() {
assert_eq!(eval("let x = Result.isOk(Ok(42))").unwrap(), "true");
assert_eq!(eval(r#"let x = Result.isOk(Err("e"))"#).unwrap(), "false");
}
#[test]
fn test_result_is_err() {
assert_eq!(eval(r#"let x = Result.isErr(Err("e"))"#).unwrap(), "true");
assert_eq!(eval("let x = Result.isErr(Ok(42))").unwrap(), "false");
}
#[test]
fn test_result_get_or_else() {
assert_eq!(eval("let x = Result.getOrElse(Ok(42), 0)").unwrap(), "42");
assert_eq!(
eval(r#"let x = Result.getOrElse(Err("e"), 0)"#).unwrap(),
"0"
);
}
#[test]
fn test_result_map() {
let source = "let x = Result.map(Ok(5), fn(x: Int): Int => x * 2)";
assert_eq!(eval(source).unwrap(), "Ok(10)");
}
#[test]
fn test_result_map_err() {
let source = r#"let x = Result.map(Err("e"), fn(x: Int): Int => x * 2)"#;
assert_eq!(eval(source).unwrap(), r#"Err("e")"#);
}
// Utility function tests
#[test]
fn test_to_string() {
assert_eq!(eval("let x = toString(42)").unwrap(), r#""42""#);
assert_eq!(eval("let x = toString(true)").unwrap(), r#""true""#);
assert_eq!(eval("let x = toString([1, 2])").unwrap(), r#""[1, 2]""#);
}
#[test]
fn test_type_of() {
assert_eq!(eval("let x = typeOf(42)").unwrap(), r#""Int""#);
assert_eq!(eval("let x = typeOf(true)").unwrap(), r#""Bool""#);
assert_eq!(eval("let x = typeOf([1, 2])").unwrap(), r#""List""#);
assert_eq!(eval(r#"let x = typeOf("hello")"#).unwrap(), r#""String""#);
}
// Pipe with stdlib tests
#[test]
fn test_pipe_with_list() {
assert_eq!(
eval("let x = [1, 2, 3] |> List.reverse").unwrap(),
"[3, 2, 1]"
);
assert_eq!(eval("let x = [1, 2, 3] |> List.length").unwrap(), "3");
}
#[test]
fn test_pipe_with_string() {
assert_eq!(
eval(r#"let x = " hello " |> String.trim"#).unwrap(),
r#""hello""#
);
}
// Combined stdlib usage tests
#[test]
fn test_list_filter_even() {
let source = r#"
fn isEven(x: Int): Bool = x % 2 == 0
let result = List.filter(List.range(1, 6), isEven)
"#;
assert_eq!(eval(source).unwrap(), "[2, 4]");
}
#[test]
fn test_option_chain() {
let source = r#"
fn times10(x: Int): Int = x * 10
let head = List.head([1, 2, 3])
let mapped = Option.map(head, times10)
let result = Option.getOrElse(mapped, 0)
"#;
assert_eq!(eval(source).unwrap(), "10");
}
#[test]
fn test_option_chain_empty() {
let source = r#"
fn times10(x: Int): Int = x * 10
let head = List.head([])
let mapped = Option.map(head, times10)
let result = Option.getOrElse(mapped, 0)
"#;
assert_eq!(eval(source).unwrap(), "0");
}
// ============ Behavioral Types Tests ============
#[test]
fn test_behavioral_pure_function() {
// A pure function with no effects should work
let source = r#"
fn double(x: Int): Int is pure = x * 2
let result = double(21)
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_behavioral_total_function() {
// A total function should work
let source = r#"
fn always42(): Int is total = 42
let result = always42()
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_behavioral_idempotent_function() {
// An idempotent function
let source = r#"
fn clamp(x: Int): Int is idempotent = if x < 0 then 0 else x
let result = clamp(clamp(-5))
"#;
assert_eq!(eval(source).unwrap(), "0");
}
#[test]
fn test_behavioral_multiple_properties() {
// A function with multiple properties
let source = r#"
fn identity(x: Int): Int is pure, is total = x
let result = identity(100)
"#;
assert_eq!(eval(source).unwrap(), "100");
}
#[test]
fn test_behavioral_deterministic() {
let source = r#"
fn square(x: Int): Int is deterministic = x * x
let result = square(7)
"#;
assert_eq!(eval(source).unwrap(), "49");
}
#[test]
fn test_behavioral_pure_with_effects_error() {
// A pure function with effects should produce a type error
let source = r#"
fn bad(x: Int): Int with {Logger} is pure = x
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("pure but has effects"));
}
// Diagnostic rendering tests
mod diagnostic_tests {
use crate::diagnostics::{render_diagnostic_plain, Diagnostic, Severity};
use crate::ast::Span;
use crate::typechecker::TypeError;
use crate::interpreter::RuntimeError;
#[test]
fn test_type_error_to_diagnostic() {
let error = TypeError {
message: "Type mismatch: expected Int, got String".to_string(),
span: Span { start: 10, end: 20 },
};
let diag = error.to_diagnostic();
assert_eq!(diag.severity, Severity::Error);
assert_eq!(diag.title, "Type Mismatch");
assert!(diag.hints.len() > 0);
}
#[test]
fn test_runtime_error_to_diagnostic() {
let error = RuntimeError {
message: "Division by zero".to_string(),
span: Some(Span { start: 5, end: 10 }),
};
let diag = error.to_diagnostic();
assert_eq!(diag.severity, Severity::Error);
assert_eq!(diag.title, "Division by Zero");
assert!(diag.hints.len() > 0);
}
#[test]
fn test_diagnostic_render_with_real_code() {
let source = "fn add(a: Int, b: Int): Int = a + b\nlet result = add(1, \"two\")";
let diag = Diagnostic {
severity: Severity::Error,
title: "Type Mismatch".to_string(),
message: "Expected Int but got String".to_string(),
span: Span { start: 56, end: 61 },
hints: vec!["The second argument should be an Int.".to_string()],
};
let output = render_diagnostic_plain(&diag, source, Some("example.lux"));
assert!(output.contains("ERROR"));
assert!(output.contains("example.lux"));
assert!(output.contains("Type Mismatch"));
assert!(output.contains("\"two\""));
assert!(output.contains("Hint:"));
}
#[test]
fn test_undefined_variable_categorization() {
let error = TypeError {
message: "Undefined variable: foobar".to_string(),
span: Span { start: 0, end: 6 },
};
let diag = error.to_diagnostic();
assert_eq!(diag.title, "Unknown Name");
assert!(diag.hints.iter().any(|h| h.contains("spelling")));
}
#[test]
fn test_purity_violation_categorization() {
let error = TypeError {
message: "Function 'foo' is declared as pure but has effects: {Console}".to_string(),
span: Span { start: 0, end: 10 },
};
let diag = error.to_diagnostic();
assert_eq!(diag.title, "Purity Violation");
}
}
}