//! Lux — Make the important things visible. //! //! A functional programming language with first-class effects, schema evolution, //! and behavioral types. See `lux philosophy` or docs/PHILOSOPHY.md. mod analysis; mod ast; mod codegen; mod debugger; mod diagnostics; mod exhaustiveness; mod formatter; mod interpreter; mod lexer; mod linter; mod lsp; mod modules; mod package; mod parser; mod registry; mod schema; mod symbol_table; mod typechecker; mod types; use diagnostics::{render, c, bc, colors}; 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 = env!("CARGO_PKG_VERSION"); 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 :doc Show documentation for a function/type :browse List exports of a module (List, String, Option, etc.) :env Show user-defined bindings :clear Clear the environment :load Load and execute a file :reload, :r Reload the last loaded file :trace on/off Enable/disable effect tracing :traces Show recorded effect traces :ast Show the AST of an expression (for debugging) Keyboard: Tab Autocomplete Ctrl-C Cancel current input Ctrl-D Exit Up/Down Browse history Ctrl-R Search history Effects: All code in the REPL runs with Console, File, and other standard effects. Use :trace on to see effect invocations during execution. Examples: > let x = 42 > x + 1 43 > fn double(n: Int): Int = n * 2 > :type double double : fn(Int) -> Int > :load myfile.lux > :reload > 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!("{}", bc(colors::GREEN, &format!("Lux {}", VERSION))); } "fmt" | "f" => { // Format files (auto-discovers if no file specified) format_files(&args[2..]); } "lint" | "l" => { // Lint files lint_files(&args[2..]); } "test" | "t" => { // 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" | "k" => { // Type check files (auto-discovers if no file specified) check_files(&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..]); } "registry" => { // Run package registry server let storage = args.iter() .position(|a| a == "--storage" || a == "-s") .and_then(|i| args.get(i + 1)) .map(|s| s.as_str()) .unwrap_or("./lux-registry"); let bind = args.iter() .position(|a| a == "--bind" || a == "-b") .and_then(|i| args.get(i + 1)) .map(|s| s.as_str()) .unwrap_or("127.0.0.1:8080"); if let Err(e) = registry::run_registry_server(storage, bind) { eprintln!("Registry server error: {}", e); std::process::exit(1); } } "serve" | "s" => { // Static file server (like python -m http.server) let port = args.iter() .position(|a| a == "--port" || a == "-p") .and_then(|i| args.get(i + 1)) .and_then(|s| s.parse::().ok()) .unwrap_or(8080); let port_value_idx = args.iter() .position(|a| a == "--port" || a == "-p") .map(|i| i + 1); let dir = args.iter().enumerate() .skip(2) .filter(|(i, a)| !a.starts_with('-') && Some(*i) != port_value_idx) .map(|(_, a)| a.as_str()) .next() .unwrap_or("."); serve_static_files(dir, port); } "compile" | "c" => { // 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]"); eprintln!(" lux compile --watch"); std::process::exit(1); } let run_after = args.iter().any(|a| a == "--run"); let emit_c = args.iter().any(|a| a == "--emit-c"); let watch = args.iter().any(|a| a == "--watch"); 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); } if watch { // Build the args to replay for each recompilation (without --watch) let compile_args: Vec = args.iter() .skip(1) .filter(|a| a.as_str() != "--watch") .cloned() .collect(); watch_and_rerun(&args[2], &compile_args); } } "repl" => { // Start REPL run_repl(); } "doc" => { // Generate API documentation generate_docs(&args[2..]); } "philosophy" => { print_philosophy(); } cmd => { // Check if it looks like a command typo if !std::path::Path::new(cmd).exists() && !cmd.starts_with('-') && !cmd.contains('.') && !cmd.contains('/') { let known_commands = vec![ "fmt", "lint", "test", "watch", "init", "check", "debug", "pkg", "registry", "serve", "compile", "doc", "repl", "philosophy", ]; let suggestions = diagnostics::find_similar_names(cmd, known_commands.into_iter(), 2); if !suggestions.is_empty() { if let Some(hint) = diagnostics::format_did_you_mean(&suggestions) { eprintln!("{} Unknown command '{}'. {}", c(colors::YELLOW, "warning:"), cmd, c(colors::YELLOW, &hint)); std::process::exit(1); } } } // Run as file run_file(cmd); } } } else { // No arguments — show help print_help(); } } fn print_help() { println!("{}", bc(colors::GREEN, &format!("Lux {}", VERSION))); println!("{}", c(colors::DIM, "Make the important things visible.")); println!(); println!(" {} Effects in types — see what code does", c(colors::DIM, "·")); println!(" {} Composition over configuration — no DI frameworks", c(colors::DIM, "·")); println!(" {} Safety without ceremony — inference where it helps", c(colors::DIM, "·")); println!(" {} One right way — opinionated formatter, integrated tools", c(colors::DIM, "·")); println!(); println!("{}", bc("", "Usage:")); println!(); println!(" {} Show this help", bc(colors::CYAN, "lux")); println!(" {} Start the REPL", bc(colors::CYAN, "lux repl")); println!(" {} {} Run a file (interpreter)", bc(colors::CYAN, "lux"), c(colors::YELLOW, "")); println!(" {} {} {} Compile to native binary", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "")); println!(" {} {} {} {} Compile with output name", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, ""), c(colors::YELLOW, "-o app")); println!(" {} {} {} {} Compile and execute", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, ""), c(colors::YELLOW, "--run")); println!(" {} {} {} {} Output C code", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, ""), c(colors::YELLOW, "--emit-c")); println!(" {} {} {} {} Compile to JavaScript", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, ""), c(colors::YELLOW, "--target js")); println!(" {} {} {} Format files {}", bc(colors::CYAN, "lux"), bc(colors::CYAN, "fmt"), c(colors::YELLOW, "[file] [--check]"), c(colors::DIM, "(alias: f)")); println!(" {} {} {} Lint files {}", bc(colors::CYAN, "lux"), bc(colors::CYAN, "lint"), c(colors::YELLOW, "[file]"), c(colors::DIM, "(alias: l)")); println!(" {} {} {} Type check files {}", bc(colors::CYAN, "lux"), bc(colors::CYAN, "check"), c(colors::YELLOW, "[file]"), c(colors::DIM, "(alias: k)")); println!(" {} {} {} Run tests {}", bc(colors::CYAN, "lux"), bc(colors::CYAN, "test"), c(colors::YELLOW, "[pattern]"), c(colors::DIM, "(alias: t)")); println!(" {} {} {} Watch and re-run on changes", bc(colors::CYAN, "lux"), bc(colors::CYAN, "watch"), c(colors::YELLOW, "")); println!(" {} {} {} Start interactive debugger", bc(colors::CYAN, "lux"), bc(colors::CYAN, "debug"), c(colors::YELLOW, "")); println!(" {} {} {} Initialize a new project", bc(colors::CYAN, "lux"), bc(colors::CYAN, "init"), c(colors::YELLOW, "[name]")); println!(" {} {} {} Package manager {}", bc(colors::CYAN, "lux"), bc(colors::CYAN, "pkg"), c(colors::YELLOW, ""), c(colors::DIM, "(install, add, remove, list, update)")); println!(" {} {} Start package registry server", bc(colors::CYAN, "lux"), bc(colors::CYAN, "registry")); println!(" {} {} {} Start static file server {}", bc(colors::CYAN, "lux"), bc(colors::CYAN, "serve"), c(colors::YELLOW, "[dir]"), c(colors::DIM, "(alias: s)")); println!(" {} {} {} Generate API documentation", bc(colors::CYAN, "lux"), bc(colors::CYAN, "doc"), c(colors::YELLOW, "[file] [-o dir]")); println!(" {} {} Show language philosophy", bc(colors::CYAN, "lux"), bc(colors::CYAN, "philosophy")); println!(" {} {} Start LSP server", bc(colors::CYAN, "lux"), c(colors::YELLOW, "--lsp")); println!(" {} {} Show this help", bc(colors::CYAN, "lux"), c(colors::YELLOW, "--help")); println!(" {} {} Show version", bc(colors::CYAN, "lux"), c(colors::YELLOW, "--version")); } fn print_philosophy() { println!("{}", bc(colors::GREEN, &format!("The Lux Philosophy"))); println!(); println!(" {}", bc("", "Make the important things visible.")); println!(); println!(" Most languages hide what matters most in production: what code"); println!(" can do, how data changes over time, and what guarantees functions"); println!(" provide. Lux makes all three first-class, compiler-checked features."); println!(); println!(" {} {}", bc(colors::CYAN, "1. Explicit over implicit"), c(colors::DIM, "— effects in types, not hidden behind interfaces")); println!(" fn processOrder(order: Order): Receipt {} {}", c(colors::YELLOW, "with {Database, Email}"), c(colors::DIM, "// signature IS documentation")); println!(); println!(" {} {}", bc(colors::CYAN, "2. Composition over configuration"), c(colors::DIM, "— no DI frameworks, no monad transformers")); println!(" run app() {} {}", c(colors::YELLOW, "with { Database = mock, Http = mock }"), c(colors::DIM, "// swap handlers, not libraries")); println!(); println!(" {} {}", bc(colors::CYAN, "3. Safety without ceremony"), c(colors::DIM, "— type inference where it helps, annotations where they document")); println!(" let x = 42 {}", c(colors::DIM, "// inferred")); println!(" fn f(x: Int): Int = x * 2 {}", c(colors::DIM, "// annotated: API contract")); println!(); println!(" {} {}", bc(colors::CYAN, "4. Practical over academic"), c(colors::DIM, "— ML semantics in C-family syntax, no monads to learn")); println!(" {} {} {}", c(colors::DIM, "fn main(): Unit"), c(colors::YELLOW, "with {Console}"), c(colors::DIM, "= Console.print(\"Hello!\")")); println!(); println!(" {} {}", bc(colors::CYAN, "5. One right way"), c(colors::DIM, "— opinionated formatter, integrated tooling, built-in testing")); println!(" lux fmt | lux lint | lux check | lux test | lux compile"); println!(); println!(" {} {}", bc(colors::CYAN, "6. Tools are the language"), c(colors::DIM, "— formatter knows the AST, linter knows the types, LSP knows the effects")); println!(); println!(" See {} for the full philosophy with language comparisons.", c(colors::CYAN, "docs/PHILOSOPHY.md")); } fn format_files(args: &[String]) { use formatter::{format, FormatConfig}; use std::path::Path; use std::time::Instant; let start = Instant::now(); let check_only = args.iter().any(|a| a == "--check"); let pattern = args.iter().find(|a| !a.starts_with('-')).map(|s| s.as_str()); // Collect files to format let mut files_to_format = Vec::new(); // If a specific file is given, use it if let Some(p) = pattern { if Path::new(p).is_file() { files_to_format.push(std::path::PathBuf::from(p)); } } // If no specific file found, auto-discover if files_to_format.is_empty() { // Current directory (non-recursive for src/) if Path::new("src").is_dir() { collect_lux_files("src", pattern, &mut files_to_format); } // Also check current directory for top-level files collect_lux_files_nonrecursive(".", pattern, &mut files_to_format); // examples/ subdirectory if Path::new("examples").is_dir() { collect_lux_files("examples", pattern, &mut files_to_format); } // tests/ subdirectory if Path::new("tests").is_dir() { collect_lux_files("tests", pattern, &mut files_to_format); } } if files_to_format.is_empty() { println!("No .lux files found."); println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/")); return; } // Sort for consistent output files_to_format.sort(); let config = FormatConfig::default(); let mut formatted_count = 0; let mut unchanged_count = 0; let mut error_count = 0; let mut would_reformat = Vec::new(); for file_path in &files_to_format { let path = file_path.to_string_lossy().to_string(); let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(e) => { eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); error_count += 1; continue; } }; let formatted = match format(&source, &config) { Ok(f) => f, Err(e) => { eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); error_count += 1; continue; } }; if check_only { if source != formatted { would_reformat.push(path); } else { unchanged_count += 1; } } else if source != formatted { if let Err(e) = std::fs::write(file_path, &formatted) { eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); error_count += 1; continue; } println!(" {} {}", c(colors::CYAN, "Formatted"), path); formatted_count += 1; } else { unchanged_count += 1; } } let elapsed = start.elapsed(); let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())); println!(); if check_only { if !would_reformat.is_empty() { println!("Files that would be reformatted:"); for path in &would_reformat { println!(" {}", c(colors::YELLOW, path)); } println!(); println!("{} {} would be reformatted, {} already formatted {}", c(colors::YELLOW, "\u{2717}"), would_reformat.len(), unchanged_count, time_str); std::process::exit(1); } else { println!("{} All {} files are correctly formatted {}", c(colors::GREEN, "\u{2713}"), unchanged_count, time_str); } } else if error_count > 0 { let mut parts = Vec::new(); if formatted_count > 0 { parts.push(format!("{} formatted", formatted_count)); } if unchanged_count > 0 { parts.push(format!("{} unchanged", unchanged_count)); } parts.push(format!("{} errors", error_count)); println!("{} {} {}", c(colors::RED, "\u{2717}"), parts.join(", "), time_str); std::process::exit(1); } else if formatted_count > 0 { println!("{} {} formatted, {} unchanged {}", c(colors::GREEN, "\u{2713}"), formatted_count, unchanged_count, time_str); } else { println!("{} All {} files already formatted {}", c(colors::GREEN, "\u{2713}"), unchanged_count, time_str); } } fn lint_files(args: &[String]) { use linter::{LintConfig, LintLevel, Linter}; use std::path::Path; use std::time::Instant; let start = Instant::now(); let explain = args.iter().any(|a| a == "--explain"); let _fix = args.iter().any(|a| a == "--fix"); let pattern = args .iter() .find(|a| !a.starts_with('-')) .map(|s| s.as_str()); // Collect files to lint let mut files_to_lint = Vec::new(); if let Some(p) = pattern { if Path::new(p).is_file() { files_to_lint.push(std::path::PathBuf::from(p)); } } if files_to_lint.is_empty() { if Path::new("src").is_dir() { collect_lux_files("src", pattern, &mut files_to_lint); } collect_lux_files_nonrecursive(".", pattern, &mut files_to_lint); if Path::new("examples").is_dir() { collect_lux_files("examples", pattern, &mut files_to_lint); } if Path::new("tests").is_dir() { collect_lux_files("tests", pattern, &mut files_to_lint); } } if files_to_lint.is_empty() { println!("No .lux files found."); println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/")); return; } files_to_lint.sort(); let config = LintConfig::default(); let mut total_warnings = 0; let mut total_errors = 0; let mut files_clean = 0; for file_path in &files_to_lint { let path = file_path.to_string_lossy().to_string(); let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(e) => { eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); total_errors += 1; continue; } }; let program = match Parser::parse_source(&source) { Ok(p) => p, Err(e) => { eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, c(colors::RED, &format!("parse error: {}", e))); total_errors += 1; continue; } }; let linter = Linter::new(config.clone(), &source); let lints = linter.lint(&program); if lints.is_empty() { files_clean += 1; } else { let errors = lints.iter().filter(|l| l.level == LintLevel::Deny).count(); let warnings = lints .iter() .filter(|l| l.level == LintLevel::Warn) .count(); total_errors += errors; total_warnings += warnings; if explain { eprint!("{}", linter::render_lints(&lints, &source, Some(&path))); } else { // Compact output: one line per lint for lint in &lints { let level_str = match lint.level { LintLevel::Deny => c(colors::RED, "error"), LintLevel::Warn => c(colors::YELLOW, "warning"), LintLevel::Allow => continue, }; let lint_code = c( colors::DIM, &format!("[{}/{}]", lint.id.category().category_name(), lint.id.name()), ); if lint.span.start > 0 || lint.span.end > 0 { let (line, col) = diagnostics::offset_to_line_col(&source, lint.span.start); eprintln!( " {}{} {}:{}:{}: {}", level_str, lint_code, path, line, col, lint.message ); } else { eprintln!(" {}{} {}: {}", level_str, lint_code, path, lint.message); } } } } } let elapsed = start.elapsed(); let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())); println!(); if total_errors > 0 || total_warnings > 0 { let mut parts = Vec::new(); if total_errors > 0 { parts.push(c(colors::RED, &format!("{} errors", total_errors))); } if total_warnings > 0 { parts.push(c( colors::YELLOW, &format!("{} warnings", total_warnings), )); } parts.push(format!("{} files clean", files_clean)); let icon = if total_errors > 0 { c(colors::RED, "\u{2717}") } else { c(colors::YELLOW, "\u{26a0}") }; println!("{} {} {}", icon, parts.join(", "), time_str); if total_errors > 0 { std::process::exit(1); } } else { println!( "{} {} files clean {}", c(colors::GREEN, "\u{2713}"), files_clean, time_str ); } } fn check_files(args: &[String]) { use linter::{LintConfig, LintLevel, Linter}; use modules::ModuleLoader; use std::path::Path; use std::time::Instant; let start = Instant::now(); let pattern = args.first().map(|s| s.as_str()); // Collect files to check let mut files_to_check = Vec::new(); // If a specific file is given, use it if let Some(p) = pattern { if Path::new(p).is_file() { files_to_check.push(std::path::PathBuf::from(p)); } } // If no specific file found, auto-discover if files_to_check.is_empty() { // Current directory (non-recursive for src/) if Path::new("src").is_dir() { collect_lux_files("src", pattern, &mut files_to_check); } // Also check current directory for top-level files collect_lux_files_nonrecursive(".", pattern, &mut files_to_check); // examples/ subdirectory if Path::new("examples").is_dir() { collect_lux_files("examples", pattern, &mut files_to_check); } // tests/ subdirectory if Path::new("tests").is_dir() { collect_lux_files("tests", pattern, &mut files_to_check); } } if files_to_check.is_empty() { println!("No .lux files found."); println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/")); return; } // Sort for consistent output files_to_check.sort(); let mut passed = 0; let mut failed = 0; let mut total_warnings = 0; let lint_config = LintConfig::default(); for file_path in &files_to_check { let path = file_path.to_string_lossy().to_string(); let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(e) => { eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); failed += 1; continue; } }; 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!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); failed += 1; continue; } }; let mut checker = TypeChecker::new(); if let Err(errors) = checker.check_program_with_modules(&program, &loader) { eprintln!("{} {}", c(colors::RED, "\u{2717}"), path); for error in errors { let diagnostic = error.to_diagnostic(); eprint!("{}", render(&diagnostic, &source, Some(&path))); } failed += 1; } else { // Type check passed — also run lints let linter = Linter::new(lint_config.clone(), &source); let lints = linter.lint(&program); let warnings = lints.iter().filter(|l| l.level == LintLevel::Warn).count(); let errors = lints.iter().filter(|l| l.level == LintLevel::Deny).count(); if errors > 0 { eprintln!("{} {}", c(colors::RED, "\u{2717}"), path); for lint in &lints { if lint.level == LintLevel::Deny { let lint_code = c( colors::DIM, &format!("[{}/{}]", lint.id.category().category_name(), lint.id.name()), ); if lint.span.start > 0 || lint.span.end > 0 { let (line, col) = diagnostics::offset_to_line_col(&source, lint.span.start); eprintln!(" {}{} {}:{}:{}: {}", c(colors::RED, "error"), lint_code, path, line, col, lint.message); } else { eprintln!(" {}{} {}: {}", c(colors::RED, "error"), lint_code, path, lint.message); } } } failed += 1; } else if warnings > 0 { println!("{} {} {}", c(colors::YELLOW, "\u{26a0}"), path, c(colors::DIM, &format!("({} lint warnings)", warnings))); total_warnings += warnings; passed += 1; } else { println!("{} {}", c(colors::GREEN, "\u{2713}"), path); passed += 1; } } } let elapsed = start.elapsed(); let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())); println!(); if failed > 0 { let mut summary = format!("{} {} passed, {} failed", c(colors::RED, "\u{2717}"), passed, failed); if total_warnings > 0 { summary.push_str(&format!(", {}", c(colors::YELLOW, &format!("{} warnings", total_warnings)))); } println!("{} {}", summary, time_str); std::process::exit(1); } else { let mut summary = format!("{} {} passed", c(colors::GREEN, "\u{2713}"), passed); if total_warnings > 0 { summary.push_str(&format!(", {}", c(colors::YELLOW, &format!("{} warnings", total_warnings)))); } println!("{} {}", summary, time_str); } } fn collect_lux_files_nonrecursive(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.is_file() && path.extension().map(|e| e == "lux").unwrap_or(false) { if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if pattern.map(|p| name.contains(p)).unwrap_or(true) { files.push(path); } } } } } /// Find a C compiler. Priority: $CC env var, build-time embedded path, PATH search. fn find_c_compiler() -> String { // 1. Explicit env var if let Ok(cc) = std::env::var("CC") { if !cc.is_empty() { return cc; } } // 2. Path captured at build time (e.g. absolute nix store path) let built_in = env!("LUX_CC_PATH"); if !built_in.is_empty() && std::path::Path::new(built_in).exists() { return built_in.to_string(); } // 3. Search PATH for name in &["cc", "gcc", "clang"] { if let Ok(output) = std::process::Command::new("which").arg(name).output() { if output.status.success() { if let Ok(p) = String::from_utf8(output.stdout) { let p = p.trim(); if !p.is_empty() { return p.to_string(); } } } } } // 4. Last resort "cc".to_string() } 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; use std::time::Instant; let start = Instant::now(); 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 '{}': {}", c(colors::RED, "error:"), 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!("{} {}", c(colors::RED, "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, loader.module_cache()) { Ok(code) => code, Err(e) => { eprintln!("{} C codegen: {}", c(colors::RED, "error:"), e); eprintln!(); eprintln!("{}", c(colors::DIM, "Note: The C backend supports functions, closures, ADTs,")); eprintln!("{}", c(colors::DIM, "pattern matching, lists, and Console.print.")); eprintln!("{}", c(colors::DIM, "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!("{} writing '{}': {}", c(colors::RED, "error:"), out_path, e); std::process::exit(1); } let elapsed = start.elapsed(); eprintln!("{} Emitted C to {} {}", c(colors::GREEN, "\u{2713}"), bc(colors::CYAN, out_path), c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()))); } 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!("{} writing temp file: {}", c(colors::RED, "error:"), e); std::process::exit(1); } // Find C compiler: $CC env var > embedded build-time path > PATH search let cc = find_c_compiler(); let compile_result = Command::new(&cc) .args(["-O2", "-o"]) .arg(&output_bin) .arg(&temp_c) .arg("-lm") .output(); match compile_result { Ok(output) => { if !output.status.success() { eprintln!("{} C compilation failed:", c(colors::RED, "error:")); eprintln!("{}", String::from_utf8_lossy(&output.stderr)); std::process::exit(1); } } Err(e) => { eprintln!("{} Failed to run C compiler '{}': {}", c(colors::RED, "error:"), cc, e); eprintln!("{}", c(colors::DIM, "Make sure gcc or clang is installed, or set CC environment variable.")); std::process::exit(1); } } let elapsed = start.elapsed(); 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: {}", c(colors::RED, "error:"), e); std::process::exit(1); } } } else { eprintln!("{} Compiled to {} {}", c(colors::GREEN, "\u{2713}"), bc(colors::CYAN, &output_bin.display().to_string()), c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()))); } } 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; use std::time::Instant; let start = Instant::now(); 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 '{}': {}", c(colors::RED, "error:"), 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!("{} {}", c(colors::RED, "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: {}", c(colors::RED, "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!("{} writing '{}': {}", c(colors::RED, "error:"), output_js.display(), e); std::process::exit(1); } let elapsed = start.elapsed(); 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: {}", c(colors::RED, "error:"), e); eprintln!("{}", c(colors::DIM, "Make sure Node.js is installed.")); std::process::exit(1); } } } else { eprintln!("{} Compiled to {} {}", c(colors::GREEN, "\u{2713}"), bc(colors::CYAN, &output_js.display().to_string()), c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()))); } } fn run_tests(args: &[String]) { use std::path::Path; use std::fs; use std::time::Instant; let start = Instant::now(); // 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!("{}", c(colors::DIM, "Test files should be named test_*.lux or *_test.lux")); println!("{}", c(colors::DIM, "Or contain functions named test_*")); return; } println!("{}\n", bc("", "Running tests...")); 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 (with module loading) let source = match fs::read_to_string(test_file) { Ok(s) => s, Err(e) => { println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &e.to_string())); total_failed += 1; continue; } }; use modules::ModuleLoader; let mut loader = ModuleLoader::new(); if let Some(parent) = test_file.parent() { loader.add_search_path(parent.to_path_buf()); } let program = match loader.load_source(&source, Some(test_file.as_path())) { Ok(p) => p, Err(e) => { println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &format!("parse error: {}", e))); total_failed += 1; continue; } }; // Type check with module support let mut checker = typechecker::TypeChecker::new(); if let Err(errors) = checker.check_program_with_modules(&program, &loader) { println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, "type error")); for err in errors { eprintln!(" {}", err); } total_failed += 1; continue; } // Get auto-generated migrations from typechecker let auto_migrations = checker.get_auto_migrations().clone(); // 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.register_auto_migrations(&auto_migrations); interp.reset_test_results(); match interp.run_with_modules(&program, &loader) { Ok(_) => { let results = interp.get_test_results(); if results.failed == 0 && results.passed == 0 { println!(" {} {} {}", c(colors::GREEN, "\u{2713}"), path_str, c(colors::DIM, "(no assertions)")); total_passed += 1; } else if results.failed == 0 { println!(" {} {} {}", c(colors::GREEN, "\u{2713}"), path_str, c(colors::DIM, &format!("({} assertions)", results.passed))); total_passed += results.passed; } else { println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &format!("{} passed, {} failed", 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!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &format!("runtime error: {}", e))); total_failed += 1; } } } else { // Run individual test functions println!(" {}:", c(colors::DIM, &path_str)); for test_name in &test_funcs { let mut interp = Interpreter::new(); interp.register_auto_migrations(&auto_migrations); interp.reset_test_results(); // First run the file to define all functions and load imports if let Err(e) = interp.run_with_modules(&program, &loader) { println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string())); 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!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string())); total_failed += 1; continue; } }; match interp.run(&call_program) { Ok(_) => { let results = interp.get_test_results(); if results.failed == 0 { println!(" {} {}", c(colors::GREEN, "\u{2713}"), test_name); total_passed += 1; } else { println!(" {} {}", c(colors::RED, "\u{2717}"), test_name); total_failed += 1; for failure in &results.failures { all_failures.push((path_str.clone(), test_name.clone(), failure.clone())); } } } Err(e) => { println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string())); total_failed += 1; } } } } } // Print failure details if !all_failures.is_empty() { println!("\n{}\n", bc(colors::RED, "--- Failures ---")); for (file, test, failure) in &all_failures { if test.is_empty() { println!("{}:", bc("", file)); } else { println!("{} - {}:", bc("", file), bc("", test)); } println!(" {}", failure.message); if let Some(expected) = &failure.expected { println!(" {} {}", c(colors::CYAN, "Expected:"), expected); } if let Some(actual) = &failure.actual { println!(" {} {}", c(colors::RED, "Actual: "), actual); } println!(); } } let elapsed = start.elapsed(); let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())); if total_failed > 0 { println!("{} {} passed, {} failed {}", c(colors::RED, "\u{2717}"), total_passed, total_failed, time_str); std::process::exit(1); } else { println!("{} {} passed {}", c(colors::GREEN, "\u{2713}"), total_passed, time_str); } } 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 collect_lux_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.is_dir() { // Recursively search subdirectories if let Some(name) = path.file_name().and_then(|n| n.to_str()) { // Skip hidden directories and common non-source dirs if !name.starts_with('.') && name != "target" && name != "node_modules" { collect_lux_files(path.to_str().unwrap_or(""), pattern, files); } } } else if path.extension().map(|e| e == "lux").unwrap_or(false) { if let Some(name) = path.file_name().and_then(|n| n.to_str()) { 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 watch_and_rerun(path: &str, compile_args: &[String]) { 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!(); println!("Watching {} for changes (Ctrl+C to stop)...", path); let mut last_modified = std::fs::metadata(file_path) .and_then(|m| m.modified()) .unwrap_or(SystemTime::UNIX_EPOCH); loop { std::thread::sleep(Duration::from_millis(500)); let modified = match std::fs::metadata(file_path).and_then(|m| m.modified()) { Ok(m) => m, Err(_) => continue, }; if modified > last_modified { last_modified = modified; // Clear screen print!("\x1B[2J\x1B[H"); println!("=== Compiling {} ===", path); println!(); let result = std::process::Command::new(std::env::current_exe().unwrap()) .args(compile_args) .status(); match result { Ok(status) if status.success() => { println!(); println!("=== Success ==="); } Ok(_) => { println!(); println!("=== Failed ==="); } Err(e) => { eprintln!("Error running compiler: {}", e); } } println!(); println!("Watching for changes..."); } } } fn serve_static_files(dir: &str, port: u16) { use std::io::{Write, BufRead, BufReader}; use std::net::TcpListener; use std::path::Path; let root = Path::new(dir).canonicalize().unwrap_or_else(|_| { eprintln!("{} Directory not found: {}", c(colors::RED, "error:"), dir); std::process::exit(1); }); let listener = match TcpListener::bind(format!("0.0.0.0:{}", port)) { Ok(l) => l, Err(e) => { if e.kind() == std::io::ErrorKind::AddrInUse { eprintln!("{} Port {} is already in use", c(colors::RED, "error:"), port); // Try to find an available port for try_port in (port + 1)..=(port + 3) { if TcpListener::bind(format!("0.0.0.0:{}", try_port)).is_ok() { eprintln!("{}", c(colors::CYAN, &format!(" Try: lux serve --port {}", try_port))); break; } } } else { eprintln!("{} Could not bind to port {}: {}", c(colors::RED, "error:"), port, e); } std::process::exit(1); } }; println!("{}", bc("", "Lux static file server")); println!(" {} {}", c(colors::DIM, "Serving:"), root.display()); println!(" {} {}", c(colors::DIM, "URL:"), bc(colors::CYAN, &format!("http://localhost:{}", port))); println!(); println!("{}", c(colors::DIM, "Press Ctrl+C to stop")); println!(); for stream in listener.incoming() { let Ok(mut stream) = stream else { continue }; // Read the request line let mut reader = BufReader::new(&stream); let mut request_line = String::new(); if reader.read_line(&mut request_line).is_err() { continue; } // Parse the request let parts: Vec<&str> = request_line.split_whitespace().collect(); if parts.len() < 2 { continue; } let method = parts[0]; let path = parts[1]; // Log the request (status is logged after response is determined) let log_method = bc("", method); let log_path = path.to_string(); // Only handle GET requests if method != "GET" { let response = "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 18\r\n\r\nMethod Not Allowed"; let _ = stream.write_all(response.as_bytes()); continue; } // Resolve the file path let mut file_path = root.clone(); let request_path = if path == "/" { "/index.html" } else { path }; // Prevent directory traversal for component in request_path.trim_start_matches('/').split('/') { if component == ".." { let response = "HTTP/1.1 403 Forbidden\r\nContent-Length: 9\r\n\r\nForbidden"; let _ = stream.write_all(response.as_bytes()); continue; } file_path.push(component); } // Try to serve the file let (status, content_type, body) = if file_path.is_file() { match std::fs::read(&file_path) { Ok(content) => { let mime = get_mime_type(&file_path); ("200 OK", mime, content) } Err(_) => { ("500 Internal Server Error", "text/plain", b"Error reading file".to_vec()) } } } else if file_path.is_dir() { // Try index.html let index_path = file_path.join("index.html"); if index_path.is_file() { match std::fs::read(&index_path) { Ok(content) => ("200 OK", "text/html", content), Err(_) => ("500 Internal Server Error", "text/plain", b"Error reading file".to_vec()), } } else { ("404 Not Found", "text/plain", b"Not Found".to_vec()) } } else { // Try with .html extension let html_path = file_path.with_extension("html"); if html_path.is_file() { match std::fs::read(&html_path) { Ok(content) => ("200 OK", "text/html", content), Err(_) => ("500 Internal Server Error", "text/plain", b"Error reading file".to_vec()), } } else { ("404 Not Found", "text/plain", b"Not Found".to_vec()) } }; // Log the request with colored status let status_color = if status.starts_with("200") { colors::GREEN } else if status.starts_with("404") { colors::YELLOW } else { colors::RED }; let status_code = status.split_whitespace().next().unwrap_or(status); println!(" {} {} {}", log_method, c(status_color, status_code), log_path); // Send response let response = format!( "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", status, content_type, body.len() ); let _ = stream.write_all(response.as_bytes()); let _ = stream.write_all(&body); } } fn get_mime_type(path: &std::path::Path) -> &'static str { match path.extension().and_then(|e| e.to_str()) { Some("html") | Some("htm") => "text/html", Some("css") => "text/css", Some("js") => "application/javascript", Some("json") => "application/json", Some("png") => "image/png", Some("jpg") | Some("jpeg") => "image/jpeg", Some("gif") => "image/gif", Some("svg") => "image/svg+xml", Some("ico") => "image/x-icon", Some("woff") => "font/woff", Some("woff2") => "font/woff2", Some("ttf") => "font/ttf", Some("txt") => "text/plain", Some("xml") => "application/xml", Some("pdf") => "application/pdf", _ => "application/octet-stream", } } 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; } // Init doesn't require being in a project (it creates one) if args[0] == "init" { init_pkg_here(args.get(1).map(|s| s.as_str())); 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 pkg init' to initialize a project here, or '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); } } "search" => { if args.len() < 2 { eprintln!("Usage: lux pkg search "); std::process::exit(1); } search_registry(&args[1]); } "publish" => { publish_package(&project_root); } "help" | "--help" | "-h" => { print_pkg_help(); } unknown => { eprintln!("Unknown package command: {}", unknown); print_pkg_help(); std::process::exit(1); } } } fn search_registry(query: &str) { let registry_url = std::env::var("LUX_REGISTRY_URL") .unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string()); println!("Searching for '{}' in {}...", query, registry_url); // Make HTTP request to registry let url = format!("{}/api/v1/search?q={}", registry_url, query); // Use a simple HTTP client (could use reqwest in production) match simple_http_get(&url) { Ok(response) => { // Parse JSON response and display results if response.contains("\"packages\":[]") { println!("No packages found matching '{}'", query); } else { println!("\nFound packages:"); // Simple parsing of package names from JSON for line in response.lines() { if line.contains("\"name\":") { if let Some(start) = line.find("\"name\":") { let rest = &line[start + 8..]; if let Some(end) = rest.find('"') { let name = &rest[..end]; println!(" {}", name); } } } } } } Err(e) => { eprintln!("Failed to connect to registry: {}", e); eprintln!("Make sure the registry server is running or check LUX_REGISTRY_URL"); std::process::exit(1); } } } fn publish_package(project_root: &std::path::Path) { use std::fs; let registry_url = std::env::var("LUX_REGISTRY_URL") .unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string()); // Load manifest let manifest_path = project_root.join("lux.toml"); if !manifest_path.exists() { eprintln!("No lux.toml found"); std::process::exit(1); } let manifest_content = fs::read_to_string(&manifest_path).unwrap(); // Extract project info let mut name = String::new(); let mut version = String::new(); for line in manifest_content.lines() { let line = line.trim(); if line.starts_with("name") { if let Some(eq) = line.find('=') { name = line[eq+1..].trim().trim_matches('"').to_string(); } } else if line.starts_with("version") { if let Some(eq) = line.find('=') { version = line[eq+1..].trim().trim_matches('"').to_string(); } } } if name.is_empty() || version.is_empty() { eprintln!("Invalid lux.toml: missing name or version"); std::process::exit(1); } println!("Publishing {} v{} to {}...", name, version, registry_url); // Create tarball of the package let tarball_name = format!("{}-{}.tar.gz", name, version); let tarball_path = project_root.join(&tarball_name); // Use tar command to create tarball let status = std::process::Command::new("tar") .arg("-czf") .arg(&tarball_path) .arg("--exclude=.lux_packages") .arg("--exclude=.git") .arg("--exclude=target") .arg("-C") .arg(project_root) .arg(".") .status(); match status { Ok(s) if s.success() => { println!("Created package tarball: {}", tarball_name); println!(); println!("To publish, upload the tarball to the registry:"); println!(" curl -X POST {}/api/v1/publish -F 'package=@{}'", registry_url, tarball_name); // Clean up tarball // fs::remove_file(&tarball_path).ok(); } Ok(_) => { eprintln!("Failed to create package tarball"); std::process::exit(1); } Err(e) => { eprintln!("Failed to run tar: {}", e); std::process::exit(1); } } } fn simple_http_get(url: &str) -> Result { use std::io::{Read, Write}; use std::net::TcpStream; // Parse URL let url = url.strip_prefix("http://").or_else(|| url.strip_prefix("https://")).unwrap_or(url); let (host_port, path) = if let Some(slash) = url.find('/') { (&url[..slash], &url[slash..]) } else { (url, "/") }; let (host, port) = if let Some(colon) = host_port.find(':') { (&host_port[..colon], host_port[colon+1..].parse::().unwrap_or(80)) } else { (host_port, 80) }; let mut stream = TcpStream::connect((host, port)) .map_err(|e| format!("Connection failed: {}", e))?; let request = format!( "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n", path, host ); stream.write_all(request.as_bytes()) .map_err(|e| format!("Write failed: {}", e))?; let mut response = String::new(); stream.read_to_string(&mut response) .map_err(|e| format!("Read failed: {}", e))?; // Extract body (after \r\n\r\n) if let Some(body_start) = response.find("\r\n\r\n") { Ok(response[body_start + 4..].to_string()) } else { Ok(response) } } fn print_pkg_help() { println!("Lux Package Manager"); println!(); println!("Usage: lux pkg [options]"); println!(); println!("Commands:"); println!(" init [name] Initialize a lux.toml in the current directory"); 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!(" search Search for packages in the registry"); println!(" publish Publish package to the registry"); println!(); println!("Registry Configuration:"); println!(" Set LUX_REGISTRY_URL to use a custom registry (default: https://pkgs.lux-lang.org)"); println!(); println!("Examples:"); println!(" lux pkg init"); println!(" lux pkg init my-project"); 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"); println!(" lux pkg search json"); println!(" lux pkg publish"); } fn init_pkg_here(name: Option<&str>) { use std::fs; use std::path::Path; let lux_toml = Path::new("lux.toml"); if lux_toml.exists() { eprintln!("lux.toml already exists in this directory"); std::process::exit(1); } // Get project name from argument or current directory name let project_name = name.map(String::from).unwrap_or_else(|| { std::env::current_dir() .ok() .and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string())) .unwrap_or_else(|| "my-project".to_string()) }); // 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" # mylib = {{ git = "https://github.com/user/mylib" }} # local = {{ path = "../local-lib" }} "#, project_name ); if let Err(e) = fs::write(lux_toml, toml_content) { eprintln!("Failed to create lux.toml: {}", e); std::process::exit(1); } println!("Initialized Lux project: {}", project_name); println!(); println!("Created lux.toml"); println!(); println!("Next steps:"); println!(" lux pkg add Add a dependency"); println!(" lux pkg install Install dependencies"); } 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: {}", c(colors::GREEN, "\u{2713}"), bc(colors::GREEN, project_name)); println!(); println!("{}", bc("", "Project structure:")); println!(" {}", bc(colors::CYAN, &format!("{}/", project_name))); println!(" {} {}", c(colors::DIM, "\u{251c}\u{2500}\u{2500}"), "lux.toml"); println!(" {} {}", c(colors::DIM, "\u{251c}\u{2500}\u{2500}"), bc(colors::CYAN, "src/")); println!(" {} {} {}", c(colors::DIM, "\u{2502}"), c(colors::DIM, " \u{2514}\u{2500}\u{2500}"), "main.lux"); println!(" {} {}", c(colors::DIM, "\u{2514}\u{2500}\u{2500}"), bc(colors::CYAN, "tests/")); println!(" {} {}", c(colors::DIM, "\u{2514}\u{2500}\u{2500}"), "test_example.lux"); println!(); println!("To get started:"); println!(" {} {}", c(colors::DIM, "$"), bc(colors::CYAN, &format!("cd {}", project_name))); println!(" {} {}", c(colors::DIM, "$"), bc(colors::CYAN, "lux src/main.lux")); } /// Generate API documentation for Lux source files fn generate_docs(args: &[String]) { use std::path::Path; use std::collections::HashMap; let output_json = args.iter().any(|a| a == "--json"); let output_dir = args.iter() .position(|a| a == "-o") .and_then(|i| args.get(i + 1)) .map(|s| s.as_str()) .unwrap_or("docs"); let input_file = args.iter().find(|a| !a.starts_with('-') && *a != output_dir); // Collect files to document let mut files_to_doc = Vec::new(); if let Some(path) = input_file { if Path::new(path).is_file() { files_to_doc.push(path.to_string()); } else { eprintln!("File not found: {}", path); std::process::exit(1); } } else { // Auto-discover files if Path::new("src").is_dir() { collect_lux_files_for_docs("src", &mut files_to_doc); } if Path::new("stdlib").is_dir() { collect_lux_files_for_docs("stdlib", &mut files_to_doc); } } if files_to_doc.is_empty() { eprintln!("No .lux files found to document"); std::process::exit(1); } // Create output directory if !output_json { if let Err(e) = std::fs::create_dir_all(output_dir) { eprintln!("Failed to create output directory: {}", e); std::process::exit(1); } } let mut all_docs: HashMap = HashMap::new(); let mut error_count = 0; for file_path in &files_to_doc { let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(e) => { eprintln!("{}: ERROR - {}", file_path, e); error_count += 1; continue; } }; match extract_module_doc(&source, file_path) { Ok(doc) => { let module_name = Path::new(file_path) .file_stem() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| "unknown".to_string()); all_docs.insert(module_name, doc); } Err(e) => { eprintln!("{}: PARSE ERROR - {}", file_path, e); error_count += 1; } } } if output_json { // Output as JSON println!("{}", docs_to_json(&all_docs)); } else { // Generate HTML files let index_html = generate_index_html(&all_docs); let index_path = format!("{}/index.html", output_dir); if let Err(e) = std::fs::write(&index_path, &index_html) { eprintln!("Failed to write index.html: {}", e); error_count += 1; } for (module_name, doc) in &all_docs { let html = generate_module_html(module_name, doc); let path = format!("{}/{}.html", output_dir, module_name); if let Err(e) = std::fs::write(&path, &html) { eprintln!("Failed to write {}: {}", path, e); error_count += 1; } } // Generate CSS let css_path = format!("{}/style.css", output_dir); if let Err(e) = std::fs::write(&css_path, DOC_CSS) { eprintln!("Failed to write style.css: {}", e); error_count += 1; } println!("Generated documentation in {}/", output_dir); println!(" {} modules documented", all_docs.len()); if error_count > 0 { println!(" {} errors", error_count); } } } fn collect_lux_files_for_docs(dir: &str, files: &mut Vec) { if let Ok(entries) = std::fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_file() && path.extension().map(|e| e == "lux").unwrap_or(false) { files.push(path.to_string_lossy().to_string()); } else if path.is_dir() { collect_lux_files_for_docs(&path.to_string_lossy(), files); } } } } #[derive(Debug, Clone)] struct ModuleDoc { description: Option, functions: Vec, types: Vec, effects: Vec, } #[derive(Debug, Clone)] struct FunctionDoc { name: String, signature: String, description: Option, is_public: bool, properties: Vec, } #[derive(Debug, Clone)] struct TypeDoc { name: String, definition: String, description: Option, is_public: bool, } #[derive(Debug, Clone)] struct EffectDoc { name: String, operations: Vec, description: Option, } fn extract_module_doc(source: &str, path: &str) -> Result { use modules::ModuleLoader; use std::path::Path; let mut loader = ModuleLoader::new(); let file_path = Path::new(path); if let Some(parent) = file_path.parent() { loader.add_search_path(parent.to_path_buf()); } let program = loader.load_source(source, Some(file_path)) .map_err(|e| format!("{}", e))?; let mut module_desc: Option = None; let mut functions = Vec::new(); let mut types = Vec::new(); let mut effects = Vec::new(); let mut pending_doc: Option = None; // Extract module-level comment (first comment before any declarations) let lines: Vec<&str> = source.lines().collect(); let mut module_comment = Vec::new(); for line in &lines { let trimmed = line.trim(); if trimmed.starts_with("//") { module_comment.push(trimmed.trim_start_matches('/').trim()); } else if !trimmed.is_empty() { break; } } if !module_comment.is_empty() { module_desc = Some(module_comment.join("\n")); } for decl in &program.declarations { match decl { ast::Declaration::Function(f) => { // Build signature let params: Vec = f.params.iter() .map(|p| format!("{}: {}", p.name.name, format_type(&p.typ))) .collect(); let effects_str = if f.effects.is_empty() { String::new() } else { format!(" with {{{}}}", f.effects.iter().map(|e| e.name.clone()).collect::>().join(", ")) }; let props: Vec = f.properties.iter() .map(|p| format!("{:?}", p).to_lowercase()) .collect(); let props_str = if props.is_empty() { String::new() } else { format!(" is {}", props.join(", ")) }; let signature = format!( "fn {}({}): {}{}{}", f.name.name, params.join(", "), format_type(&f.return_type), props_str, effects_str ); // Extract doc comment let doc = extract_doc_comment(source, f.span.start); functions.push(FunctionDoc { name: f.name.name.clone(), signature, description: doc, is_public: matches!(f.visibility, ast::Visibility::Public), properties: props, }); } ast::Declaration::Type(t) => { let doc = extract_doc_comment(source, t.span.start); types.push(TypeDoc { name: t.name.name.clone(), definition: format_type_def(t), description: doc, is_public: matches!(t.visibility, ast::Visibility::Public), }); } ast::Declaration::Effect(e) => { let doc = extract_doc_comment(source, e.span.start); let ops: Vec = e.operations.iter() .map(|op| { let params: Vec = op.params.iter() .map(|p| format!("{}: {}", p.name.name, format_type(&p.typ))) .collect(); format!("{}({}): {}", op.name.name, params.join(", "), format_type(&op.return_type)) }) .collect(); effects.push(EffectDoc { name: e.name.name.clone(), operations: ops, description: doc, }); } _ => {} } } Ok(ModuleDoc { description: module_desc, functions, types, effects, }) } fn extract_doc_comment(source: &str, pos: usize) -> Option { // Look backwards from the declaration for doc comments let prefix = &source[..pos]; let lines: Vec<&str> = prefix.lines().collect(); let mut doc_lines = Vec::new(); for line in lines.iter().rev() { let trimmed = line.trim(); if trimmed.starts_with("///") { doc_lines.push(trimmed.trim_start_matches('/').trim()); } else if trimmed.starts_with("//") { // Regular comment, skip continue; } else if trimmed.is_empty() { if !doc_lines.is_empty() { break; } } else { break; } } if doc_lines.is_empty() { None } else { doc_lines.reverse(); Some(doc_lines.join("\n")) } } fn format_type(t: &ast::TypeExpr) -> String { match t { ast::TypeExpr::Named(ident) => ident.name.clone(), ast::TypeExpr::App(base, args) => { let args_str: Vec = args.iter().map(format_type).collect(); format!("{}<{}>", format_type(base), args_str.join(", ")) } ast::TypeExpr::Function { params, return_type, .. } => { let params_str: Vec = params.iter().map(format_type).collect(); format!("fn({}): {}", params_str.join(", "), format_type(return_type)) } ast::TypeExpr::Tuple(types) => { let types_str: Vec = types.iter().map(format_type).collect(); format!("({})", types_str.join(", ")) } ast::TypeExpr::Record(fields) => { let fields_str: Vec = fields.iter() .map(|f| format!("{}: {}", f.name.name, format_type(&f.typ))) .collect(); format!("{{ {} }}", fields_str.join(", ")) } ast::TypeExpr::Unit => "Unit".to_string(), ast::TypeExpr::Versioned { base, .. } => format_type(base), } } fn format_type_def(t: &ast::TypeDecl) -> String { match &t.definition { ast::TypeDef::Alias(typ) => format!("type {} = {}", t.name.name, format_type(typ)), ast::TypeDef::Enum(variants) => { let variants_str: Vec = variants.iter() .map(|v| { match &v.fields { ast::VariantFields::Unit => v.name.name.clone(), ast::VariantFields::Tuple(types) => { let types_str: Vec = types.iter().map(format_type).collect(); format!("{}({})", v.name.name, types_str.join(", ")) } ast::VariantFields::Record(fields) => { let fields_str: Vec = fields.iter() .map(|f| format!("{}: {}", f.name.name, format_type(&f.typ))) .collect(); format!("{}{{ {} }}", v.name.name, fields_str.join(", ")) } } }) .collect(); format!("type {} = {}", t.name.name, variants_str.join(" | ")) } ast::TypeDef::Record(fields) => { let fields_str: Vec = fields.iter() .map(|f| format!("{}: {}", f.name.name, format_type(&f.typ))) .collect(); format!("type {} = {{ {} }}", t.name.name, fields_str.join(", ")) } } } fn docs_to_json(docs: &std::collections::HashMap) -> String { let mut json = String::from("{\n"); let mut first_module = true; for (name, doc) in docs { if !first_module { json.push_str(",\n"); } first_module = false; json.push_str(&format!(" \"{}\": {{\n", escape_json(name))); if let Some(desc) = &doc.description { json.push_str(&format!(" \"description\": \"{}\",\n", escape_json(desc))); } // Functions json.push_str(" \"functions\": [\n"); for (i, f) in doc.functions.iter().enumerate() { json.push_str(&format!( " {{\"name\": \"{}\", \"signature\": \"{}\", \"public\": {}, \"description\": {}}}", escape_json(&f.name), escape_json(&f.signature), f.is_public, f.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string()) )); if i < doc.functions.len() - 1 { json.push(','); } json.push('\n'); } json.push_str(" ],\n"); // Types json.push_str(" \"types\": [\n"); for (i, t) in doc.types.iter().enumerate() { json.push_str(&format!( " {{\"name\": \"{}\", \"definition\": \"{}\", \"public\": {}, \"description\": {}}}", escape_json(&t.name), escape_json(&t.definition), t.is_public, t.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string()) )); if i < doc.types.len() - 1 { json.push(','); } json.push('\n'); } json.push_str(" ],\n"); // Effects json.push_str(" \"effects\": [\n"); for (i, e) in doc.effects.iter().enumerate() { let ops_json: Vec = e.operations.iter() .map(|o| format!("\"{}\"", escape_json(o))) .collect(); json.push_str(&format!( " {{\"name\": \"{}\", \"operations\": [{}], \"description\": {}}}", escape_json(&e.name), ops_json.join(", "), e.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string()) )); if i < doc.effects.len() - 1 { json.push(','); } json.push('\n'); } json.push_str(" ]\n"); json.push_str(" }"); } json.push_str("\n}"); json } fn escape_json(s: &str) -> String { s.replace('\\', "\\\\") .replace('"', "\\\"") .replace('\n', "\\n") .replace('\r', "\\r") .replace('\t', "\\t") } fn generate_index_html(docs: &std::collections::HashMap) -> String { let mut html = String::from(r#" Lux API Documentation

