//! 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 Show the type of an expression :clear Clear the environment :load 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 = 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 "); } } ":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 "); } } ":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 { 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"]"#); } // 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")); } // 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"); } } }