//! Lux - A functional programming language with first-class effects mod ast; mod codegen; mod debugger; mod diagnostics; mod exhaustiveness; mod formatter; mod interpreter; mod lexer; mod lsp; mod modules; mod package; 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" => { print_help(); } "--version" | "-v" => { println!("Lux {}", VERSION); } "fmt" => { // Format files if args.len() < 3 { eprintln!("Usage: lux fmt [--check]"); std::process::exit(1); } let check_only = args.iter().any(|a| a == "--check"); for arg in &args[2..] { if arg.starts_with('-') { continue; } format_file(arg, check_only); } } "test" => { // Run tests run_tests(&args[2..]); } "watch" => { // Watch mode if args.len() < 3 { eprintln!("Usage: lux watch "); std::process::exit(1); } watch_file(&args[2]); } "init" => { // Initialize a new project init_project(args.get(2).map(|s| s.as_str())); } "check" => { // Type check without running if args.len() < 3 { eprintln!("Usage: lux check "); std::process::exit(1); } check_file(&args[2]); } "debug" => { // Start debugger if args.len() < 3 { eprintln!("Usage: lux debug "); std::process::exit(1); } if let Err(e) = debugger::Debugger::run(&args[2]) { eprintln!("Debugger error: {}", e); std::process::exit(1); } } "pkg" => { // Package manager handle_pkg_command(&args[2..]); } "compile" => { // Compile to native binary or JavaScript if args.len() < 3 { eprintln!("Usage: lux compile [-o binary]"); eprintln!(" lux compile --run"); eprintln!(" lux compile --emit-c [-o file.c]"); eprintln!(" lux compile --target js [-o file.js]"); std::process::exit(1); } let run_after = args.iter().any(|a| a == "--run"); let emit_c = args.iter().any(|a| a == "--emit-c"); let target_js = args.iter() .position(|a| a == "--target") .and_then(|i| args.get(i + 1)) .map(|s| s.as_str() == "js") .unwrap_or(false); let output_path = args.iter() .position(|a| a == "-o") .and_then(|i| args.get(i + 1)) .map(|s| s.as_str()); if target_js { compile_to_js(&args[2], output_path, run_after); } else { compile_to_c(&args[2], output_path, run_after, emit_c); } } path => { // Run a file run_file(path); } } } else { // Start REPL run_repl(); } } fn print_help() { println!("Lux {} - A functional language with first-class effects", VERSION); println!(); println!("Usage:"); println!(" lux Start the REPL"); println!(" lux Run a file (interpreter)"); println!(" lux compile Compile to native binary"); println!(" lux compile -o app Compile to binary named 'app'"); println!(" lux compile --run Compile and execute"); println!(" lux compile --emit-c Output C code instead of binary"); println!(" lux compile --target js Compile to JavaScript"); println!(" lux fmt Format a file (--check to verify only)"); println!(" lux check Type check without running"); println!(" lux test [pattern] Run tests (optional pattern filter)"); println!(" lux watch Watch and re-run on changes"); println!(" lux debug Start interactive debugger"); println!(" lux init [name] Initialize a new project"); println!(" lux pkg Package manager (install, add, remove, list, update)"); println!(" lux --lsp Start LSP server (for IDE integration)"); println!(" lux --help Show this help"); println!(" lux --version Show version"); } fn format_file(path: &str, check_only: bool) { use formatter::{format, FormatConfig}; let source = match std::fs::read_to_string(path) { Ok(s) => s, Err(e) => { eprintln!("Error reading file '{}': {}", path, e); std::process::exit(1); } }; let config = FormatConfig::default(); let formatted = match format(&source, &config) { Ok(f) => f, Err(e) => { eprintln!("Error formatting '{}': {}", path, e); std::process::exit(1); } }; if check_only { if source != formatted { eprintln!("{} would be reformatted", path); std::process::exit(1); } else { println!("{} is correctly formatted", path); } } else { if source != formatted { if let Err(e) = std::fs::write(path, &formatted) { eprintln!("Error writing file '{}': {}", path, e); std::process::exit(1); } println!("Formatted {}", path); } else { println!("{} unchanged", path); } } } fn check_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); } }; let mut loader = ModuleLoader::new(); if let Some(parent) = file_path.parent() { loader.add_search_path(parent.to_path_buf()); } 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); } println!("{}: OK", path); } fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: bool) { use codegen::c_backend::CBackend; use modules::ModuleLoader; use std::path::Path; use std::process::Command; 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); } }; // Parse with module loading let mut loader = ModuleLoader::new(); if let Some(parent) = file_path.parent() { loader.add_search_path(parent.to_path_buf()); } let program = match loader.load_source(&source, Some(file_path)) { Ok(p) => p, Err(e) => { eprintln!("Module error: {}", e); std::process::exit(1); } }; // Type check 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); } // Generate C code let mut backend = CBackend::new(); let c_code = match backend.generate(&program) { Ok(code) => code, Err(e) => { eprintln!("C codegen error: {}", e); eprintln!(); eprintln!("Note: The C backend supports functions, closures, ADTs,"); eprintln!("pattern matching, lists, and Console.print."); eprintln!(); eprintln!("Not yet supported: other effects, some advanced features."); std::process::exit(1); } }; // Handle --emit-c: output C code instead of binary if emit_c { if let Some(out_path) = output_path { if let Err(e) = std::fs::write(out_path, &c_code) { eprintln!("Error writing file '{}': {}", out_path, e); std::process::exit(1); } eprintln!("Wrote C code to {}", out_path); } else { println!("{}", c_code); } return; } // Default: compile to native binary let temp_c = std::env::temp_dir().join("lux_output.c"); // Determine output binary name let output_bin = if let Some(out) = output_path { Path::new(out).to_path_buf() } else { // Derive from source filename: foo.lux -> ./foo let stem = file_path.file_stem() .and_then(|s| s.to_str()) .unwrap_or("a.out"); Path::new(".").join(stem) }; if let Err(e) = std::fs::write(&temp_c, &c_code) { eprintln!("Error writing temp file: {}", e); std::process::exit(1); } // Find C compiler let cc = std::env::var("CC").unwrap_or_else(|_| "cc".to_string()); let compile_result = Command::new(&cc) .args(["-O2", "-o"]) .arg(&output_bin) .arg(&temp_c) .output(); match compile_result { Ok(output) => { if !output.status.success() { eprintln!("C compilation failed:"); eprintln!("{}", String::from_utf8_lossy(&output.stderr)); std::process::exit(1); } } Err(e) => { eprintln!("Failed to run C compiler '{}': {}", cc, e); eprintln!("Make sure gcc or clang is installed, or set CC environment variable."); std::process::exit(1); } } // Clean up temp file let _ = std::fs::remove_file(&temp_c); if run_after { // Run the compiled binary let run_result = Command::new(&output_bin).status(); match run_result { Ok(status) => { std::process::exit(status.code().unwrap_or(1)); } Err(e) => { eprintln!("Failed to run compiled binary: {}", e); std::process::exit(1); } } } else { // Just print where the binary is eprintln!("Compiled to {}", output_bin.display()); } } fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) { use codegen::js_backend::JsBackend; use modules::ModuleLoader; use std::path::Path; use std::process::Command; 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); } }; // Parse with module loading let mut loader = ModuleLoader::new(); if let Some(parent) = file_path.parent() { loader.add_search_path(parent.to_path_buf()); } let program = match loader.load_source(&source, Some(file_path)) { Ok(p) => p, Err(e) => { eprintln!("Module error: {}", e); std::process::exit(1); } }; // Type check 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); } // Generate JavaScript code let mut backend = JsBackend::new(); let js_code = match backend.generate(&program) { Ok(code) => code, Err(e) => { eprintln!("JS codegen error: {}", e); std::process::exit(1); } }; // Determine output file path let output_js = if let Some(out) = output_path { Path::new(out).to_path_buf() } else { // Derive from source filename: foo.lux -> ./foo.js let stem = file_path.file_stem() .and_then(|s| s.to_str()) .unwrap_or("output"); Path::new(".").join(format!("{}.js", stem)) }; // Write the JavaScript file if let Err(e) = std::fs::write(&output_js, &js_code) { eprintln!("Error writing file '{}': {}", output_js.display(), e); std::process::exit(1); } if run_after { // Run with Node.js let node_result = Command::new("node") .arg(&output_js) .status(); match node_result { Ok(status) => { std::process::exit(status.code().unwrap_or(1)); } Err(e) => { eprintln!("Failed to run with Node.js: {}", e); eprintln!("Make sure Node.js is installed."); std::process::exit(1); } } } else { eprintln!("Compiled to {}", output_js.display()); } } fn run_tests(args: &[String]) { use std::path::Path; use std::fs; // Find test files let pattern = args.first().map(|s| s.as_str()); // Look for test files in multiple locations let mut test_files = Vec::new(); // Current directory collect_test_files(".", pattern, &mut test_files); // tests/ subdirectory if Path::new("tests").is_dir() { collect_test_files("tests", pattern, &mut test_files); } // examples/ subdirectory (for example tests) if Path::new("examples").is_dir() { collect_test_files("examples", pattern, &mut test_files); } // If a specific file is given, use that if let Some(p) = pattern { if Path::new(p).is_file() { test_files.clear(); test_files.push(std::path::PathBuf::from(p)); } } if test_files.is_empty() { println!("No test files found."); println!("Test files should be named test_*.lux or *_test.lux"); println!("Or contain functions named test_*"); return; } println!("Running tests...\n"); let mut total_passed = 0; let mut total_failed = 0; let mut all_failures = Vec::new(); for test_file in &test_files { let path_str = test_file.to_string_lossy().to_string(); // Read and parse the file let source = match fs::read_to_string(test_file) { Ok(s) => s, Err(e) => { println!(" {} ... ERROR: {}", path_str, e); total_failed += 1; continue; } }; let program = match Parser::parse_source(&source) { Ok(p) => p, Err(e) => { println!(" {} ... PARSE ERROR: {}", path_str, e); total_failed += 1; continue; } }; // Type check let mut checker = typechecker::TypeChecker::new(); if let Err(errors) = checker.check_program(&program) { println!(" {} ... TYPE ERROR", path_str); for err in errors { eprintln!(" {}", err); } total_failed += 1; continue; } // Find test functions (functions starting with test_) let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| { if let ast::Declaration::Function(f) = d { if f.name.name.starts_with("test_") { return Some(f.name.name.clone()); } } None }).collect(); if test_funcs.is_empty() { // No test functions, run the whole file let mut interp = Interpreter::new(); interp.reset_test_results(); match interp.run(&program) { Ok(_) => { let results = interp.get_test_results(); if results.failed == 0 && results.passed == 0 { // No Test assertions, just check it runs println!(" {} ... OK (no assertions)", path_str); total_passed += 1; } else if results.failed == 0 { println!(" {} ... OK ({} assertions)", path_str, results.passed); total_passed += results.passed; } else { println!(" {} ... FAILED ({} passed, {} failed)", path_str, results.passed, results.failed); total_passed += results.passed; total_failed += results.failed; for failure in &results.failures { all_failures.push((path_str.clone(), "".to_string(), failure.clone())); } } } Err(e) => { println!(" {} ... RUNTIME ERROR: {}", path_str, e); total_failed += 1; } } } else { // Run individual test functions println!(" {}:", path_str); for test_name in &test_funcs { let mut interp = Interpreter::new(); interp.reset_test_results(); // First run the file to define all functions if let Err(e) = interp.run(&program) { println!(" {} ... ERROR: {}", test_name, e); total_failed += 1; continue; } // Call the test function let call_source = format!("let testResult = run {}() with {{}}", test_name); let call_program = match Parser::parse_source(&call_source) { Ok(p) => p, Err(e) => { println!(" {} ... ERROR: {}", test_name, e); total_failed += 1; continue; } }; match interp.run(&call_program) { Ok(_) => { let results = interp.get_test_results(); if results.failed == 0 { println!(" {} ... OK", test_name); total_passed += 1; } else { println!(" {} ... FAILED", test_name); total_failed += 1; for failure in &results.failures { all_failures.push((path_str.clone(), test_name.clone(), failure.clone())); } } } Err(e) => { println!(" {} ... ERROR: {}", test_name, e); total_failed += 1; } } } } } // Print failure details if !all_failures.is_empty() { println!("\n--- Failures ---\n"); for (file, test, failure) in &all_failures { if test.is_empty() { println!("{}:", file); } else { println!("{} - {}:", file, test); } println!(" {}", failure.message); if let Some(expected) = &failure.expected { println!(" Expected: {}", expected); } if let Some(actual) = &failure.actual { println!(" Actual: {}", actual); } println!(); } } println!("Results: {} passed, {} failed", total_passed, total_failed); if total_failed > 0 { std::process::exit(1); } } fn collect_test_files(dir: &str, pattern: Option<&str>, files: &mut Vec) { use std::fs; for entry in fs::read_dir(dir).into_iter().flatten().flatten() { let path = entry.path(); if path.extension().map(|e| e == "lux").unwrap_or(false) { if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if name.starts_with("test_") || name.ends_with("_test.lux") { if pattern.map(|p| name.contains(p)).unwrap_or(true) { files.push(path); } } } } } } fn watch_file(path: &str) { use std::time::{Duration, SystemTime}; use std::path::Path; let file_path = Path::new(path); if !file_path.exists() { eprintln!("File not found: {}", path); std::process::exit(1); } println!("Watching {} for changes (Ctrl+C to stop)...", path); println!(); let mut last_modified = SystemTime::UNIX_EPOCH; loop { let metadata = match std::fs::metadata(file_path) { Ok(m) => m, Err(_) => { std::thread::sleep(Duration::from_millis(500)); continue; } }; let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH); if modified > last_modified { last_modified = modified; // Clear screen print!("\x1B[2J\x1B[H"); println!("=== Running {} ===", path); println!(); // Run the file let result = std::process::Command::new(std::env::current_exe().unwrap()) .arg(path) .status(); match result { Ok(status) if status.success() => { println!(); println!("=== Success ==="); } Ok(_) => { println!(); println!("=== Failed ==="); } Err(e) => { eprintln!("Error running file: {}", e); } } println!(); println!("Watching for changes..."); } std::thread::sleep(Duration::from_millis(500)); } } fn handle_pkg_command(args: &[String]) { use package::{PackageManager, DependencySource}; use std::path::PathBuf; if args.is_empty() { print_pkg_help(); return; } // Help doesn't require being in a project if matches!(args[0].as_str(), "help" | "--help" | "-h") { print_pkg_help(); return; } let project_root = match PackageManager::find_project_root() { Some(root) => root, None => { eprintln!("Error: Not in a Lux project (no lux.toml found)"); eprintln!("Run 'lux init' to create a new project"); std::process::exit(1); } }; let pkg = PackageManager::new(&project_root); match args[0].as_str() { "install" | "i" => { if let Err(e) = pkg.install() { eprintln!("Error: {}", e); std::process::exit(1); } } "add" => { if args.len() < 2 { eprintln!("Usage: lux pkg add [version] [--git ] [--path ]"); std::process::exit(1); } let name = &args[1]; let mut version = "0.0.0".to_string(); let mut source = DependencySource::Registry; let mut i = 2; while i < args.len() { match args[i].as_str() { "--git" => { if i + 1 < args.len() { let url = args[i + 1].clone(); let branch = if i + 3 < args.len() && args[i + 2] == "--branch" { i += 2; Some(args[i + 1].clone()) } else { None }; source = DependencySource::Git { url, branch }; i += 2; } else { eprintln!("--git requires a URL"); std::process::exit(1); } } "--path" => { if i + 1 < args.len() { source = DependencySource::Path { path: PathBuf::from(&args[i + 1]), }; i += 2; } else { eprintln!("--path requires a path"); std::process::exit(1); } } v if !v.starts_with('-') => { version = v.to_string(); i += 1; } _ => i += 1, } } if let Err(e) = pkg.add(name, &version, source) { eprintln!("Error: {}", e); std::process::exit(1); } } "remove" | "rm" => { if args.len() < 2 { eprintln!("Usage: lux pkg remove "); std::process::exit(1); } if let Err(e) = pkg.remove(&args[1]) { eprintln!("Error: {}", e); std::process::exit(1); } } "list" | "ls" => { if let Err(e) = pkg.list() { eprintln!("Error: {}", e); std::process::exit(1); } } "update" => { if let Err(e) = pkg.update() { eprintln!("Error: {}", e); std::process::exit(1); } } "clean" => { if let Err(e) = pkg.clean() { eprintln!("Error: {}", e); std::process::exit(1); } } "help" | "--help" | "-h" => { print_pkg_help(); } unknown => { eprintln!("Unknown package command: {}", unknown); print_pkg_help(); std::process::exit(1); } } } fn print_pkg_help() { println!("Lux Package Manager"); println!(); println!("Usage: lux pkg [options]"); println!(); println!("Commands:"); println!(" install, i Install all dependencies from lux.toml"); println!(" add Add a dependency"); println!(" Options:"); println!(" [version] Specify version (default: 0.0.0)"); println!(" --git Install from git repository"); println!(" --branch Git branch (with --git)"); println!(" --path Install from local path"); println!(" remove, rm Remove a dependency"); println!(" list, ls List dependencies and their status"); println!(" update Update all dependencies"); println!(" clean Remove installed packages"); println!(); println!("Examples:"); println!(" lux pkg install"); println!(" lux pkg add http 1.0.0"); println!(" lux pkg add mylib --git https://github.com/user/mylib"); println!(" lux pkg add local-lib --path ../lib"); println!(" lux pkg remove http"); } fn init_project(name: Option<&str>) { use std::fs; use std::path::Path; let project_name = name.unwrap_or("my-lux-project"); let project_dir = Path::new(project_name); if project_dir.exists() { eprintln!("Directory '{}' already exists", project_name); std::process::exit(1); } // Create project structure fs::create_dir_all(project_dir.join("src")).unwrap(); fs::create_dir_all(project_dir.join("tests")).unwrap(); // Create lux.toml let toml_content = format!( r#"[project] name = "{}" version = "0.1.0" description = "A Lux project" [dependencies] # Add dependencies here # example = "1.0.0" "#, project_name ); fs::write(project_dir.join("lux.toml"), toml_content).unwrap(); // Create main.lux let main_content = r#"// Main entry point fn main(): Unit with {Console} = { Console.print("Hello from Lux!") } let output = run main() with {} "#; fs::write(project_dir.join("src").join("main.lux"), main_content).unwrap(); // Create a test file let test_content = r#"// Example test file fn testAddition(): Bool = { let result = 2 + 2 result == 4 } fn main(): Unit with {Console} = { if testAddition() then Console.print("Test passed!") else Console.print("Test failed!") } let output = run main() with {} "#; fs::write(project_dir.join("tests").join("test_example.lux"), test_content).unwrap(); // Create .gitignore let gitignore_content = r#"# Lux build artifacts /target/ *.luxc # Editor files .vscode/ .idea/ *.swp *~ "#; fs::write(project_dir.join(".gitignore"), gitignore_content).unwrap(); println!("Created new Lux project: {}", project_name); println!(); println!("Project structure:"); println!(" {}/", project_name); println!(" ├── lux.toml"); println!(" ├── src/"); println!(" │ └── main.lux"); println!(" └── tests/"); println!(" └── test_example.lux"); println!(); println!("To get started:"); println!(" cd {}", project_name); println!(" lux src/main.lux"); } 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""#); } // Bug fix tests #[test] fn test_record_equality() { assert_eq!(eval("let x = { a: 1, b: 2 } == { a: 1, b: 2 }").unwrap(), "true"); assert_eq!(eval("let x = { a: 1 } == { a: 2 }").unwrap(), "false"); assert_eq!(eval("let x = { a: 1, b: 2 } == { a: 1, b: 3 }").unwrap(), "false"); } #[test] fn test_invalid_escape_sequence() { let result = eval(r#"let x = "\z""#); assert!(result.is_err()); assert!(result.unwrap_err().contains("Invalid escape sequence")); } #[test] fn test_unknown_effect_error() { let result = eval("fn test(): Unit with {FakeEffect} = ()"); assert!(result.is_err()); assert!(result.unwrap_err().contains("Unknown effect")); } #[test] fn test_circular_type_definitions() { let result = eval("type A = B\ntype B = A"); assert!(result.is_err(), "Should detect circular type definitions"); let err = result.unwrap_err(); assert!(err.contains("Circular"), "Error should mention 'Circular': {}", err); } #[test] fn test_mutual_recursion() { let source = r#" fn isEven(n: Int): Bool = if n == 0 then true else isOdd(n - 1) fn isOdd(n: Int): Bool = if n == 0 then false else isEven(n - 1) let result = isEven(10) "#; assert_eq!(eval(source).unwrap(), "true"); } // 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")); } #[test] fn test_behavioral_deterministic_with_random_error() { // A deterministic function cannot use Random effect let source = r#" fn bad(): Int with {Random} is deterministic = Random.int(1, 100) "#; let result = eval(source); assert!(result.is_err()); assert!(result.unwrap_err().contains("deterministic but uses non-deterministic effects")); } #[test] fn test_behavioral_deterministic_with_time_error() { // A deterministic function cannot use Time effect let source = r#" fn bad(): Int with {Time} is deterministic = Time.now() "#; let result = eval(source); assert!(result.is_err()); assert!(result.unwrap_err().contains("deterministic but uses non-deterministic effects")); } #[test] fn test_behavioral_commutative_add() { // Addition is commutative let source = r#" fn add(a: Int, b: Int): Int is commutative = a + b let result = add(3, 5) "#; assert_eq!(eval(source).unwrap(), "8"); } #[test] fn test_behavioral_commutative_mul() { // Multiplication is commutative let source = r#" fn mul(a: Int, b: Int): Int is commutative = a * b let result = mul(3, 5) "#; assert_eq!(eval(source).unwrap(), "15"); } #[test] fn test_behavioral_commutative_subtract_error() { // Subtraction is NOT commutative let source = r#" fn sub(a: Int, b: Int): Int is commutative = a - b "#; let result = eval(source); assert!(result.is_err()); assert!(result.unwrap_err().contains("commutative but its body is not")); } #[test] fn test_behavioral_commutative_wrong_params_error() { // Commutative requires exactly 2 params let source = r#" fn bad(a: Int): Int is commutative = a "#; let result = eval(source); assert!(result.is_err()); assert!(result.unwrap_err().contains("has 1 parameters (expected 2)")); } #[test] fn test_behavioral_idempotent_identity() { // Identity function is idempotent let source = r#" fn identity(x: Int): Int is idempotent = x let result = identity(42) "#; assert_eq!(eval(source).unwrap(), "42"); } #[test] fn test_behavioral_idempotent_constant() { // Constant function is idempotent let source = r#" fn always42(x: Int): Int is idempotent = 42 let result = always42(100) "#; assert_eq!(eval(source).unwrap(), "42"); } #[test] fn test_behavioral_idempotent_clamping() { // Clamping pattern is idempotent let source = r#" fn clampPositive(x: Int): Int is idempotent = if x < 0 then 0 else x let result = clampPositive(-5) "#; assert_eq!(eval(source).unwrap(), "0"); } #[test] fn test_behavioral_idempotent_increment_error() { // Increment is NOT idempotent let source = r#" fn increment(x: Int): Int is idempotent = x + 1 "#; let result = eval(source); assert!(result.is_err()); assert!(result.unwrap_err().contains("idempotent but could not be verified")); } #[test] fn test_behavioral_total_non_recursive() { // Non-recursive function is total let source = r#" fn double(x: Int): Int is total = x * 2 let result = double(21) "#; assert_eq!(eval(source).unwrap(), "42"); } #[test] fn test_behavioral_total_structural_recursion() { // Structural recursion (n - 1) is total let source = r#" fn factorial(n: Int): Int is total = if n <= 1 then 1 else n * factorial(n - 1) let result = factorial(5) "#; assert_eq!(eval(source).unwrap(), "120"); } #[test] fn test_behavioral_total_infinite_loop_error() { // Infinite loop is NOT total let source = r#" fn loop(x: Int): Int is total = loop(x) "#; let result = eval(source); assert!(result.is_err()); assert!(result.unwrap_err().contains("may not terminate")); } #[test] fn test_behavioral_total_with_fail_error() { // Total function cannot use Fail effect let source = r#" effect Fail { fn fail(msg: String): Unit } fn bad(x: Int): Int with {Fail} is total = if x < 0 then { Fail.fail("negative"); 0 } else x "#; let result = eval(source); assert!(result.is_err()); assert!(result.unwrap_err().contains("total but uses the Fail effect")); } #[test] fn test_where_clause_parsing() { // Where clause property constraints are parsed correctly let source = r#" fn retry(action: F, times: Int): Int where F is idempotent = times fn idempotentFn(): Int is idempotent = 42 let x = 1 "#; // Just verify it parses and type-checks without errors assert!(eval(source).is_ok()); } #[test] fn test_schema_version_preserved() { // Version annotations are preserved in type annotations let source = r#" fn processV1(x: Int @v1): Int = x let value: Int @v1 = 42 let result = processV1(value) "#; assert!(eval(source).is_ok()); } #[test] fn test_schema_version_mismatch_error() { // Version mismatch should produce an error when versions don't match let source = r#" fn expectV2(x: Int @v2): Int = x let oldValue: Int @v1 = 42 let result = expectV2(oldValue) "#; let result = eval(source); assert!(result.is_err()); assert!(result.unwrap_err().contains("Version mismatch")); } #[test] fn test_schema_version_atleast() { // @v2+ should accept v2 or higher let source = r#" fn processAtLeastV2(x: Int @v2+): Int = x let value: Int @v2 = 42 let result = processAtLeastV2(value) "#; assert!(eval(source).is_ok()); } #[test] fn test_schema_version_latest() { // @latest should be compatible with any version let source = r#" fn processLatest(x: Int @latest): Int = x let value: Int @v1 = 42 let result = processLatest(value) "#; assert!(eval(source).is_ok()); } // 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"); } #[test] fn test_resumable_handler() { // Test that resume works in handler bodies let source = r#" effect Counter { fn increment(): Int } fn useCounter(): Int with {Counter} = { let a = Counter.increment() let b = Counter.increment() a + b } handler countingHandler: Counter { fn increment() = resume(10) } let result = run useCounter() with { Counter = countingHandler } "#; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); // Each increment returns 10, so a + b = 10 + 10 = 20 assert_eq!(result, "20"); } #[test] fn test_resume_outside_handler_fails() { // Resume outside handler should fail at runtime let source = r#" fn bad(): Int = resume(42) let result = bad() "#; let result = run_with_effects(source, Value::Unit, Value::Unit); assert!(result.is_err()); let err_msg = result.unwrap_err(); assert!(err_msg.contains("outside") || err_msg.contains("Resume"), "Error should mention resume outside handler: {}", err_msg); } // Schema Evolution tests #[test] fn test_schema_versioned() { let source = r#" let user = Schema.versioned("User", 1, { name: "Alice", age: 30 }) let version = Schema.getVersion(user) "#; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "1"); } #[test] fn test_schema_migrate_same_version() { let source = r#" let user = Schema.versioned("User", 2, { name: "Bob" }) let migrated = Schema.migrate(user, 2) let version = Schema.getVersion(migrated) "#; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "2"); } #[test] fn test_schema_migrate_upgrade() { let source = r#" let user = Schema.versioned("User", 1, { name: "Charlie" }) let migrated = Schema.migrate(user, 3) let version = Schema.getVersion(migrated) "#; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "3"); } #[test] fn test_schema_migrate_downgrade_fails() { let source = r#" let user = Schema.versioned("User", 3, { name: "Dave" }) let migrated = Schema.migrate(user, 1) "#; let result = run_with_effects(source, Value::Unit, Value::Unit); assert!(result.is_err()); assert!(result.unwrap_err().contains("downgrade")); } // HttpServer effect type-checking tests #[test] fn test_http_server_typecheck() { // Verify HttpServer effect operations type-check correctly // We can't actually run the server in tests, but we can verify types use crate::parser::Parser; use crate::typechecker::TypeChecker; let source = r#" // Function that uses HttpServer effect fn handleRequest(req: { method: String, path: String, body: String, headers: List<(String, String)> }): Unit with {HttpServer} = HttpServer.respond(200, "Hello!") fn serveOne(port: Int): Unit with {HttpServer} = { HttpServer.listen(port) let req = HttpServer.accept() handleRequest(req) HttpServer.stop() } "#; let program = Parser::parse_source(source).expect("parse failed"); let mut checker = TypeChecker::new(); let result = checker.check_program(&program); assert!(result.is_ok(), "HttpServer type checking failed: {:?}", result); } // Behavioral types tests #[test] fn test_behavioral_pure_function() { use crate::parser::Parser; use crate::typechecker::TypeChecker; let source = r#" fn add(a: Int, b: Int): Int is pure = a + b fn square(x: Int): Int is pure = x * x let result = add(square(3), square(4)) "#; let program = Parser::parse_source(source).expect("parse failed"); let mut checker = TypeChecker::new(); assert!(checker.check_program(&program).is_ok()); } #[test] fn test_behavioral_deterministic() { use crate::parser::Parser; use crate::typechecker::TypeChecker; let source = r#" fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1) let result = factorial(5) "#; let program = Parser::parse_source(source).expect("parse failed"); let mut checker = TypeChecker::new(); assert!(checker.check_program(&program).is_ok()); } #[test] fn test_behavioral_commutative() { use crate::parser::Parser; use crate::typechecker::TypeChecker; // Commutative verification works with arithmetic operators let source = r#" fn add(a: Int, b: Int): Int is commutative = a + b fn mul(a: Int, b: Int): Int is commutative = a * b let result = add(10, 20) "#; let program = Parser::parse_source(source).expect("parse failed"); let mut checker = TypeChecker::new(); assert!(checker.check_program(&program).is_ok()); } #[test] fn test_behavioral_idempotent() { use crate::parser::Parser; use crate::typechecker::TypeChecker; // Idempotent verification works with abs pattern let source = r#" fn absolute(x: Int): Int is idempotent = if x < 0 then 0 - x else x let result = absolute(-42) "#; let program = Parser::parse_source(source).expect("parse failed"); let mut checker = TypeChecker::new(); assert!(checker.check_program(&program).is_ok()); } #[test] fn test_behavioral_total() { use crate::parser::Parser; use crate::typechecker::TypeChecker; let source = r#" fn sumTo(n: Int): Int is total = if n <= 0 then 0 else n + sumTo(n - 1) fn power(base: Int, exp: Int): Int is total = if exp <= 0 then 1 else base * power(base, exp - 1) let result = sumTo(10) "#; let program = Parser::parse_source(source).expect("parse failed"); let mut checker = TypeChecker::new(); assert!(checker.check_program(&program).is_ok()); } // Math module tests #[test] fn test_math_abs() { let (result, _) = run_with_effects("let x = Math.abs(-42)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "42"); let (result, _) = run_with_effects("let x = Math.abs(42)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "42"); } #[test] fn test_math_min_max() { let (result, _) = run_with_effects("let x = Math.min(3, 7)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "3"); let (result, _) = run_with_effects("let x = Math.max(3, 7)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "7"); } #[test] fn test_math_sqrt() { let (result, _) = run_with_effects("let x = Math.sqrt(16)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "4"); } #[test] fn test_math_pow() { let (result, _) = run_with_effects("let x = Math.pow(2, 10)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "1024"); } #[test] fn test_math_floor_ceil_round() { let (result, _) = run_with_effects("let x = Math.floor(3.7)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "3"); let (result, _) = run_with_effects("let x = Math.ceil(3.2)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "4"); let (result, _) = run_with_effects("let x = Math.round(3.5)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "4"); } // List module additional functions #[test] fn test_list_is_empty() { let (result, _) = run_with_effects("let x = List.isEmpty([])", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "true"); let (result, _) = run_with_effects("let x = List.isEmpty([1, 2])", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "false"); } #[test] fn test_list_find() { let source = "let x = List.find([1, 2, 3, 4, 5], fn(x: Int): Bool => x > 3)"; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "Some(4)"); let source = "let x = List.find([1, 2, 3], fn(x: Int): Bool => x > 10)"; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "None"); } #[test] fn test_list_any_all() { let source = "let x = List.any([1, 2, 3], fn(x: Int): Bool => x > 2)"; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "true"); let source = "let x = List.all([1, 2, 3], fn(x: Int): Bool => x > 0)"; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "true"); let source = "let x = List.all([1, 2, 3], fn(x: Int): Bool => x > 2)"; let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "false"); } #[test] fn test_list_take_drop() { let (result, _) = run_with_effects("let x = List.take([1, 2, 3, 4, 5], 3)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "[1, 2, 3]"); let (result, _) = run_with_effects("let x = List.drop([1, 2, 3, 4, 5], 2)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "[3, 4, 5]"); } // String module additional functions #[test] fn test_string_starts_ends_with() { let (result, _) = run_with_effects("let x = String.startsWith(\"hello\", \"he\")", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "true"); let (result, _) = run_with_effects("let x = String.endsWith(\"hello\", \"lo\")", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "true"); } #[test] fn test_string_case_conversion() { let (result, _) = run_with_effects("let x = String.toUpper(\"hello\")", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "\"HELLO\""); let (result, _) = run_with_effects("let x = String.toLower(\"HELLO\")", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "\"hello\""); } #[test] fn test_string_substring() { let (result, _) = run_with_effects("let x = String.substring(\"hello world\", 0, 5)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "\"hello\""); let (result, _) = run_with_effects("let x = String.substring(\"hello\", 2, 4)", Value::Unit, Value::Unit).unwrap(); assert_eq!(result, "\"ll\""); } } // 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"); } } // Integration tests for example files mod example_tests { use super::*; use std::fs; fn check_file(path: &str) -> Result<(), String> { let source = fs::read_to_string(path) .map_err(|e| format!("Failed to read {}: {}", path, e))?; let program = Parser::parse_source(&source) .map_err(|e| format!("Parse error in {}: {}", path, e))?; let mut checker = TypeChecker::new(); checker.check_program(&program).map_err(|errors| { format!("Type errors in {}:\n{}", path, errors.iter().map(|e| e.to_string()).collect::>().join("\n")) })?; Ok(()) } #[test] fn test_example_hello() { check_file("examples/hello.lux").unwrap(); } #[test] fn test_example_factorial() { check_file("examples/factorial.lux").unwrap(); } #[test] fn test_example_functional() { check_file("examples/functional.lux").unwrap(); } #[test] fn test_example_pipelines() { check_file("examples/pipelines.lux").unwrap(); } #[test] fn test_example_tailcall() { check_file("examples/tailcall.lux").unwrap(); } #[test] fn test_example_effects() { check_file("examples/effects.lux").unwrap(); } #[test] fn test_example_handlers() { check_file("examples/handlers.lux").unwrap(); } #[test] fn test_example_builtin_effects() { check_file("examples/builtin_effects.lux").unwrap(); } #[test] fn test_example_datatypes() { check_file("examples/datatypes.lux").unwrap(); } #[test] fn test_example_generics() { check_file("examples/generics.lux").unwrap(); } #[test] fn test_example_traits() { check_file("examples/traits.lux").unwrap(); } #[test] fn test_example_interpolation() { check_file("examples/interpolation.lux").unwrap(); } #[test] fn test_example_behavioral() { check_file("examples/behavioral.lux").unwrap(); } #[test] fn test_example_behavioral_types() { check_file("examples/behavioral_types.lux").unwrap(); } #[test] fn test_example_versioning() { check_file("examples/versioning.lux").unwrap(); } #[test] fn test_example_schema_evolution() { check_file("examples/schema_evolution.lux").unwrap(); } #[test] fn test_example_file_io() { check_file("examples/file_io.lux").unwrap(); } #[test] fn test_example_json() { check_file("examples/json.lux").unwrap(); } #[test] fn test_example_random() { check_file("examples/random.lux").unwrap(); } #[test] fn test_example_statemachine() { check_file("examples/statemachine.lux").unwrap(); } #[test] fn test_example_shell() { check_file("examples/shell.lux").unwrap(); } #[test] fn test_example_http() { check_file("examples/http.lux").unwrap(); } #[test] fn test_example_http_server() { check_file("examples/http_server.lux").unwrap(); } #[test] fn test_example_jit_test() { check_file("examples/jit_test.lux").unwrap(); } #[test] fn test_project_json_parser() { check_file("projects/json-parser/main.lux").unwrap(); } #[test] fn test_project_markdown_converter() { check_file("projects/markdown-converter/main.lux").unwrap(); } #[test] fn test_project_todo_app() { check_file("projects/todo-app/main.lux").unwrap(); } #[test] fn test_project_mini_interpreter() { check_file("projects/mini-interpreter/main.lux").unwrap(); } #[test] fn test_project_guessing_game() { check_file("projects/guessing-game/main.lux").unwrap(); } #[test] fn test_project_rest_api() { check_file("projects/rest-api/main.lux").unwrap(); } } }