//! Lux - A functional programming language with first-class effects mod ast; mod diagnostics; mod exhaustiveness; mod interpreter; mod lexer; mod lsp; 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 Show the type of an expression :info Show info about a binding :env Show user-defined bindings :clear Clear the environment :load 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 = std::env::args().collect(); if args.len() > 1 { match args[1].as_str() { "--lsp" => { // Run LSP server if let Err(e) = lsp::LspServer::run() { eprintln!("LSP server error: {}", e); std::process::exit(1); } } "--help" | "-h" => { println!("Lux {} - A functional language with first-class effects", VERSION); println!(); println!("Usage:"); println!(" lux Start the REPL"); println!(" lux Run a file"); println!(" lux --lsp Start LSP server (for IDE integration)"); println!(" lux --help Show this help"); } "--version" | "-v" => { println!("Lux {}", VERSION); } path => { // Run a file run_file(path); } } } 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, commands: Vec, user_defined: HashSet, } impl LuxHelper { fn new() -> Self { let keywords: HashSet = 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)> { // 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 { 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::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 = 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 { 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 "); } } ":info" | ":i" => { if let Some(name) = arg { show_info(name, checker); } else { println!("Usage: :info "); } } ":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 "); } } ":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!(" {} : ", 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 { 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::>() .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"]"#); } // String interpolation tests #[test] fn test_string_interpolation_simple() { let source = r#" let name = "World" let x = "Hello, {name}!" "#; assert_eq!(eval(source).unwrap(), r#""Hello, World!""#); } #[test] fn test_string_interpolation_numbers() { let source = r#" let n = 42 let x = "The answer is {n}" "#; assert_eq!(eval(source).unwrap(), r#""The answer is 42""#); } #[test] fn test_string_interpolation_expressions() { let source = r#" let a = 2 let b = 3 let x = "{a} + {b} = {a + b}" "#; assert_eq!(eval(source).unwrap(), r#""2 + 3 = 5""#); } #[test] fn test_string_interpolation_escaped_braces() { let source = r#"let x = "literal \{braces\}""#; assert_eq!(eval(source).unwrap(), r#""literal {braces}""#); } // 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 => 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 => 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")); } // Built-in effect tests mod effect_tests { use crate::interpreter::{Interpreter, Value}; use crate::parser::Parser; use crate::typechecker::TypeChecker; fn run_with_effects(source: &str, initial_state: Value, reader_value: Value) -> Result<(String, 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::>().join("\n") })?; let mut interp = Interpreter::new(); interp.set_state(initial_state); interp.set_reader(reader_value); let result = interp.run(&program).map_err(|e| e.to_string())?; let final_state = interp.get_state(); Ok((format!("{}", result), format!("{}", final_state))) } #[test] fn test_state_get() { let source = r#" fn getValue(): Int with {State} = State.get() let result = run getValue() with {} "#; let (result, _) = run_with_effects(source, Value::Int(42), Value::Unit).unwrap(); assert_eq!(result, "42"); } #[test] fn test_state_put() { let source = r#" fn setValue(x: Int): Unit with {State} = State.put(x) let result = run setValue(100) with {} "#; let (_, final_state) = run_with_effects(source, Value::Int(0), Value::Unit).unwrap(); assert_eq!(final_state, "100"); } #[test] fn test_state_get_and_put() { let source = r#" fn increment(): Int with {State} = { let current = State.get() State.put(current + 1) State.get() } let result = run increment() with {} "#; let (result, final_state) = run_with_effects(source, Value::Int(10), Value::Unit).unwrap(); assert_eq!(result, "11"); assert_eq!(final_state, "11"); } #[test] fn test_reader_ask() { let source = r#" fn getConfig(): String with {Reader} = Reader.ask() let result = run getConfig() with {} "#; let (result, _) = run_with_effects(source, Value::Unit, Value::String("config_value".to_string())).unwrap(); // Value's Display includes quotes for strings assert_eq!(result, "\"config_value\""); } #[test] fn test_fail_effect() { let source = r#" fn failing(): Int with {Fail} = Fail.fail("oops") let result = run failing() with {} "#; let result = run_with_effects(source, Value::Unit, Value::Unit); assert!(result.is_err()); assert!(result.unwrap_err().contains("oops")); } #[test] fn test_combined_effects() { let source = r#" fn compute(): Int with {State, Reader} = { let config = Reader.ask() let current = State.get() State.put(current + 1) current } let result = run compute() with {} "#; let (result, final_state) = run_with_effects(source, Value::Int(5), Value::String("test".to_string())).unwrap(); assert_eq!(result, "5"); assert_eq!(final_state, "6"); } #[test] fn test_random_int() { let source = r#" fn getRandomInt(): Int with {Random} = Random.int(1, 10) let result = run getRandomInt() with {} "#; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); let num: i64 = result.parse().expect("Should be an integer"); assert!(num >= 1 && num <= 10, "Random int should be in range 1-10, got {}", num); } #[test] fn test_random_float() { let source = r#" fn getRandomFloat(): Float with {Random} = Random.float() let result = run getRandomFloat() with {} "#; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); let num: f64 = result.parse().expect("Should be a float"); assert!(num >= 0.0 && num < 1.0, "Random float should be in range [0, 1), got {}", num); } #[test] fn test_random_bool() { let source = r#" fn getRandomBool(): Bool with {Random} = Random.bool() let result = run getRandomBool() with {} "#; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); assert!(result == "true" || result == "false", "Random bool should be true or false, got {}", result); } #[test] fn test_time_now() { let source = r#" fn getTime(): Int with {Time} = Time.now() let result = run getTime() with {} "#; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); let timestamp: i64 = result.parse().expect("Should be a timestamp"); // Timestamp should be a reasonable Unix time in milliseconds (after 2020) assert!(timestamp > 1577836800000, "Timestamp should be after 2020"); } } // 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_undefined_variable_suggestion() { // Test that similar variable names are suggested let source = r#" let myVariable = 42 let x = myVriable "#; let result = super::eval(source); assert!(result.is_err()); let err = result.unwrap_err(); // The error should contain a "Did you mean?" suggestion assert!(err.contains("Did you mean") || err.contains("myVariable"), "Error should suggest 'myVariable': {}", err); } #[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, 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 = 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 = 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, 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); } #[test] fn test_effect_inference_function() { // Test that effects are inferred when not explicitly declared // This function uses Console effect without declaring it let source = r#" effect Console { fn print(msg: String): Unit } fn greet(name: String): Unit = Console.print("Hello") let result = 42 "#; let result = eval(source); assert!(result.is_ok(), "Expected success with inferred effects but got: {:?}", result); } #[test] fn test_effect_inference_lambda() { // Test that lambda effects are inferred let source = r#" effect Logger { fn log(msg: String): Unit } let logFn = fn(msg: String) => Logger.log(msg) let result = 42 "#; let result = eval(source); assert!(result.is_ok(), "Expected success with inferred lambda effects but got: {:?}", result); } #[test] fn test_explicit_effects_validation() { // Test that explicitly declared effects are validated against usage let source = r#" effect Console { fn print(msg: String): Unit } fn greet(name: String): Unit with {Console} = Console.print("Hello") let result = 42 "#; let result = eval(source); assert!(result.is_ok(), "Expected success with explicit effects but got: {:?}", result); } #[test] fn test_doc_comments_on_function() { // Test that doc comments are parsed and attached to functions let source = r#" /// Adds two numbers together. /// Returns the sum. fn add(a: Int, b: Int): Int = a + b let result = add(1, 2) "#; assert_eq!(eval(source).unwrap(), "3"); } #[test] fn test_doc_comments_on_type() { // Test that doc comments are parsed and attached to types let source = r#" /// A point in 2D space. type Point { x: Int, y: Int } let p = { x: 1, y: 2 } let result = p.x + p.y "#; assert_eq!(eval(source).unwrap(), "3"); } #[test] fn test_doc_comments_multiline() { // Test that multiple doc comment lines are combined let source = r#" /// First line of documentation. /// Second line of documentation. /// Third line of documentation. fn documented(): Int = 42 let result = documented() "#; assert_eq!(eval(source).unwrap(), "42"); } } }