Lux API Documentation

Modules

    "#); let mut modules: Vec<_> = docs.keys().collect(); modules.sort(); for name in modules { let doc = &docs[name]; let desc = doc.description.as_ref() .map(|d| d.lines().next().unwrap_or("")) .unwrap_or(""); html.push_str(&format!( "
  • {} - {}
  • \n", name, name, html_escape(desc) )); } html.push_str(r#"
"#); html } fn generate_module_html(name: &str, doc: &ModuleDoc) -> String { let mut html = format!(r#" {} - Lux API
Back to Index

{}

"#, name, name); if let Some(desc) = &doc.description { html.push_str(&format!("
{}
\n", html_escape(desc))); } // Types if !doc.types.is_empty() { html.push_str("
\n

Types

\n"); for t in &doc.types { let visibility = if t.is_public { "pub " } else { "" }; html.push_str(&format!( "
\n {}{}\n", visibility, html_escape(&t.definition) )); if let Some(desc) = &t.description { html.push_str(&format!("

{}

\n", html_escape(desc))); } html.push_str("
\n"); } html.push_str("
\n"); } // Effects if !doc.effects.is_empty() { html.push_str("
\n

Effects

\n"); for e in &doc.effects { html.push_str(&format!( "
\n

effect {}

\n", html_escape(&e.name) )); if let Some(desc) = &e.description { html.push_str(&format!("

