Files
lux/src/main.rs
Brandon Lucas 3ee3529ef6 feat: improve REPL with syntax highlighting and documentation
REPL improvements:
- Syntax highlighting for keywords (magenta), types (blue),
  strings (green), numbers (yellow), comments (gray)
- :doc command to show documentation for functions
- :browse command to list module exports
- Added docs for List, String, Option, Result, Console,
  Random, File, Http, Time, Sql, Postgres, Test modules

Example usage:
  lux> :doc List.map
  lux> :browse String
  lux> :doc fn

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 04:43:33 -05:00

4114 lines
138 KiB
Rust

//! Lux - A functional programming language with first-class effects
mod analysis;
mod ast;
mod codegen;
mod debugger;
mod diagnostics;
mod exhaustiveness;
mod formatter;
mod interpreter;
mod lexer;
mod lsp;
mod modules;
mod package;
mod parser;
mod registry;
mod schema;
mod typechecker;
mod types;
use diagnostics::render;
use interpreter::Interpreter;
use parser::Parser;
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::history::DefaultHistory;
use rustyline::validate::Validator;
use rustyline::{Config, Editor, Helper};
use std::borrow::Cow;
use std::collections::HashSet;
use typechecker::TypeChecker;
const VERSION: &str = "0.1.0";
const HELP: &str = r#"
Lux - A functional language with first-class effects
Commands:
:help, :h Show this help
:quit, :q Exit the REPL
:type <expr> Show the type of an expression
:info <name> Show info about a binding
:doc <name> Show documentation for a function/type
:browse <mod> List exports of a module (List, String, Option, etc.)
:env Show user-defined bindings
:clear Clear the environment
:load <file> Load and execute a file
:trace on/off Enable/disable effect tracing
:traces Show recorded effect traces
Keyboard:
Tab Autocomplete
Ctrl-C Cancel current input
Ctrl-D Exit
Up/Down Browse history
Ctrl-R Search history
Examples:
> let x = 42
> x + 1
43
> fn double(n: Int): Int = n * 2
> :type double
double : fn(Int) -> Int
> double(21)
42
> match Some(5) { Some(x) => x, None => 0 }
5
"#;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
match args[1].as_str() {
"--lsp" => {
// Run LSP server
if let Err(e) = lsp::LspServer::run() {
eprintln!("LSP server error: {}", e);
std::process::exit(1);
}
}
"--help" | "-h" => {
print_help();
}
"--version" | "-v" => {
println!("Lux {}", VERSION);
}
"fmt" => {
// Format files (auto-discovers if no file specified)
format_files(&args[2..]);
}
"test" => {
// Run tests
run_tests(&args[2..]);
}
"watch" => {
// Watch mode
if args.len() < 3 {
eprintln!("Usage: lux watch <file.lux>");
std::process::exit(1);
}
watch_file(&args[2]);
}
"init" => {
// Initialize a new project
init_project(args.get(2).map(|s| s.as_str()));
}
"check" => {
// Type check files (auto-discovers if no file specified)
check_files(&args[2..]);
}
"debug" => {
// Start debugger
if args.len() < 3 {
eprintln!("Usage: lux debug <file.lux>");
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);
}
}
"compile" => {
// Compile to native binary or JavaScript
if args.len() < 3 {
eprintln!("Usage: lux compile <file.lux> [-o binary]");
eprintln!(" lux compile <file.lux> --run");
eprintln!(" lux compile <file.lux> --emit-c [-o file.c]");
eprintln!(" lux compile <file.lux> --target js [-o file.js]");
std::process::exit(1);
}
let run_after = args.iter().any(|a| a == "--run");
let emit_c = args.iter().any(|a| a == "--emit-c");
let target_js = args.iter()
.position(|a| a == "--target")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str() == "js")
.unwrap_or(false);
let output_path = args.iter()
.position(|a| a == "-o")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str());
if target_js {
compile_to_js(&args[2], output_path, run_after);
} else {
compile_to_c(&args[2], output_path, run_after, emit_c);
}
}
path => {
// Run a file
run_file(path);
}
}
} else {
// Start REPL
run_repl();
}
}
fn print_help() {
println!("Lux {} - A functional language with first-class effects", VERSION);
println!();
println!("Usage:");
println!(" lux Start the REPL");
println!(" lux <file.lux> Run a file (interpreter)");
println!(" lux compile <file.lux> Compile to native binary");
println!(" lux compile <f> -o app Compile to binary named 'app'");
println!(" lux compile <f> --run Compile and execute");
println!(" lux compile <f> --emit-c Output C code instead of binary");
println!(" lux compile <f> --target js Compile to JavaScript");
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 <file.lux> Watch and re-run on changes");
println!(" lux debug <file.lux> Start interactive debugger");
println!(" lux init [name] Initialize a new project");
println!(" lux pkg <command> Package manager (install, add, remove, list, update)");
println!(" lux registry Start package registry server");
println!(" -s, --storage <dir> Storage directory (default: ./lux-registry)");
println!(" -b, --bind <addr> Bind address (default: 127.0.0.1:8080)");
println!(" lux --lsp Start LSP server (for IDE integration)");
println!(" lux --help Show this help");
println!(" lux --version Show version");
}
fn format_files(args: &[String]) {
use formatter::{format, FormatConfig};
use std::path::Path;
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 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!("{}: 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 {
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_files(args: &[String]) {
use modules::ModuleLoader;
use std::path::Path;
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!("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);
}
}
fn collect_lux_files_nonrecursive(dir: &str, pattern: Option<&str>, files: &mut Vec<std::path::PathBuf>) {
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) {
use codegen::c_backend::CBackend;
use modules::ModuleLoader;
use std::path::Path;
use std::process::Command;
let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file '{}': {}", path, e);
std::process::exit(1);
}
};
// Parse with module loading
let mut loader = ModuleLoader::new();
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p,
Err(e) => {
eprintln!("Module error: {}", e);
std::process::exit(1);
}
};
// Type check
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
for error in errors {
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
}
std::process::exit(1);
}
// Generate C code
let mut backend = CBackend::new();
let c_code = match backend.generate(&program) {
Ok(code) => code,
Err(e) => {
eprintln!("C codegen error: {}", e);
eprintln!();
eprintln!("Note: The C backend supports functions, closures, ADTs,");
eprintln!("pattern matching, lists, and Console.print.");
eprintln!();
eprintln!("Not yet supported: other effects, some advanced features.");
std::process::exit(1);
}
};
// Handle --emit-c: output C code instead of binary
if emit_c {
if let Some(out_path) = output_path {
if let Err(e) = std::fs::write(out_path, &c_code) {
eprintln!("Error writing file '{}': {}", out_path, e);
std::process::exit(1);
}
eprintln!("Wrote C code to {}", out_path);
} else {
println!("{}", c_code);
}
return;
}
// Default: compile to native binary
let temp_c = std::env::temp_dir().join("lux_output.c");
// Determine output binary name
let output_bin = if let Some(out) = output_path {
Path::new(out).to_path_buf()
} else {
// Derive from source filename: foo.lux -> ./foo
let stem = file_path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("a.out");
Path::new(".").join(stem)
};
if let Err(e) = std::fs::write(&temp_c, &c_code) {
eprintln!("Error writing temp file: {}", e);
std::process::exit(1);
}
// Find C compiler
let cc = std::env::var("CC").unwrap_or_else(|_| "cc".to_string());
let compile_result = Command::new(&cc)
.args(["-O2", "-o"])
.arg(&output_bin)
.arg(&temp_c)
.output();
match compile_result {
Ok(output) => {
if !output.status.success() {
eprintln!("C compilation failed:");
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
std::process::exit(1);
}
}
Err(e) => {
eprintln!("Failed to run C compiler '{}': {}", cc, e);
eprintln!("Make sure gcc or clang is installed, or set CC environment variable.");
std::process::exit(1);
}
}
// 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
let run_result = Command::new(&output_bin).status();
match run_result {
Ok(status) => {
std::process::exit(status.code().unwrap_or(1));
}
Err(e) => {
eprintln!("Failed to run compiled binary: {}", e);
std::process::exit(1);
}
}
} else {
// Just print where the binary is
eprintln!("Compiled to {}", output_bin.display());
}
}
fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) {
use codegen::js_backend::JsBackend;
use modules::ModuleLoader;
use std::path::Path;
use std::process::Command;
let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file '{}': {}", path, e);
std::process::exit(1);
}
};
// Parse with module loading
let mut loader = ModuleLoader::new();
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p,
Err(e) => {
eprintln!("Module error: {}", e);
std::process::exit(1);
}
};
// Type check
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
for error in errors {
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
}
std::process::exit(1);
}
// Generate JavaScript code
let mut backend = JsBackend::new();
let js_code = match backend.generate(&program) {
Ok(code) => code,
Err(e) => {
eprintln!("JS codegen error: {}", e);
std::process::exit(1);
}
};
// Determine output file path
let output_js = if let Some(out) = output_path {
Path::new(out).to_path_buf()
} else {
// Derive from source filename: foo.lux -> ./foo.js
let stem = file_path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
Path::new(".").join(format!("{}.js", stem))
};
// Write the JavaScript file
if let Err(e) = std::fs::write(&output_js, &js_code) {
eprintln!("Error writing file '{}': {}", output_js.display(), e);
std::process::exit(1);
}
if run_after {
// Run with Node.js
let node_result = Command::new("node")
.arg(&output_js)
.status();
match node_result {
Ok(status) => {
std::process::exit(status.code().unwrap_or(1));
}
Err(e) => {
eprintln!("Failed to run with Node.js: {}", e);
eprintln!("Make sure Node.js is installed.");
std::process::exit(1);
}
}
} else {
eprintln!("Compiled to {}", output_js.display());
}
}
fn run_tests(args: &[String]) {
use std::path::Path;
use std::fs;
// Find test files
let pattern = args.first().map(|s| s.as_str());
// Look for test files in multiple locations
let mut test_files = Vec::new();
// Current directory
collect_test_files(".", pattern, &mut test_files);
// tests/ subdirectory
if Path::new("tests").is_dir() {
collect_test_files("tests", pattern, &mut test_files);
}
// examples/ subdirectory (for example tests)
if Path::new("examples").is_dir() {
collect_test_files("examples", pattern, &mut test_files);
}
// If a specific file is given, use that
if let Some(p) = pattern {
if Path::new(p).is_file() {
test_files.clear();
test_files.push(std::path::PathBuf::from(p));
}
}
if test_files.is_empty() {
println!("No test files found.");
println!("Test files should be named test_*.lux or *_test.lux");
println!("Or contain functions named test_*");
return;
}
println!("Running tests...\n");
let mut total_passed = 0;
let mut total_failed = 0;
let mut all_failures = Vec::new();
for test_file in &test_files {
let path_str = test_file.to_string_lossy().to_string();
// Read and parse the file
let source = match fs::read_to_string(test_file) {
Ok(s) => s,
Err(e) => {
println!(" {} ... ERROR: {}", path_str, e);
total_failed += 1;
continue;
}
};
let program = match Parser::parse_source(&source) {
Ok(p) => p,
Err(e) => {
println!(" {} ... PARSE ERROR: {}", path_str, e);
total_failed += 1;
continue;
}
};
// Type check
let mut checker = typechecker::TypeChecker::new();
if let Err(errors) = checker.check_program(&program) {
println!(" {} ... TYPE ERROR", path_str);
for err in errors {
eprintln!(" {}", err);
}
total_failed += 1;
continue;
}
// 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(&program) {
Ok(_) => {
let results = interp.get_test_results();
if results.failed == 0 && results.passed == 0 {
// No Test assertions, just check it runs
println!(" {} ... OK (no assertions)", path_str);
total_passed += 1;
} else if results.failed == 0 {
println!(" {} ... OK ({} assertions)", path_str, results.passed);
total_passed += results.passed;
} else {
println!(" {} ... FAILED ({} passed, {} failed)", path_str, results.passed, results.failed);
total_passed += results.passed;
total_failed += results.failed;
for failure in &results.failures {
all_failures.push((path_str.clone(), "".to_string(), failure.clone()));
}
}
}
Err(e) => {
println!(" {} ... RUNTIME ERROR: {}", path_str, e);
total_failed += 1;
}
}
} else {
// Run individual test functions
println!(" {}:", path_str);
for test_name in &test_funcs {
let mut interp = Interpreter::new();
interp.register_auto_migrations(&auto_migrations);
interp.reset_test_results();
// First run the file to define all functions
if let Err(e) = interp.run(&program) {
println!(" {} ... ERROR: {}", test_name, e);
total_failed += 1;
continue;
}
// Call the test function
let call_source = format!("let testResult = run {}() with {{}}", test_name);
let call_program = match Parser::parse_source(&call_source) {
Ok(p) => p,
Err(e) => {
println!(" {} ... ERROR: {}", test_name, e);
total_failed += 1;
continue;
}
};
match interp.run(&call_program) {
Ok(_) => {
let results = interp.get_test_results();
if results.failed == 0 {
println!(" {} ... OK", test_name);
total_passed += 1;
} else {
println!(" {} ... FAILED", test_name);
total_failed += 1;
for failure in &results.failures {
all_failures.push((path_str.clone(), test_name.clone(), failure.clone()));
}
}
}
Err(e) => {
println!(" {} ... ERROR: {}", test_name, e);
total_failed += 1;
}
}
}
}
}
// Print failure details
if !all_failures.is_empty() {
println!("\n--- Failures ---\n");
for (file, test, failure) in &all_failures {
if test.is_empty() {
println!("{}:", file);
} else {
println!("{} - {}:", file, test);
}
println!(" {}", failure.message);
if let Some(expected) = &failure.expected {
println!(" Expected: {}", expected);
}
if let Some(actual) = &failure.actual {
println!(" Actual: {}", actual);
}
println!();
}
}
println!("Results: {} passed, {} failed", total_passed, total_failed);
if total_failed > 0 {
std::process::exit(1);
}
}
fn collect_test_files(dir: &str, pattern: Option<&str>, files: &mut Vec<std::path::PathBuf>) {
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<std::path::PathBuf>) {
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 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 <name>' 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 <package> [version] [--git <url>] [--path <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 <package>");
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 <query>");
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<String, String> {
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::<u16>().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 <command> [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 <pkg> Add a dependency");
println!(" Options:");
println!(" [version] Specify version (default: 0.0.0)");
println!(" --git <url> Install from git repository");
println!(" --branch <name> Git branch (with --git)");
println!(" --path <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 <query> 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 <package> 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: {}", project_name);
println!();
println!("Project structure:");
println!(" {}/", project_name);
println!(" ├── lux.toml");
println!(" ├── src/");
println!(" │ └── main.lux");
println!(" └── tests/");
println!(" └── test_example.lux");
println!();
println!("To get started:");
println!(" cd {}", project_name);
println!(" lux src/main.lux");
}
fn run_file(path: &str) {
use modules::ModuleLoader;
use std::path::Path;
let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file '{}': {}", path, e);
std::process::exit(1);
}
};
// Set up module loader with the file's directory as a search path
let mut loader = ModuleLoader::new();
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
// Load and parse the program (including any imports)
let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p,
Err(e) => {
eprintln!("Module error: {}", e);
std::process::exit(1);
}
};
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
for error in errors {
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
}
std::process::exit(1);
}
let mut interp = Interpreter::new();
match interp.run_with_modules(&program, &loader) {
Ok(value) => {
if !matches!(value, interpreter::Value::Unit) {
println!("{}", value);
}
}
Err(e) => {
let diagnostic = e.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
std::process::exit(1);
}
}
}
/// REPL helper for tab completion and syntax highlighting
struct LuxHelper {
keywords: HashSet<String>,
commands: Vec<String>,
user_defined: HashSet<String>,
}
impl LuxHelper {
fn new() -> Self {
let keywords: HashSet<String> = vec![
"fn", "let", "if", "then", "else", "match", "with", "effect", "handler",
"handle", "resume", "type", "import", "pub", "is", "pure", "total",
"idempotent", "deterministic", "commutative", "where", "assume", "true",
"false", "None", "Some", "Ok", "Err", "Int", "Float", "String", "Bool",
"Char", "Unit", "Option", "Result", "List",
]
.into_iter()
.map(String::from)
.collect();
let commands = vec![
":help", ":h", ":quit", ":q", ":type", ":t", ":clear", ":load", ":l",
":trace", ":traces", ":info", ":i", ":env", ":doc", ":d", ":browse", ":b",
]
.into_iter()
.map(String::from)
.collect();
Self {
keywords,
commands,
user_defined: HashSet::new(),
}
}
fn add_definition(&mut self, name: &str) {
self.user_defined.insert(name.to_string());
}
}
impl Completer for LuxHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Pair>)> {
// 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<String> {
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::path::PathBuf> {
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<LuxHelper, DefaultHistory> = 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<String> {
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 <expression>");
}
}
":info" | ":i" => {
if let Some(name) = arg {
show_info(name, checker);
} else {
println!("Usage: :info <name>");
}
}
":env" => {
show_environment(checker, helper);
}
":clear" => {
*interp = Interpreter::new();
*checker = TypeChecker::new();
helper.user_defined.clear();
println!("Environment cleared.");
}
":load" | ":l" => {
if let Some(path) = arg {
load_file(path, interp, checker, helper);
} else {
println!("Usage: :load <filename>");
}
}
":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 <name>");
}
}
":browse" | ":b" => {
if let Some(module) = arg {
browse_module(module);
} else {
println!("Usage: :browse <module>");
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!(" {} : <unknown>", name);
}
}
}
}
fn show_doc(name: &str) {
// Built-in documentation for common functions
let doc = match name {
// List functions
"List.length" => Some(("List.length : List<T> -> Int", "Returns the number of elements in a list.")),
"List.head" => Some(("List.head : List<T> -> Option<T>", "Returns the first element of a list, or None if empty.")),
"List.tail" => Some(("List.tail : List<T> -> Option<List<T>>", "Returns all elements except the first, or None if empty.")),
"List.map" => Some(("List.map : (List<A>, fn(A) -> B) -> List<B>", "Applies a function to each element, returning a new list.")),
"List.filter" => Some(("List.filter : (List<T>, fn(T) -> Bool) -> List<T>", "Returns elements for which the predicate returns true.")),
"List.fold" => Some(("List.fold : (List<T>, U, fn(U, T) -> U) -> U", "Reduces a list to a single value using an accumulator function.")),
"List.reverse" => Some(("List.reverse : List<T> -> List<T>", "Returns a new list with elements in reverse order.")),
"List.concat" => Some(("List.concat : (List<T>, List<T>) -> List<T>", "Concatenates two lists.")),
"List.take" => Some(("List.take : (List<T>, Int) -> List<T>", "Returns the first n elements.")),
"List.drop" => Some(("List.drop : (List<T>, Int) -> List<T>", "Returns all elements after the first n.")),
"List.get" => Some(("List.get : (List<T>, Int) -> Option<T>", "Returns the element at index n, or None if out of bounds.")),
"List.contains" => Some(("List.contains : (List<T>, T) -> Bool", "Returns true if the list contains the element.")),
"List.all" => Some(("List.all : (List<T>, fn(T) -> Bool) -> Bool", "Returns true if all elements satisfy the predicate.")),
"List.any" => Some(("List.any : (List<T>, fn(T) -> Bool) -> Bool", "Returns true if any element satisfies the predicate.")),
"List.find" => Some(("List.find : (List<T>, fn(T) -> Bool) -> Option<T>", "Returns the first element satisfying the predicate.")),
"List.sort" => Some(("List.sort : List<Int> -> List<Int>", "Sorts a list of integers in ascending order.")),
"List.range" => Some(("List.range : (Int, Int) -> List<Int>", "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<String>", "Splits a string by a delimiter.")),
"String.join" => Some(("String.join : (List<String>, 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<A>, fn(A) -> B) -> Option<B>", "Applies a function to the value inside Some, or returns None.")),
"Option.flatMap" => Some(("Option.flatMap : (Option<A>, fn(A) -> Option<B>) -> Option<B>", "Like map, but the function returns an Option.")),
"Option.getOrElse" => Some(("Option.getOrElse : (Option<T>, T) -> T", "Returns the value inside Some, or the default if None.")),
"Option.isSome" => Some(("Option.isSome : Option<T> -> Bool", "Returns true if the value is Some.")),
"Option.isNone" => Some(("Option.isNone : Option<T> -> Bool", "Returns true if the value is None.")),
// Result functions
"Result.map" => Some(("Result.map : (Result<A, E>, fn(A) -> B) -> Result<B, E>", "Applies a function to the Ok value.")),
"Result.flatMap" => Some(("Result.flatMap : (Result<A, E>, fn(A) -> Result<B, E>) -> Result<B, E>", "Like map, but the function returns a Result.")),
"Result.getOrElse" => Some(("Result.getOrElse : (Result<T, E>, T) -> T", "Returns the Ok value, or the default if Err.")),
"Result.isOk" => Some(("Result.isOk : Result<T, E> -> Bool", "Returns true if the value is Ok.")),
"Result.isErr" => Some(("Result.isErr : Result<T, E> -> 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<Row>", "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<Row>", "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<T> = ...", "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<T> -> Int"),
("head", "List<T> -> Option<T>"),
("tail", "List<T> -> Option<List<T>>"),
("get", "(List<T>, Int) -> Option<T>"),
("map", "(List<A>, fn(A) -> B) -> List<B>"),
("filter", "(List<T>, fn(T) -> Bool) -> List<T>"),
("fold", "(List<T>, U, fn(U, T) -> U) -> U"),
("reverse", "List<T> -> List<T>"),
("concat", "(List<T>, List<T>) -> List<T>"),
("take", "(List<T>, Int) -> List<T>"),
("drop", "(List<T>, Int) -> List<T>"),
("contains", "(List<T>, T) -> Bool"),
("all", "(List<T>, fn(T) -> Bool) -> Bool"),
("any", "(List<T>, fn(T) -> Bool) -> Bool"),
("find", "(List<T>, fn(T) -> Bool) -> Option<T>"),
("sort", "List<Int> -> List<Int>"),
("range", "(Int, Int) -> List<Int>"),
("forEach", "(List<T>, fn(T) -> Unit) -> Unit"),
],
"String" => vec![
("length", "String -> Int"),
("concat", "(String, String) -> String"),
("substring", "(String, Int, Int) -> String"),
("split", "(String, String) -> List<String>"),
("join", "(List<String>, 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<String>"),
],
"Option" => vec![
("map", "(Option<A>, fn(A) -> B) -> Option<B>"),
("flatMap", "(Option<A>, fn(A) -> Option<B>) -> Option<B>"),
("getOrElse", "(Option<T>, T) -> T"),
("isSome", "Option<T> -> Bool"),
("isNone", "Option<T> -> Bool"),
],
"Result" => vec![
("map", "(Result<A, E>, fn(A) -> B) -> Result<B, E>"),
("flatMap", "(Result<A, E>, fn(A) -> Result<B, E>) -> Result<B, E>"),
("getOrElse", "(Result<T, E>, T) -> T"),
("isOk", "Result<T, E> -> Bool"),
("isErr", "Result<T, E> -> 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<Row>"),
("queryOne", "(SqlConn, String) -> Option<Row>"),
("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<Row>"),
("queryOne", "(Int, String) -> Option<Row>"),
("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 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<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::<Vec<_>>()
.join("\n")
})?;
let mut interp = Interpreter::new();
let value = interp.run(&program).map_err(|e| e.to_string())?;
Ok(format!("{}", value))
}
#[test]
fn test_arithmetic() {
assert_eq!(eval("let x = 1 + 2").unwrap(), "3");
assert_eq!(eval("let x = 10 - 3").unwrap(), "7");
assert_eq!(eval("let x = 4 * 5").unwrap(), "20");
assert_eq!(eval("let x = 15 / 3").unwrap(), "5");
}
#[test]
fn test_function() {
let source = r#"
fn add(a: Int, b: Int): Int = a + b
let result = add(3, 4)
"#;
assert_eq!(eval(source).unwrap(), "7");
}
#[test]
fn test_if_expr() {
let source = r#"
fn max(a: Int, b: Int): Int = if a > b then a else b
let result = max(5, 3)
"#;
assert_eq!(eval(source).unwrap(), "5");
}
#[test]
fn test_recursion() {
let source = r#"
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
let result = factorial(5)
"#;
assert_eq!(eval(source).unwrap(), "120");
}
#[test]
fn test_lambda() {
let source = r#"
let double = fn(x: Int): Int => x * 2
let result = double(21)
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_records() {
let source = r#"
let person = { name: "Alice", age: 30 }
let result = person.age
"#;
assert_eq!(eval(source).unwrap(), "30");
}
#[test]
fn test_lists() {
let source = "let nums = [1, 2, 3]";
assert_eq!(eval(source).unwrap(), "[1, 2, 3]");
}
#[test]
fn test_tuples() {
let source = "let pair = (42, \"hello\")";
assert_eq!(eval(source).unwrap(), "(42, \"hello\")");
}
#[test]
fn test_block() {
let source = r#"
let result = {
let x = 10
let y = 20
x + y
}
"#;
assert_eq!(eval(source).unwrap(), "30");
}
#[test]
fn test_pipe() {
let source = r#"
fn double(x: Int): Int = x * 2
fn add_one(x: Int): Int = x + 1
let result = 5 |> double |> add_one
"#;
assert_eq!(eval(source).unwrap(), "11");
}
// ============ Standard Library Tests ============
// List tests
#[test]
fn test_list_length() {
assert_eq!(eval("let x = List.length([1, 2, 3])").unwrap(), "3");
assert_eq!(eval("let x = List.length([])").unwrap(), "0");
}
#[test]
fn test_list_reverse() {
assert_eq!(
eval("let x = List.reverse([1, 2, 3])").unwrap(),
"[3, 2, 1]"
);
assert_eq!(eval("let x = List.reverse([])").unwrap(), "[]");
}
#[test]
fn test_list_range() {
assert_eq!(eval("let x = List.range(0, 5)").unwrap(), "[0, 1, 2, 3, 4]");
assert_eq!(eval("let x = List.range(3, 3)").unwrap(), "[]");
assert_eq!(eval("let x = List.range(-2, 2)").unwrap(), "[-2, -1, 0, 1]");
}
#[test]
fn test_list_head() {
assert_eq!(eval("let x = List.head([1, 2, 3])").unwrap(), "Some(1)");
assert_eq!(eval("let x = List.head([])").unwrap(), "None");
}
#[test]
fn test_list_tail() {
assert_eq!(
eval("let x = List.tail([1, 2, 3])").unwrap(),
"Some([2, 3])"
);
assert_eq!(eval("let x = List.tail([1])").unwrap(), "Some([])");
assert_eq!(eval("let x = List.tail([])").unwrap(), "None");
}
#[test]
fn test_list_concat() {
assert_eq!(
eval("let x = List.concat([1, 2], [3, 4])").unwrap(),
"[1, 2, 3, 4]"
);
assert_eq!(eval("let x = List.concat([], [1])").unwrap(), "[1]");
assert_eq!(eval("let x = List.concat([1], [])").unwrap(), "[1]");
}
#[test]
fn test_list_get() {
assert_eq!(
eval("let x = List.get([10, 20, 30], 0)").unwrap(),
"Some(10)"
);
assert_eq!(
eval("let x = List.get([10, 20, 30], 2)").unwrap(),
"Some(30)"
);
assert_eq!(eval("let x = List.get([10, 20, 30], 5)").unwrap(), "None");
assert_eq!(eval("let x = List.get([10, 20, 30], -1)").unwrap(), "None");
}
#[test]
fn test_list_map() {
let source = r#"
fn double(x: Int): Int = x * 2
let result = List.map([1, 2, 3], double)
"#;
assert_eq!(eval(source).unwrap(), "[2, 4, 6]");
}
#[test]
fn test_list_map_lambda() {
let source = "let x = List.map([1, 2, 3], fn(x: Int): Int => x * x)";
assert_eq!(eval(source).unwrap(), "[1, 4, 9]");
}
#[test]
fn test_list_filter() {
let source = "let x = List.filter([1, 2, 3, 4, 5], fn(x: Int): Bool => x > 2)";
assert_eq!(eval(source).unwrap(), "[3, 4, 5]");
}
#[test]
fn test_list_filter_all() {
let source = "let x = List.filter([1, 2, 3], fn(x: Int): Bool => x > 10)";
assert_eq!(eval(source).unwrap(), "[]");
}
#[test]
fn test_list_fold() {
let source = "let x = List.fold([1, 2, 3, 4], 0, fn(acc: Int, x: Int): Int => acc + x)";
assert_eq!(eval(source).unwrap(), "10");
}
#[test]
fn test_list_fold_product() {
let source = "let x = List.fold([1, 2, 3, 4], 1, fn(acc: Int, x: Int): Int => acc * x)";
assert_eq!(eval(source).unwrap(), "24");
}
// String tests
#[test]
fn test_string_length() {
assert_eq!(eval(r#"let x = String.length("hello")"#).unwrap(), "5");
assert_eq!(eval(r#"let x = String.length("")"#).unwrap(), "0");
}
#[test]
fn test_string_split() {
assert_eq!(
eval(r#"let x = String.split("a,b,c", ",")"#).unwrap(),
r#"["a", "b", "c"]"#
);
assert_eq!(
eval(r#"let x = String.split("hello", ",")"#).unwrap(),
r#"["hello"]"#
);
}
#[test]
fn test_string_join() {
assert_eq!(
eval(r#"let x = String.join(["a", "b", "c"], "-")"#).unwrap(),
r#""a-b-c""#
);
assert_eq!(
eval(r#"let x = String.join(["hello"], ",")"#).unwrap(),
r#""hello""#
);
assert_eq!(eval(r#"let x = String.join([], ",")"#).unwrap(), r#""""#);
}
#[test]
fn test_string_trim() {
assert_eq!(
eval(r#"let x = String.trim(" hello ")"#).unwrap(),
r#""hello""#
);
assert_eq!(
eval(r#"let x = String.trim("hello")"#).unwrap(),
r#""hello""#
);
assert_eq!(eval(r#"let x = String.trim(" ")"#).unwrap(), r#""""#);
}
#[test]
fn test_string_contains() {
assert_eq!(
eval(r#"let x = String.contains("hello world", "world")"#).unwrap(),
"true"
);
assert_eq!(
eval(r#"let x = String.contains("hello", "xyz")"#).unwrap(),
"false"
);
assert_eq!(
eval(r#"let x = String.contains("hello", "")"#).unwrap(),
"true"
);
}
#[test]
fn test_string_replace() {
assert_eq!(
eval(r#"let x = String.replace("hello", "l", "L")"#).unwrap(),
r#""heLLo""#
);
assert_eq!(
eval(r#"let x = String.replace("aaa", "a", "b")"#).unwrap(),
r#""bbb""#
);
}
#[test]
fn test_string_chars() {
assert_eq!(eval(r#"let x = String.chars("hi")"#).unwrap(), "['h', 'i']");
assert_eq!(eval(r#"let x = String.chars("")"#).unwrap(), "[]");
}
#[test]
fn test_string_lines() {
// Note: Using actual newline in the string
let source = r#"let x = String.lines("a
b
c")"#;
assert_eq!(eval(source).unwrap(), r#"["a", "b", "c"]"#);
}
// String interpolation tests
#[test]
fn test_string_interpolation_simple() {
let source = r#"
let name = "World"
let x = "Hello, {name}!"
"#;
assert_eq!(eval(source).unwrap(), r#""Hello, World!""#);
}
#[test]
fn test_string_interpolation_numbers() {
let source = r#"
let n = 42
let x = "The answer is {n}"
"#;
assert_eq!(eval(source).unwrap(), r#""The answer is 42""#);
}
#[test]
fn test_string_interpolation_expressions() {
let source = r#"
let a = 2
let b = 3
let x = "{a} + {b} = {a + b}"
"#;
assert_eq!(eval(source).unwrap(), r#""2 + 3 = 5""#);
}
#[test]
fn test_string_interpolation_escaped_braces() {
let source = r#"let x = "literal \{braces\}""#;
assert_eq!(eval(source).unwrap(), r#""literal {braces}""#);
}
// Option tests
#[test]
fn test_option_constructors() {
assert_eq!(eval("let x = Some(42)").unwrap(), "Some(42)");
assert_eq!(eval("let x = None").unwrap(), "None");
}
#[test]
fn test_option_is_some() {
assert_eq!(eval("let x = Option.isSome(Some(42))").unwrap(), "true");
assert_eq!(eval("let x = Option.isSome(None)").unwrap(), "false");
}
#[test]
fn test_option_is_none() {
assert_eq!(eval("let x = Option.isNone(None)").unwrap(), "true");
assert_eq!(eval("let x = Option.isNone(Some(42))").unwrap(), "false");
}
#[test]
fn test_option_get_or_else() {
assert_eq!(eval("let x = Option.getOrElse(Some(42), 0)").unwrap(), "42");
assert_eq!(eval("let x = Option.getOrElse(None, 0)").unwrap(), "0");
}
#[test]
fn test_option_map() {
let source = "let x = Option.map(Some(5), fn(x: Int): Int => x * 2)";
assert_eq!(eval(source).unwrap(), "Some(10)");
}
#[test]
fn test_option_map_none() {
let source = "let x = Option.map(None, fn(x: Int): Int => x * 2)";
assert_eq!(eval(source).unwrap(), "None");
}
#[test]
fn test_option_flat_map() {
let source = "let x = Option.flatMap(Some(5), fn(x: Int): Option<Int> => 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<Int> => 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<F>(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<F>(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<F>(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::<Vec<_>>().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");
}
}
// 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<Int>, 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>): 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, String>): 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<Int>, 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::<Vec<_>>().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();
}
}
}