diff --git a/src/main.rs b/src/main.rs index 3879adf..b5aed5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ //! Lux - A functional programming language with first-class effects +mod analysis; mod ast; mod codegen; mod debugger; @@ -87,18 +88,8 @@ fn main() { println!("Lux {}", VERSION); } "fmt" => { - // Format files - if args.len() < 3 { - eprintln!("Usage: lux fmt [--check]"); - std::process::exit(1); - } - let check_only = args.iter().any(|a| a == "--check"); - for arg in &args[2..] { - if arg.starts_with('-') { - continue; - } - format_file(arg, check_only); - } + // Format files (auto-discovers if no file specified) + format_files(&args[2..]); } "test" => { // Run tests @@ -117,12 +108,8 @@ fn main() { init_project(args.get(2).map(|s| s.as_str())); } "check" => { - // Type check without running - if args.len() < 3 { - eprintln!("Usage: lux check "); - std::process::exit(1); - } - check_file(&args[2]); + // Type check files (auto-discovers if no file specified) + check_files(&args[2..]); } "debug" => { // Start debugger @@ -188,8 +175,8 @@ fn print_help() { println!(" lux compile --run Compile and execute"); println!(" lux compile --emit-c Output C code instead of binary"); println!(" lux compile --target js Compile to JavaScript"); - println!(" lux fmt Format a file (--check to verify only)"); - println!(" lux check Type check without running"); + println!(" lux fmt [file] [--check] Format files (auto-discovers if no file given)"); + println!(" lux check [file] Type check files (auto-discovers if no file given)"); println!(" lux test [pattern] Run tests (optional pattern filter)"); println!(" lux watch Watch and re-run on changes"); println!(" lux debug Start interactive debugger"); @@ -200,82 +187,230 @@ fn print_help() { println!(" lux --version Show version"); } -fn format_file(path: &str, check_only: bool) { +fn format_files(args: &[String]) { use formatter::{format, FormatConfig}; + use std::path::Path; - let source = match std::fs::read_to_string(path) { - Ok(s) => s, - Err(e) => { - eprintln!("Error reading file '{}': {}", path, e); - std::process::exit(1); + let 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!("Looking in: ., src/, examples/, tests/"); + return; + } + + // Sort for consistent output + files_to_format.sort(); let config = FormatConfig::default(); - let formatted = match format(&source, &config) { - Ok(f) => f, - Err(e) => { - eprintln!("Error formatting '{}': {}", path, e); - std::process::exit(1); - } - }; + let mut formatted_count = 0; + let mut unchanged_count = 0; + let mut error_count = 0; + let mut would_reformat = Vec::new(); - if check_only { - if source != formatted { - eprintln!("{} would be reformatted", path); - std::process::exit(1); - } else { - println!("{} is correctly formatted", path); - } - } else { - if source != formatted { - if let Err(e) = std::fs::write(path, &formatted) { - eprintln!("Error writing file '{}': {}", path, e); - std::process::exit(1); + 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!("{}: ERROR - {}", path, e); + error_count += 1; + continue; + } + }; + + let formatted = match format(&source, &config) { + Ok(f) => f, + Err(e) => { + eprintln!("{}: FORMAT ERROR - {}", 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!("{}: WRITE ERROR - {}", path, e); + error_count += 1; + continue; } println!("Formatted {}", path); + formatted_count += 1; } else { - println!("{} unchanged", path); + unchanged_count += 1; } } + + println!(); + if check_only { + if !would_reformat.is_empty() { + println!("Files that would be reformatted:"); + for path in &would_reformat { + println!(" {}", path); + } + println!(); + println!("{} would be reformatted, {} already formatted", would_reformat.len(), unchanged_count); + std::process::exit(1); + } else { + println!("All {} files are correctly formatted", unchanged_count); + } + } else { + println!("{} formatted, {} unchanged, {} errors", formatted_count, unchanged_count, error_count); + } + + if error_count > 0 { + std::process::exit(1); + } } -fn check_file(path: &str) { +fn check_files(args: &[String]) { use modules::ModuleLoader; use std::path::Path; - let file_path = Path::new(path); - let source = match std::fs::read_to_string(file_path) { - Ok(s) => s, - Err(e) => { - eprintln!("Error reading file '{}': {}", path, e); - std::process::exit(1); - } - }; + let pattern = args.first().map(|s| s.as_str()); - let mut loader = ModuleLoader::new(); - if let Some(parent) = file_path.parent() { - loader.add_search_path(parent.to_path_buf()); + // 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)); + } } - let program = match loader.load_source(&source, Some(file_path)) { - Ok(p) => p, - Err(e) => { - eprintln!("Module error: {}", e); - std::process::exit(1); + // 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); } - }; - 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))); + // 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!("Looking in: ., src/, examples/, tests/"); + return; + } + + // Sort for consistent output + files_to_check.sort(); + + let mut passed = 0; + let mut failed = 0; + + 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!("{}: ERROR - {}", 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!("{}: MODULE ERROR - {}", path, e); + failed += 1; + continue; + } + }; + + let mut checker = TypeChecker::new(); + if let Err(errors) = checker.check_program_with_modules(&program, &loader) { + eprintln!("{}: FAILED", path); + for error in errors { + let diagnostic = error.to_diagnostic(); + eprint!("{}", render(&diagnostic, &source, Some(&path))); + } + failed += 1; + } else { + println!("{}: OK", path); + passed += 1; + } + } + + println!(); + println!("Checked {} files: {} passed, {} failed", passed + failed, passed, failed); + + if failed > 0 { std::process::exit(1); } +} - println!("{}: OK", path); +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); + } + } + } + } } fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: bool) { @@ -389,8 +524,9 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: } } - // Clean up temp file - let _ = std::fs::remove_file(&temp_c); + // Keep temp file for debugging + eprintln!("C file: {}", temp_c.display()); + // let _ = std::fs::remove_file(&temp_c); if run_after { // Run the compiled binary @@ -706,6 +842,29 @@ fn collect_test_files(dir: &str, pattern: Option<&str>, files: &mut Vec, 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; @@ -783,11 +942,17 @@ fn handle_pkg_command(args: &[String]) { 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 init' to create a new project"); + eprintln!("Run 'lux pkg init' to initialize a project here, or 'lux init ' to create a new project"); std::process::exit(1); } }; @@ -898,6 +1063,7 @@ fn print_pkg_help() { 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:"); @@ -911,6 +1077,8 @@ fn print_pkg_help() { println!(" clean Remove installed packages"); 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"); @@ -918,6 +1086,55 @@ fn print_pkg_help() { println!(" lux pkg remove http"); } +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; @@ -2267,6 +2484,33 @@ c")"#; 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 @@ -2313,6 +2557,52 @@ c")"#; 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};