{}

\n", html_escape(desc))); } html.push_str("
    \n"); for op in &e.operations { html.push_str(&format!("
  • {}
  • \n", html_escape(op))); } html.push_str("
\n
\n"); } html.push_str("
\n"); } // Functions if !doc.functions.is_empty() { html.push_str("
\n

Functions

\n"); for f in &doc.functions { let visibility = if f.is_public { "pub " } else { "" }; html.push_str(&format!( "
\n {}{}\n", f.name, visibility, html_escape(&f.signature) )); if let Some(desc) = &f.description { html.push_str(&format!("

{}

\n", html_escape(desc))); } html.push_str("
\n"); } html.push_str("
\n"); } html.push_str(r#"
"#); html } fn html_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) } const DOC_CSS: &str = r#" :root { --bg-color: #1a1a2e; --text-color: #e0e0e0; --link-color: #64b5f6; --code-bg: #16213e; --header-bg: #0f3460; --accent: #e94560; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: var(--bg-color); color: var(--text-color); margin: 0; padding: 0; line-height: 1.6; } header { background-color: var(--header-bg); padding: 1rem 2rem; border-bottom: 2px solid var(--accent); } header h1 { margin: 0; color: white; } header a { color: var(--link-color); text-decoration: none; font-size: 0.9rem; } main { max-width: 900px; margin: 0 auto; padding: 2rem; } h2 { color: var(--accent); border-bottom: 1px solid var(--accent); padding-bottom: 0.5rem; } h3 { color: var(--link-color); } .module-list { list-style: none; padding: 0; } .module-list li { padding: 0.5rem 0; border-bottom: 1px solid rgba(255,255,255,0.1); } .module-list a { color: var(--link-color); text-decoration: none; font-weight: bold; } .item { background-color: var(--code-bg); border-radius: 8px; padding: 1rem; margin: 1rem 0; } .signature { display: block; background-color: rgba(0,0,0,0.3); padding: 0.5rem 1rem; border-radius: 4px; font-family: 'Fira Code', 'Monaco', monospace; overflow-x: auto; } .description { margin-top: 0.5rem; color: #aaa; } .operations { list-style: none; padding-left: 1rem; } .operations li { padding: 0.25rem 0; } .module-description { background-color: var(--code-bg); padding: 1rem; border-radius: 8px; margin-bottom: 2rem; border-left: 3px solid var(--accent); } "#; 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, last_loaded_file: Option, } 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", ":reload", ":r", ":trace", ":traces", ":info", ":i", ":env", ":doc", ":d", ":browse", ":b", ":ast", ] .into_iter() .map(String::from) .collect(); Self { keywords, commands, user_defined: HashSet::new(), last_loaded_file: None, } } 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)) } fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { let mut result = String::with_capacity(line.len() * 2); let mut chars = line.char_indices().peekable(); while let Some((i, c)) = chars.next() { if c == '"' { // String literal - highlight in green result.push_str("\x1b[32m\""); let mut escaped = false; for (_, ch) in chars.by_ref() { result.push(ch); if escaped { escaped = false; } else if ch == '\\' { escaped = true; } else if ch == '"' { break; } } result.push_str("\x1b[0m"); } else if c == ':' && i == 0 { // Command - highlight in cyan result.push_str("\x1b[36m:"); for (_, ch) in chars.by_ref() { if ch.is_whitespace() { result.push_str("\x1b[0m"); result.push(ch); break; } result.push(ch); } // Continue with the rest for (_, ch) in chars.by_ref() { result.push(ch); } } else if c.is_ascii_digit() || (c == '-' && chars.peek().map(|(_, ch)| ch.is_ascii_digit()).unwrap_or(false)) { // Number - highlight in yellow result.push_str("\x1b[33m"); result.push(c); while let Some(&(_, ch)) = chars.peek() { if ch.is_ascii_digit() || ch == '.' { result.push(ch); chars.next(); } else { break; } } result.push_str("\x1b[0m"); } else if c.is_alphabetic() || c == '_' { // Identifier or keyword let start = i; let mut end = i + c.len_utf8(); while let Some(&(j, ch)) = chars.peek() { if ch.is_alphanumeric() || ch == '_' { end = j + ch.len_utf8(); chars.next(); } else { break; } } let word = &line[start..end]; // Check if it's a keyword if self.keywords.contains(word) { result.push_str("\x1b[35m"); // Magenta for keywords result.push_str(word); result.push_str("\x1b[0m"); } else if word.starts_with(char::is_uppercase) { result.push_str("\x1b[34m"); // Blue for types/constructors result.push_str(word); result.push_str("\x1b[0m"); } else { result.push_str(word); } } else if c == '/' && chars.peek().map(|(_, ch)| *ch == '/').unwrap_or(false) { // Comment - highlight in gray result.push_str("\x1b[90m"); result.push(c); for (_, ch) in chars.by_ref() { result.push(ch); } result.push_str("\x1b[0m"); } else { result.push(c); } } Cow::Owned(result) } fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool { true } } 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 { helper.last_loaded_file = Some(path.to_string()); load_file(path, interp, checker, helper); } else { println!("Usage: :load "); } } ":reload" | ":r" => { if let Some(ref path) = helper.last_loaded_file.clone() { println!("Reloading {}...", path); // Clear environment first *interp = Interpreter::new(); *checker = TypeChecker::new(); helper.user_defined.clear(); load_file(path, interp, checker, helper); } else { println!("No file to reload. Use :load first."); } } ":ast" => { if let Some(expr_str) = arg { show_ast(expr_str); } else { println!("Usage: :ast "); } } ":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(); } } ":doc" | ":d" => { if let Some(name) = arg { show_doc(name); } else { println!("Usage: :doc "); } } ":browse" | ":b" => { if let Some(module) = arg { browse_module(module); } else { println!("Usage: :browse "); println!("Available modules: List, String, Option, Result, Console, Random, File, Http"); } } _ => { 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_doc(name: &str) { // Built-in documentation for common functions let doc = match name { // List functions "List.length" => Some(("List.length : List -> Int", "Returns the number of elements in a list.")), "List.head" => Some(("List.head : List -> Option", "Returns the first element of a list, or None if empty.")), "List.tail" => Some(("List.tail : List -> Option>", "Returns all elements except the first, or None if empty.")), "List.map" => Some(("List.map : (List, fn(A) -> B) -> List", "Applies a function to each element, returning a new list.")), "List.filter" => Some(("List.filter : (List, fn(T) -> Bool) -> List", "Returns elements for which the predicate returns true.")), "List.fold" => Some(("List.fold : (List, U, fn(U, T) -> U) -> U", "Reduces a list to a single value using an accumulator function.")), "List.reverse" => Some(("List.reverse : List -> List", "Returns a new list with elements in reverse order.")), "List.concat" => Some(("List.concat : (List, List) -> List", "Concatenates two lists.")), "List.take" => Some(("List.take : (List, Int) -> List", "Returns the first n elements.")), "List.drop" => Some(("List.drop : (List, Int) -> List", "Returns all elements after the first n.")), "List.get" => Some(("List.get : (List, Int) -> Option", "Returns the element at index n, or None if out of bounds.")), "List.contains" => Some(("List.contains : (List, T) -> Bool", "Returns true if the list contains the element.")), "List.all" => Some(("List.all : (List, fn(T) -> Bool) -> Bool", "Returns true if all elements satisfy the predicate.")), "List.any" => Some(("List.any : (List, fn(T) -> Bool) -> Bool", "Returns true if any element satisfies the predicate.")), "List.find" => Some(("List.find : (List, fn(T) -> Bool) -> Option", "Returns the first element satisfying the predicate.")), "List.sort" => Some(("List.sort : List -> List", "Sorts a list of integers in ascending order.")), "List.range" => Some(("List.range : (Int, Int) -> List", "Creates a list of integers from start to end (exclusive).")), // String functions "String.length" => Some(("String.length : String -> Int", "Returns the number of characters in a string.")), "String.concat" => Some(("String.concat : (String, String) -> String", "Concatenates two strings.")), "String.substring" => Some(("String.substring : (String, Int, Int) -> String", "Returns a substring from start to end index.")), "String.split" => Some(("String.split : (String, String) -> List", "Splits a string by a delimiter.")), "String.join" => Some(("String.join : (List, String) -> String", "Joins a list of strings with a delimiter.")), "String.trim" => Some(("String.trim : String -> String", "Removes leading and trailing whitespace.")), "String.contains" => Some(("String.contains : (String, String) -> Bool", "Returns true if the string contains the substring.")), "String.replace" => Some(("String.replace : (String, String, String) -> String", "Replaces all occurrences of a pattern.")), "String.startsWith" => Some(("String.startsWith : (String, String) -> Bool", "Returns true if the string starts with the prefix.")), "String.endsWith" => Some(("String.endsWith : (String, String) -> Bool", "Returns true if the string ends with the suffix.")), "String.toUpper" => Some(("String.toUpper : String -> String", "Converts to uppercase.")), "String.toLower" => Some(("String.toLower : String -> String", "Converts to lowercase.")), // Option functions "Option.map" => Some(("Option.map : (Option, fn(A) -> B) -> Option", "Applies a function to the value inside Some, or returns None.")), "Option.flatMap" => Some(("Option.flatMap : (Option, fn(A) -> Option) -> Option", "Like map, but the function returns an Option.")), "Option.getOrElse" => Some(("Option.getOrElse : (Option, T) -> T", "Returns the value inside Some, or the default if None.")), "Option.isSome" => Some(("Option.isSome : Option -> Bool", "Returns true if the value is Some.")), "Option.isNone" => Some(("Option.isNone : Option -> Bool", "Returns true if the value is None.")), // Result functions "Result.map" => Some(("Result.map : (Result, fn(A) -> B) -> Result", "Applies a function to the Ok value.")), "Result.flatMap" => Some(("Result.flatMap : (Result, fn(A) -> Result) -> Result", "Like map, but the function returns a Result.")), "Result.getOrElse" => Some(("Result.getOrElse : (Result, T) -> T", "Returns the Ok value, or the default if Err.")), "Result.isOk" => Some(("Result.isOk : Result -> Bool", "Returns true if the value is Ok.")), "Result.isErr" => Some(("Result.isErr : Result -> Bool", "Returns true if the value is Err.")), // Effects "Console.print" => Some(("Console.print : String -> Unit", "Prints a string to the console.")), "Console.readLine" => Some(("Console.readLine : () -> String", "Reads a line from standard input.")), "Random.int" => Some(("Random.int : (Int, Int) -> Int", "Generates a random integer in the range [min, max].")), "Random.float" => Some(("Random.float : () -> Float", "Generates a random float in [0, 1).")), "Random.bool" => Some(("Random.bool : () -> Bool", "Generates a random boolean.")), "File.read" => Some(("File.read : String -> String", "Reads the contents of a file.")), "File.write" => Some(("File.write : (String, String) -> Unit", "Writes content to a file.")), "File.exists" => Some(("File.exists : String -> Bool", "Returns true if the file exists.")), "Http.get" => Some(("Http.get : String -> String", "Performs an HTTP GET request.")), "Http.post" => Some(("Http.post : (String, String) -> String", "Performs an HTTP POST request with a body.")), "Time.now" => Some(("Time.now : () -> Int", "Returns the current Unix timestamp in milliseconds.")), "Sql.open" => Some(("Sql.open : String -> SqlConn", "Opens a SQLite database file.")), "Sql.openMemory" => Some(("Sql.openMemory : () -> SqlConn", "Opens an in-memory SQLite database.")), "Sql.query" => Some(("Sql.query : (SqlConn, String) -> List", "Executes a SQL query and returns all rows.")), "Sql.execute" => Some(("Sql.execute : (SqlConn, String) -> Int", "Executes a SQL statement and returns affected rows.")), "Postgres.connect" => Some(("Postgres.connect : String -> Int", "Connects to a PostgreSQL database.")), "Postgres.query" => Some(("Postgres.query : (Int, String) -> List", "Executes a SQL query on PostgreSQL.")), "Postgres.execute" => Some(("Postgres.execute : (Int, String) -> Int", "Executes a SQL statement on PostgreSQL.")), // Language constructs "fn" => Some(("fn name(args): Type = body", "Defines a function.")), "let" => Some(("let name = value", "Binds a value to a name.")), "if" => Some(("if condition then expr1 else expr2", "Conditional expression.")), "match" => Some(("match value { pattern => expr, ... }", "Pattern matching expression.")), "effect" => Some(("effect Name { fn op(...): Type }", "Defines an effect with operations.")), "handler" => Some(("handler { op => body }", "Defines an effect handler.")), "with" => Some(("fn f(): T with {Effect1, Effect2}", "Declares effects a function may perform.")), "type" => Some(("type Name = ...", "Defines a type alias.")), "run" => Some(("run expr with { handler }", "Executes an expression with effect handlers.")), _ => None, }; match doc { Some((sig, desc)) => { println!("\x1b[1m{}\x1b[0m", sig); println!(); println!(" {}", desc); } None => { println!("No documentation for '{}'", name); println!("Try :doc List.map or :browse List"); } } } fn browse_module(module: &str) { let exports: Vec<(&str, &str)> = match module { "List" => vec![ ("length", "List -> Int"), ("head", "List -> Option"), ("tail", "List -> Option>"), ("get", "(List, Int) -> Option"), ("map", "(List, fn(A) -> B) -> List"), ("filter", "(List, fn(T) -> Bool) -> List"), ("fold", "(List, U, fn(U, T) -> U) -> U"), ("reverse", "List -> List"), ("concat", "(List, List) -> List"), ("take", "(List, Int) -> List"), ("drop", "(List, Int) -> List"), ("contains", "(List, T) -> Bool"), ("all", "(List, fn(T) -> Bool) -> Bool"), ("any", "(List, fn(T) -> Bool) -> Bool"), ("find", "(List, fn(T) -> Bool) -> Option"), ("sort", "List -> List"), ("range", "(Int, Int) -> List"), ("forEach", "(List, fn(T) -> Unit) -> Unit"), ], "String" => vec![ ("length", "String -> Int"), ("concat", "(String, String) -> String"), ("substring", "(String, Int, Int) -> String"), ("split", "(String, String) -> List"), ("join", "(List, String) -> String"), ("trim", "String -> String"), ("contains", "(String, String) -> Bool"), ("replace", "(String, String, String) -> String"), ("startsWith", "(String, String) -> Bool"), ("endsWith", "(String, String) -> Bool"), ("toUpper", "String -> String"), ("toLower", "String -> String"), ("lines", "String -> List"), ], "Option" => vec![ ("map", "(Option, fn(A) -> B) -> Option"), ("flatMap", "(Option, fn(A) -> Option) -> Option"), ("getOrElse", "(Option, T) -> T"), ("isSome", "Option -> Bool"), ("isNone", "Option -> Bool"), ], "Result" => vec![ ("map", "(Result, fn(A) -> B) -> Result"), ("flatMap", "(Result, fn(A) -> Result) -> Result"), ("getOrElse", "(Result, T) -> T"), ("isOk", "Result -> Bool"), ("isErr", "Result -> Bool"), ], "Console" => vec![ ("print", "String -> Unit"), ("readLine", "() -> String"), ], "Random" => vec![ ("int", "(Int, Int) -> Int"), ("float", "() -> Float"), ("bool", "() -> Bool"), ], "File" => vec![ ("read", "String -> String"), ("write", "(String, String) -> Unit"), ("exists", "String -> Bool"), ("delete", "String -> Unit"), ], "Http" => vec![ ("get", "String -> String"), ("post", "(String, String) -> String"), ], "Time" => vec![ ("now", "() -> Int"), ], "Sql" => vec![ ("open", "String -> SqlConn"), ("openMemory", "() -> SqlConn"), ("close", "SqlConn -> Unit"), ("query", "(SqlConn, String) -> List"), ("queryOne", "(SqlConn, String) -> Option"), ("execute", "(SqlConn, String) -> Int"), ("beginTx", "SqlConn -> Unit"), ("commit", "SqlConn -> Unit"), ("rollback", "SqlConn -> Unit"), ], "Postgres" => vec![ ("connect", "String -> Int"), ("close", "Int -> Unit"), ("query", "(Int, String) -> List"), ("queryOne", "(Int, String) -> Option"), ("execute", "(Int, String) -> Int"), ("beginTx", "Int -> Unit"), ("commit", "Int -> Unit"), ("rollback", "Int -> Unit"), ], "Test" => vec![ ("assert", "(Bool, String) -> Unit"), ("assertEqual", "(T, T) -> Unit"), ("assertTrue", "Bool -> Unit"), ("assertFalse", "Bool -> Unit"), ("fail", "String -> Unit"), ], _ => { println!("Unknown module: {}", module); println!("Available modules: List, String, Option, Result, Console, Random, File, Http, Time, Sql, Postgres, Test"); return; } }; println!("\x1b[1mmodule {}\x1b[0m", module); println!(); for (name, sig) in exports { println!(" \x1b[34m{}.{}\x1b[0m : {}", module, name, sig); } } 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 show_ast(expr_str: &str) { // Wrap expression in a let to parse it let wrapped = format!("let _expr_ = {}", expr_str); match Parser::parse_source(&wrapped) { Ok(program) => { // Pretty print the AST for decl in &program.declarations { println!("{:#?}", decl); } } 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}""#); } #[test] fn test_multiline_string() { let source = r#" let s = """ hello world """ let result = String.length(s) "#; // "hello\nworld" = 11 chars assert_eq!(eval(source).unwrap(), "11"); } #[test] fn test_multiline_string_with_quotes() { // Quotes are fine in the middle of triple-quoted strings let source = "let s = \"\"\"\n She said \"hello\" to him.\n\"\"\""; assert_eq!(eval(source).unwrap(), r#""She said "hello" to him.""#); } #[test] fn test_multiline_string_interpolation() { let source = r#" let name = "Lux" let s = """ Hello, {name}! """ "#; assert_eq!(eval(source).unwrap(), r#""Hello, Lux!""#); } #[test] fn test_multiline_string_empty() { let source = r#"let s = """""""#; assert_eq!(eval(source).unwrap(), r#""""#); } #[test] fn test_multiline_string_inline() { let source = r#"let s = """hello world""""#; assert_eq!(eval(source).unwrap(), r#""hello world""#); } // 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_where_clause_satisfied() { // Where clause should be satisfied when passing an idempotent function // Note: Parameter type must be the type parameter F for constraint to apply let source = r#" fn runIdempotent(action: F): Int where F is idempotent = 42 fn idempotentFn(): Int is idempotent = 42 let result = runIdempotent(idempotentFn) "#; assert!(eval(source).is_ok()); } #[test] fn test_where_clause_violation() { // Where clause should fail when passing a non-idempotent function // The function notIdempotent doesn't declare "is idempotent", so this should fail let source = r#" fn runIdempotent(action: F): Int where F is idempotent = 42 fn notIdempotent(): Int = 42 let result = runIdempotent(notIdempotent) "#; let result = eval(source); assert!(result.is_err(), "Should fail when where clause is violated"); assert!(result.unwrap_err().contains("property constraint"), "Error should mention property constraint"); } #[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()); } #[test] fn test_schema_breaking_change_with_migration() { // Breaking change with migration should not error let source = r#" type User @v1 { name: String } type User @v2 { name: String, email: String, from @v1 = { name: old.name, email: "unknown@example.com" } } let x = 1 "#; assert!(eval(source).is_ok(), "Breaking change with migration should be allowed"); } #[test] fn test_schema_breaking_change_without_migration() { // Breaking change without migration should produce an error let source = r#" type Config @v1 { host: String } type Config @v2 { host: String, port: Int } "#; let result = eval(source); assert!(result.is_err(), "Breaking change without migration should fail"); let err = result.unwrap_err(); assert!(err.contains("Breaking changes") || err.contains("required field"), "Error should mention breaking changes: {}", err); } #[test] fn test_schema_compatible_change() { // Compatible change (adding optional field) should not error // Note: Currently we don't have Option types fully integrated, // so this test verifies the basic flow let source = r#" type Settings @v1 { theme: String } type Settings @v2 { theme: String } let x = 1 "#; let result = eval(source); assert!(result.is_ok(), "Compatible change should be allowed: {:?}", result); } // 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::error( "Type Mismatch", "Expected Int but got String", Span { start: 56, end: 61 }, ) .with_hint("The second argument should be an Int."); 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, "Undefined Variable"); assert!(diag.hints.iter().any(|h| h.contains("spelling"))); // Check error code is set assert!(diag.code.is_some()); } #[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"); } } // ============ Multi-line Arguments Tests ============ #[test] fn test_multiline_function_args() { let source = r#" fn add(a: Int, b: Int): Int = a + b let result = add( 1, 2 ) "#; assert_eq!(eval(source).unwrap(), "3"); } #[test] fn test_multiline_function_args_with_lambda() { let source = r#" let xs = List.map( [1, 2, 3], fn(x) => x * 2 ) "#; assert_eq!(eval(source).unwrap(), "[2, 4, 6]"); } // ============ Tuple Index Tests ============ #[test] fn test_tuple_index_access() { let source = r#" let pair = (42, "hello") let first = pair.0 "#; assert_eq!(eval(source).unwrap(), "42"); } #[test] fn test_tuple_index_access_second() { let source = r#" let pair = (42, "hello") let second = pair.1 "#; assert_eq!(eval(source).unwrap(), "\"hello\""); } #[test] fn test_tuple_index_triple() { let source = r#" let triple = (1, 2, 3) let sum = triple.0 + triple.1 + triple.2 "#; assert_eq!(eval(source).unwrap(), "6"); } #[test] fn test_tuple_index_in_function() { let source = r#" fn first(pair: (Int, String)): Int = pair.0 fn second(pair: (Int, String)): String = pair.1 let p = (42, "hello") let result = first(p) "#; assert_eq!(eval(source).unwrap(), "42"); } // 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(); } } // === Map type tests === #[test] fn test_map_new_and_size() { let source = r#" let m = Map.new() let result = Map.size(m) "#; assert_eq!(eval(source).unwrap(), "0"); } #[test] fn test_map_set_and_get() { let source = r#" let m = Map.new() let m2 = Map.set(m, "name", "Alice") let result = Map.get(m2, "name") "#; assert_eq!(eval(source).unwrap(), "Some(\"Alice\")"); } #[test] fn test_map_get_missing() { let source = r#" let m = Map.new() let result = Map.get(m, "missing") "#; assert_eq!(eval(source).unwrap(), "None"); } #[test] fn test_map_contains() { let source = r#" let m = Map.set(Map.new(), "x", 1) let result = (Map.contains(m, "x"), Map.contains(m, "y")) "#; assert_eq!(eval(source).unwrap(), "(true, false)"); } #[test] fn test_map_remove() { let source = r#" let m = Map.set(Map.set(Map.new(), "a", 1), "b", 2) let m2 = Map.remove(m, "a") let result = (Map.size(m2), Map.contains(m2, "a"), Map.contains(m2, "b")) "#; assert_eq!(eval(source).unwrap(), "(1, false, true)"); } #[test] fn test_map_keys_and_values() { let source = r#" let m = Map.set(Map.set(Map.new(), "b", 2), "a", 1) let result = Map.keys(m) "#; assert_eq!(eval(source).unwrap(), "[\"a\", \"b\"]"); } #[test] fn test_map_from_list() { let source = r#" let m = Map.fromList([("x", 10), ("y", 20)]) let result = (Map.get(m, "x"), Map.size(m)) "#; assert_eq!(eval(source).unwrap(), "(Some(10), 2)"); } #[test] fn test_map_to_list() { let source = r#" let m = Map.set(Map.set(Map.new(), "b", 2), "a", 1) let result = Map.toList(m) "#; assert_eq!(eval(source).unwrap(), "[(\"a\", 1), (\"b\", 2)]"); } #[test] fn test_map_merge() { let source = r#" let m1 = Map.fromList([("a", 1), ("b", 2)]) let m2 = Map.fromList([("b", 3), ("c", 4)]) let merged = Map.merge(m1, m2) let result = (Map.get(merged, "a"), Map.get(merged, "b"), Map.get(merged, "c")) "#; assert_eq!(eval(source).unwrap(), "(Some(1), Some(3), Some(4))"); } #[test] fn test_map_immutability() { let source = r#" let m1 = Map.fromList([("a", 1)]) let m2 = Map.set(m1, "b", 2) let result = (Map.size(m1), Map.size(m2)) "#; assert_eq!(eval(source).unwrap(), "(1, 2)"); } #[test] fn test_map_is_empty() { let source = r#" let m1 = Map.new() let m2 = Map.set(m1, "x", 1) let result = (Map.isEmpty(m1), Map.isEmpty(m2)) "#; assert_eq!(eval(source).unwrap(), "(true, false)"); } #[test] fn test_map_type_annotation() { let source = r#" fn lookup(m: Map, key: String): Option = Map.get(m, key) let m = Map.fromList([("age", 30)]) let result = lookup(m, "age") "#; assert_eq!(eval(source).unwrap(), "Some(30)"); } #[test] fn test_file_copy() { use std::io::Write; // Create a temp file, copy it, verify contents let dir = std::env::temp_dir().join("lux_test_file_copy"); let _ = std::fs::create_dir_all(&dir); let src = dir.join("src.txt"); let dst = dir.join("dst.txt"); std::fs::File::create(&src).unwrap().write_all(b"hello copy").unwrap(); let _ = std::fs::remove_file(&dst); let source = format!(r#" fn main(): Unit with {{File}} = File.copy("{}", "{}") let _ = run main() with {{}} let result = "done" "#, src.display(), dst.display()); let result = eval(&source); assert!(result.is_ok(), "File.copy failed: {:?}", result); let contents = std::fs::read_to_string(&dst).unwrap(); assert_eq!(contents, "hello copy"); // Cleanup let _ = std::fs::remove_dir_all(&dir); } #[test] fn test_effectful_callback_propagation() { // WISH-7: effectful callbacks in List.forEach should propagate effects // This should type-check successfully because Console effect is inferred let source = r#" fn printAll(items: List): Unit = List.forEach(items, fn(x: String): Unit => Console.print(x)) let result = "ok" "#; let result = eval(source); assert!(result.is_ok(), "Effectful callback should type-check: {:?}", result); } #[test] fn test_effectful_callback_in_map() { // Effectful callback in List.map should propagate effects let source = r#" fn readAll(paths: List): List = List.map(paths, fn(p: String): String => File.read(p)) let result = "ok" "#; let result = eval(source); assert!(result.is_ok(), "Effectful callback in map should type-check: {:?}", result); } }