Files
lux/src/main.rs
Brandon Lucas 05a85ea27f feat: implement type classes / traits
Add support for type classes (traits) with full parsing, type checking, and
validation. The implementation includes:

- Trait declarations: trait Show { fn show(x: T): String }
- Trait implementations: impl Show for Int { fn show(x: Int) = ... }
- Super traits: trait Ord: Eq { ... }
- Trait constraints in where clauses: where T: Show + Eq
- Type parameters on traits: trait Functor<F> { ... }
- Default method implementations
- Validation of required method implementations

This provides a foundation for ad-hoc polymorphism and enables
more expressive type-safe abstractions.

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

1381 lines
41 KiB
Rust

//! Lux - A functional programming language with first-class effects
mod ast;
mod diagnostics;
mod exhaustiveness;
mod interpreter;
mod lexer;
mod modules;
mod parser;
mod schema;
mod typechecker;
mod types;
use diagnostics::render;
use interpreter::Interpreter;
use parser::Parser;
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::history::DefaultHistory;
use rustyline::validate::Validator;
use rustyline::{Config, Editor, Helper};
use std::borrow::Cow;
use std::collections::HashSet;
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
:info <name> Show info about a binding
:env Show user-defined bindings
:clear Clear the environment
:load <file> Load and execute a file
:trace on/off Enable/disable effect tracing
:traces Show recorded effect traces
Keyboard:
Tab Autocomplete
Ctrl-C Cancel current input
Ctrl-D Exit
Up/Down Browse history
Ctrl-R Search history
Examples:
> let x = 42
> x + 1
43
> fn double(n: Int): Int = n * 2
> :type double
double : fn(Int) -> Int
> double(21)
42
> match Some(5) { Some(x) => x, None => 0 }
5
"#;
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);
}
}
}
/// REPL helper for tab completion and syntax highlighting
struct LuxHelper {
keywords: HashSet<String>,
commands: Vec<String>,
user_defined: HashSet<String>,
}
impl LuxHelper {
fn new() -> Self {
let keywords: HashSet<String> = vec![
"fn", "let", "if", "then", "else", "match", "with", "effect", "handler",
"handle", "resume", "type", "import", "pub", "is", "pure", "total",
"idempotent", "deterministic", "commutative", "where", "assume", "true",
"false", "None", "Some", "Ok", "Err", "Int", "Float", "String", "Bool",
"Char", "Unit", "Option", "Result", "List",
]
.into_iter()
.map(String::from)
.collect();
let commands = vec![
":help", ":h", ":quit", ":q", ":type", ":t", ":clear", ":load", ":l",
":trace", ":traces", ":info", ":i", ":env",
]
.into_iter()
.map(String::from)
.collect();
Self {
keywords,
commands,
user_defined: HashSet::new(),
}
}
fn add_definition(&mut self, name: &str) {
self.user_defined.insert(name.to_string());
}
}
impl Completer for LuxHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Pair>)> {
// Find the start of the current word
let start = line[..pos]
.rfind(|c: char| c.is_whitespace() || "(){}[],.;:".contains(c))
.map(|i| i + 1)
.unwrap_or(0);
let prefix = &line[start..pos];
if prefix.is_empty() {
return Ok((pos, Vec::new()));
}
let mut completions = Vec::new();
// Complete commands
if prefix.starts_with(':') {
for cmd in &self.commands {
if cmd.starts_with(prefix) {
completions.push(Pair {
display: cmd.clone(),
replacement: cmd.clone(),
});
}
}
} else {
// Complete keywords
for kw in &self.keywords {
if kw.starts_with(prefix) {
completions.push(Pair {
display: kw.clone(),
replacement: kw.clone(),
});
}
}
// Complete user-defined names
for name in &self.user_defined {
if name.starts_with(prefix) {
completions.push(Pair {
display: name.clone(),
replacement: name.clone(),
});
}
}
}
Ok((start, completions))
}
}
impl Hinter for LuxHelper {
type Hint = String;
fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option<String> {
None
}
}
impl Highlighter for LuxHelper {
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
}
}
impl Validator for LuxHelper {}
impl Helper for LuxHelper {}
fn get_history_path() -> Option<std::path::PathBuf> {
std::env::var_os("HOME").map(|home| {
std::path::PathBuf::from(home).join(".lux_history")
})
}
fn run_repl() {
println!("Lux v{}", VERSION);
println!("Type :help for help, :quit to exit\n");
let config = Config::builder()
.history_ignore_space(true)
.completion_type(rustyline::CompletionType::List)
.edit_mode(rustyline::EditMode::Emacs)
.build();
let helper = LuxHelper::new();
let mut rl: Editor<LuxHelper, DefaultHistory> = Editor::with_config(config).unwrap();
rl.set_helper(Some(helper));
// Load history
if let Some(history_path) = get_history_path() {
if let Some(parent) = history_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = rl.load_history(&history_path);
}
let mut interp = Interpreter::new();
let mut checker = TypeChecker::new();
let mut buffer = String::new();
let mut continuation = false;
loop {
let prompt = if continuation { "... " } else { "lux> " };
match rl.readline(prompt) {
Ok(line) => {
let line = line.trim_end().to_string();
// Don't add empty lines or continuations to history
if !line.is_empty() && !continuation {
let _ = rl.add_history_entry(line.as_str());
}
// Handle commands
if !continuation && line.starts_with(':') {
handle_command(&line, &mut interp, &mut checker, rl.helper_mut().unwrap());
continue;
}
// Accumulate input
buffer.push_str(&line);
buffer.push('\n');
// Check for continuation (unbalanced braces/parens)
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;
}
// Track new definitions for completion
if let Some(name) = extract_definition_name(&input) {
rl.helper_mut().unwrap().add_definition(&name);
}
eval_input(&input, &mut interp, &mut checker);
}
Err(ReadlineError::Interrupted) => {
// Ctrl-C: clear buffer
buffer.clear();
continuation = false;
println!("^C");
}
Err(ReadlineError::Eof) => {
// Ctrl-D: exit
break;
}
Err(err) => {
eprintln!("Error: {:?}", err);
break;
}
}
}
// Save history
if let Some(history_path) = get_history_path() {
let _ = rl.save_history(&history_path);
}
println!("\nGoodbye!");
}
fn extract_definition_name(input: &str) -> Option<String> {
let input = input.trim();
if input.starts_with("fn ") {
input.split_whitespace().nth(1).and_then(|s| {
s.split('(').next().map(String::from)
})
} else if input.starts_with("let ") {
input.split_whitespace().nth(1).and_then(|s| {
s.split([':', '=']).next().map(|s| s.trim().to_string())
})
} else if input.starts_with("type ") {
input.split_whitespace().nth(1).and_then(|s| {
s.split(['<', '=']).next().map(|s| s.trim().to_string())
})
} else if input.starts_with("effect ") {
input.split_whitespace().nth(1).map(|s| {
s.trim_end_matches('{').trim().to_string()
})
} else {
None
}
}
fn handle_command(
line: &str,
interp: &mut Interpreter,
checker: &mut TypeChecker,
helper: &mut LuxHelper,
) {
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>");
}
}
":info" | ":i" => {
if let Some(name) = arg {
show_info(name, checker);
} else {
println!("Usage: :info <name>");
}
}
":env" => {
show_environment(checker, helper);
}
":clear" => {
*interp = Interpreter::new();
*checker = TypeChecker::new();
helper.user_defined.clear();
println!("Environment cleared.");
}
":load" | ":l" => {
if let Some(path) = arg {
load_file(path, interp, checker, helper);
} 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_info(name: &str, checker: &TypeChecker) {
// Look up in the type environment
if let Some(scheme) = checker.lookup(name) {
println!("{} : {}", name, scheme);
} else {
println!("'{}' is not defined.", name);
}
}
fn show_environment(checker: &TypeChecker, helper: &LuxHelper) {
println!("User-defined bindings:");
if helper.user_defined.is_empty() {
println!(" (none)");
} else {
for name in &helper.user_defined {
if let Some(scheme) = checker.lookup(name) {
println!(" {} : {}", name, scheme);
} else {
println!(" {} : <unknown>", name);
}
}
}
}
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, helper: &mut LuxHelper) {
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;
}
// Add definitions for completion
for decl in &program.declarations {
if let Some(name) = extract_definition_name(&format!("{:?}", decl)) {
helper.add_definition(&name);
}
}
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");
}
}
// Exhaustiveness checking tests
mod exhaustiveness_tests {
use super::*;
#[test]
fn test_exhaustive_bool_match() {
let source = r#"
fn check(b: Bool): Int = match b {
true => 1,
false => 0
}
let result = check(true)
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_non_exhaustive_bool_match() {
let source = r#"
fn check(b: Bool): Int = match b {
true => 1
}
let result = check(true)
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Non-exhaustive"));
}
#[test]
fn test_exhaustive_option_match() {
let source = r#"
fn unwrap_or(opt: Option<Int>, default: Int): Int = match opt {
Some(x) => x,
None => default
}
let result = unwrap_or(Some(42), 0)
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_non_exhaustive_option_missing_none() {
let source = r#"
fn get_value(opt: Option<Int>): Int = match opt {
Some(x) => x
}
let result = get_value(Some(1))
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Non-exhaustive"));
}
#[test]
fn test_wildcard_is_exhaustive() {
let source = r#"
fn classify(n: Int): String = match n {
0 => "zero",
1 => "one",
_ => "many"
}
let result = classify(5)
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_variable_pattern_is_exhaustive() {
let source = r#"
fn identity(n: Int): Int = match n {
x => x
}
let result = identity(42)
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_redundant_arm_warning() {
let source = r#"
fn test_fn(n: Int): Int = match n {
_ => 1,
0 => 2
}
let result = test_fn(0)
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Redundant"));
}
#[test]
fn test_exhaustive_result_match() {
let source = r#"
fn handle_result(r: Result<Int, String>): Int = match r {
Ok(n) => n,
Err(_) => 0
}
let result = handle_result(Ok(42))
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_tail_call_optimization() {
// This test verifies that tail-recursive functions don't overflow the stack.
// Without TCO, a countdown from 10000 would cause a stack overflow.
let source = r#"
fn countdown(n: Int): Int = if n <= 0 then 0 else countdown(n - 1)
let result = countdown(10000)
"#;
assert_eq!(eval(source).unwrap(), "0");
}
#[test]
fn test_tail_call_with_accumulator() {
// Test TCO with an accumulator pattern (common for tail-recursive sum)
let source = r#"
fn sum_to(n: Int, acc: Int): Int = if n <= 0 then acc else sum_to(n - 1, acc + n)
let result = sum_to(1000, 0)
"#;
// Sum from 1 to 1000 = 1000 * 1001 / 2 = 500500
assert_eq!(eval(source).unwrap(), "500500");
}
#[test]
fn test_tail_call_in_match() {
// Test that TCO works through match expressions
let source = r#"
fn process(opt: Option<Int>, acc: Int): Int = match opt {
Some(n) => if n <= 0 then acc else process(Some(n - 1), acc + n),
None => acc
}
let result = process(Some(100), 0)
"#;
// Sum from 1 to 100 = 5050
assert_eq!(eval(source).unwrap(), "5050");
}
#[test]
fn test_trait_definition() {
// Test that trait declarations parse and type check correctly
let source = r#"
trait Show {
fn show(x: Int): String
}
let result = 42
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_trait_impl() {
// Test that impl declarations parse and type check correctly
let source = r#"
trait Double {
fn double(x: Int): Int
}
impl Double for Int {
fn double(x: Int): Int = x * 2
}
let result = 21
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_trait_with_super_trait() {
// Test super trait syntax
let source = r#"
trait Eq {
fn eq(a: Int, b: Int): Bool
}
trait Ord: Eq {
fn lt(a: Int, b: Int): Bool
}
let result = 42
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_impl_with_where_clause() {
// Test impl with where clause for trait constraints
let source = r#"
trait Show {
fn show(x: Int): String
}
let result = 42
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
}
}