Files
lux/src/main.rs
Brandon Lucas ec365ebb3f feat: add File.copy and propagate effectful callback effects (WISH-7, WISH-14)
File.copy(source, dest) copies files via interpreter (std::fs::copy) and
C backend (fread/fwrite). Effectful callbacks passed to higher-order
functions like List.map/forEach now propagate their effects to the
enclosing function's inferred effect set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:24:28 -05:00

5684 lines
194 KiB
Rust

//! Lux — Make the important things visible.
//!
//! A functional programming language with first-class effects, schema evolution,
//! and behavioral types. See `lux philosophy` or docs/PHILOSOPHY.md.
mod analysis;
mod ast;
mod codegen;
mod debugger;
mod diagnostics;
mod exhaustiveness;
mod formatter;
mod interpreter;
mod lexer;
mod linter;
mod lsp;
mod modules;
mod package;
mod parser;
mod registry;
mod schema;
mod symbol_table;
mod typechecker;
mod types;
use diagnostics::{render, c, bc, colors};
use interpreter::Interpreter;
use parser::Parser;
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::history::DefaultHistory;
use rustyline::validate::Validator;
use rustyline::{Config, Editor, Helper};
use std::borrow::Cow;
use std::collections::HashSet;
use typechecker::TypeChecker;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const HELP: &str = r#"
Lux - A functional language with first-class effects
Commands:
:help, :h Show this help
:quit, :q Exit the REPL
:type <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
:reload, :r Reload the last loaded file
:trace on/off Enable/disable effect tracing
:traces Show recorded effect traces
:ast <expr> Show the AST of an expression (for debugging)
Keyboard:
Tab Autocomplete
Ctrl-C Cancel current input
Ctrl-D Exit
Up/Down Browse history
Ctrl-R Search history
Effects:
All code in the REPL runs with Console, File, and other standard effects.
Use :trace on to see effect invocations during execution.
Examples:
> let x = 42
> x + 1
43
> fn double(n: Int): Int = n * 2
> :type double
double : fn(Int) -> Int
> :load myfile.lux
> :reload
> double(21)
42
> match Some(5) { Some(x) => x, None => 0 }
5
"#;
fn main() {
let args: Vec<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!("{}", bc(colors::GREEN, &format!("Lux {}", VERSION)));
}
"fmt" | "f" => {
// Format files (auto-discovers if no file specified)
format_files(&args[2..]);
}
"lint" | "l" => {
// Lint files
lint_files(&args[2..]);
}
"test" | "t" => {
// Run tests
run_tests(&args[2..]);
}
"watch" => {
// Watch mode
if args.len() < 3 {
eprintln!("Usage: lux watch <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" | "k" => {
// Type check files (auto-discovers if no file specified)
check_files(&args[2..]);
}
"debug" => {
// Start debugger
if args.len() < 3 {
eprintln!("Usage: lux debug <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);
}
}
"serve" | "s" => {
// Static file server (like python -m http.server)
let port = args.iter()
.position(|a| a == "--port" || a == "-p")
.and_then(|i| args.get(i + 1))
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(8080);
let port_value_idx = args.iter()
.position(|a| a == "--port" || a == "-p")
.map(|i| i + 1);
let dir = args.iter().enumerate()
.skip(2)
.filter(|(i, a)| !a.starts_with('-') && Some(*i) != port_value_idx)
.map(|(_, a)| a.as_str())
.next()
.unwrap_or(".");
serve_static_files(dir, port);
}
"compile" | "c" => {
// Compile to native binary or JavaScript
if args.len() < 3 {
eprintln!("Usage: lux compile <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]");
eprintln!(" lux compile <file.lux> --watch");
std::process::exit(1);
}
let run_after = args.iter().any(|a| a == "--run");
let emit_c = args.iter().any(|a| a == "--emit-c");
let watch = args.iter().any(|a| a == "--watch");
let target_js = args.iter()
.position(|a| a == "--target")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str() == "js")
.unwrap_or(false);
let output_path = args.iter()
.position(|a| a == "-o")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str());
if target_js {
compile_to_js(&args[2], output_path, run_after);
} else {
compile_to_c(&args[2], output_path, run_after, emit_c);
}
if watch {
// Build the args to replay for each recompilation (without --watch)
let compile_args: Vec<String> = args.iter()
.skip(1)
.filter(|a| a.as_str() != "--watch")
.cloned()
.collect();
watch_and_rerun(&args[2], &compile_args);
}
}
"repl" => {
// Start REPL
run_repl();
}
"doc" => {
// Generate API documentation
generate_docs(&args[2..]);
}
"philosophy" => {
print_philosophy();
}
cmd => {
// Check if it looks like a command typo
if !std::path::Path::new(cmd).exists() && !cmd.starts_with('-') && !cmd.contains('.') && !cmd.contains('/') {
let known_commands = vec![
"fmt", "lint", "test", "watch", "init", "check", "debug",
"pkg", "registry", "serve", "compile", "doc", "repl", "philosophy",
];
let suggestions = diagnostics::find_similar_names(cmd, known_commands.into_iter(), 2);
if !suggestions.is_empty() {
if let Some(hint) = diagnostics::format_did_you_mean(&suggestions) {
eprintln!("{} Unknown command '{}'. {}", c(colors::YELLOW, "warning:"), cmd, c(colors::YELLOW, &hint));
std::process::exit(1);
}
}
}
// Run as file
run_file(cmd);
}
}
} else {
// No arguments — show help
print_help();
}
}
fn print_help() {
println!("{}", bc(colors::GREEN, &format!("Lux {}", VERSION)));
println!("{}", c(colors::DIM, "Make the important things visible."));
println!();
println!(" {} Effects in types — see what code does", c(colors::DIM, "·"));
println!(" {} Composition over configuration — no DI frameworks", c(colors::DIM, "·"));
println!(" {} Safety without ceremony — inference where it helps", c(colors::DIM, "·"));
println!(" {} One right way — opinionated formatter, integrated tools", c(colors::DIM, "·"));
println!();
println!("{}", bc("", "Usage:"));
println!();
println!(" {} Show this help", bc(colors::CYAN, "lux"));
println!(" {} Start the REPL", bc(colors::CYAN, "lux repl"));
println!(" {} {} Run a file (interpreter)", bc(colors::CYAN, "lux"), c(colors::YELLOW, "<file.lux>"));
println!(" {} {} {} Compile to native binary", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<file.lux>"));
println!(" {} {} {} {} Compile with output name", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "-o app"));
println!(" {} {} {} {} Compile and execute", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "--run"));
println!(" {} {} {} {} Output C code", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "--emit-c"));
println!(" {} {} {} {} Compile to JavaScript", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "--target js"));
println!(" {} {} {} Format files {}",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "fmt"), c(colors::YELLOW, "[file] [--check]"),
c(colors::DIM, "(alias: f)"));
println!(" {} {} {} Lint files {}",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "lint"), c(colors::YELLOW, "[file]"),
c(colors::DIM, "(alias: l)"));
println!(" {} {} {} Type check files {}",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "check"), c(colors::YELLOW, "[file]"),
c(colors::DIM, "(alias: k)"));
println!(" {} {} {} Run tests {}",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "test"), c(colors::YELLOW, "[pattern]"),
c(colors::DIM, "(alias: t)"));
println!(" {} {} {} Watch and re-run on changes",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "watch"), c(colors::YELLOW, "<file.lux>"));
println!(" {} {} {} Start interactive debugger",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "debug"), c(colors::YELLOW, "<file.lux>"));
println!(" {} {} {} Initialize a new project",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "init"), c(colors::YELLOW, "[name]"));
println!(" {} {} {} Package manager {}",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "pkg"), c(colors::YELLOW, "<command>"),
c(colors::DIM, "(install, add, remove, list, update)"));
println!(" {} {} Start package registry server",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "registry"));
println!(" {} {} {} Start static file server {}",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "serve"), c(colors::YELLOW, "[dir]"),
c(colors::DIM, "(alias: s)"));
println!(" {} {} {} Generate API documentation",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "doc"), c(colors::YELLOW, "[file] [-o dir]"));
println!(" {} {} Show language philosophy",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "philosophy"));
println!(" {} {} Start LSP server",
bc(colors::CYAN, "lux"), c(colors::YELLOW, "--lsp"));
println!(" {} {} Show this help",
bc(colors::CYAN, "lux"), c(colors::YELLOW, "--help"));
println!(" {} {} Show version",
bc(colors::CYAN, "lux"), c(colors::YELLOW, "--version"));
}
fn print_philosophy() {
println!("{}", bc(colors::GREEN, &format!("The Lux Philosophy")));
println!();
println!(" {}", bc("", "Make the important things visible."));
println!();
println!(" Most languages hide what matters most in production: what code");
println!(" can do, how data changes over time, and what guarantees functions");
println!(" provide. Lux makes all three first-class, compiler-checked features.");
println!();
println!(" {} {}", bc(colors::CYAN, "1. Explicit over implicit"), c(colors::DIM, "— effects in types, not hidden behind interfaces"));
println!(" fn processOrder(order: Order): Receipt {} {}", c(colors::YELLOW, "with {Database, Email}"), c(colors::DIM, "// signature IS documentation"));
println!();
println!(" {} {}", bc(colors::CYAN, "2. Composition over configuration"), c(colors::DIM, "— no DI frameworks, no monad transformers"));
println!(" run app() {} {}", c(colors::YELLOW, "with { Database = mock, Http = mock }"), c(colors::DIM, "// swap handlers, not libraries"));
println!();
println!(" {} {}", bc(colors::CYAN, "3. Safety without ceremony"), c(colors::DIM, "— type inference where it helps, annotations where they document"));
println!(" let x = 42 {}", c(colors::DIM, "// inferred"));
println!(" fn f(x: Int): Int = x * 2 {}", c(colors::DIM, "// annotated: API contract"));
println!();
println!(" {} {}", bc(colors::CYAN, "4. Practical over academic"), c(colors::DIM, "— ML semantics in C-family syntax, no monads to learn"));
println!(" {} {} {}", c(colors::DIM, "fn main(): Unit"), c(colors::YELLOW, "with {Console}"), c(colors::DIM, "= Console.print(\"Hello!\")"));
println!();
println!(" {} {}", bc(colors::CYAN, "5. One right way"), c(colors::DIM, "— opinionated formatter, integrated tooling, built-in testing"));
println!(" lux fmt | lux lint | lux check | lux test | lux compile");
println!();
println!(" {} {}", bc(colors::CYAN, "6. Tools are the language"), c(colors::DIM, "— formatter knows the AST, linter knows the types, LSP knows the effects"));
println!();
println!(" See {} for the full philosophy with language comparisons.", c(colors::CYAN, "docs/PHILOSOPHY.md"));
}
fn format_files(args: &[String]) {
use formatter::{format, FormatConfig};
use std::path::Path;
use std::time::Instant;
let start = Instant::now();
let check_only = args.iter().any(|a| a == "--check");
let pattern = args.iter().find(|a| !a.starts_with('-')).map(|s| s.as_str());
// Collect files to format
let mut files_to_format = Vec::new();
// If a specific file is given, use it
if let Some(p) = pattern {
if Path::new(p).is_file() {
files_to_format.push(std::path::PathBuf::from(p));
}
}
// If no specific file found, auto-discover
if files_to_format.is_empty() {
// Current directory (non-recursive for src/)
if Path::new("src").is_dir() {
collect_lux_files("src", pattern, &mut files_to_format);
}
// Also check current directory for top-level files
collect_lux_files_nonrecursive(".", pattern, &mut files_to_format);
// examples/ subdirectory
if Path::new("examples").is_dir() {
collect_lux_files("examples", pattern, &mut files_to_format);
}
// tests/ subdirectory
if Path::new("tests").is_dir() {
collect_lux_files("tests", pattern, &mut files_to_format);
}
}
if files_to_format.is_empty() {
println!("No .lux files found.");
println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/"));
return;
}
// Sort for consistent output
files_to_format.sort();
let config = FormatConfig::default();
let mut formatted_count = 0;
let mut unchanged_count = 0;
let mut error_count = 0;
let mut would_reformat = Vec::new();
for file_path in &files_to_format {
let path = file_path.to_string_lossy().to_string();
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
error_count += 1;
continue;
}
};
let formatted = match format(&source, &config) {
Ok(f) => f,
Err(e) => {
eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
error_count += 1;
continue;
}
};
if check_only {
if source != formatted {
would_reformat.push(path);
} else {
unchanged_count += 1;
}
} else if source != formatted {
if let Err(e) = std::fs::write(file_path, &formatted) {
eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
error_count += 1;
continue;
}
println!(" {} {}", c(colors::CYAN, "Formatted"), path);
formatted_count += 1;
} else {
unchanged_count += 1;
}
}
let elapsed = start.elapsed();
let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()));
println!();
if check_only {
if !would_reformat.is_empty() {
println!("Files that would be reformatted:");
for path in &would_reformat {
println!(" {}", c(colors::YELLOW, path));
}
println!();
println!("{} {} would be reformatted, {} already formatted {}",
c(colors::YELLOW, "\u{2717}"), would_reformat.len(), unchanged_count, time_str);
std::process::exit(1);
} else {
println!("{} All {} files are correctly formatted {}",
c(colors::GREEN, "\u{2713}"), unchanged_count, time_str);
}
} else if error_count > 0 {
let mut parts = Vec::new();
if formatted_count > 0 { parts.push(format!("{} formatted", formatted_count)); }
if unchanged_count > 0 { parts.push(format!("{} unchanged", unchanged_count)); }
parts.push(format!("{} errors", error_count));
println!("{} {} {}", c(colors::RED, "\u{2717}"), parts.join(", "), time_str);
std::process::exit(1);
} else if formatted_count > 0 {
println!("{} {} formatted, {} unchanged {}",
c(colors::GREEN, "\u{2713}"), formatted_count, unchanged_count, time_str);
} else {
println!("{} All {} files already formatted {}",
c(colors::GREEN, "\u{2713}"), unchanged_count, time_str);
}
}
fn lint_files(args: &[String]) {
use linter::{LintConfig, LintLevel, Linter};
use std::path::Path;
use std::time::Instant;
let start = Instant::now();
let explain = args.iter().any(|a| a == "--explain");
let _fix = args.iter().any(|a| a == "--fix");
let pattern = args
.iter()
.find(|a| !a.starts_with('-'))
.map(|s| s.as_str());
// Collect files to lint
let mut files_to_lint = Vec::new();
if let Some(p) = pattern {
if Path::new(p).is_file() {
files_to_lint.push(std::path::PathBuf::from(p));
}
}
if files_to_lint.is_empty() {
if Path::new("src").is_dir() {
collect_lux_files("src", pattern, &mut files_to_lint);
}
collect_lux_files_nonrecursive(".", pattern, &mut files_to_lint);
if Path::new("examples").is_dir() {
collect_lux_files("examples", pattern, &mut files_to_lint);
}
if Path::new("tests").is_dir() {
collect_lux_files("tests", pattern, &mut files_to_lint);
}
}
if files_to_lint.is_empty() {
println!("No .lux files found.");
println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/"));
return;
}
files_to_lint.sort();
let config = LintConfig::default();
let mut total_warnings = 0;
let mut total_errors = 0;
let mut files_clean = 0;
for file_path in &files_to_lint {
let path = file_path.to_string_lossy().to_string();
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
total_errors += 1;
continue;
}
};
let program = match Parser::parse_source(&source) {
Ok(p) => p,
Err(e) => {
eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, c(colors::RED, &format!("parse error: {}", e)));
total_errors += 1;
continue;
}
};
let linter = Linter::new(config.clone(), &source);
let lints = linter.lint(&program);
if lints.is_empty() {
files_clean += 1;
} else {
let errors = lints.iter().filter(|l| l.level == LintLevel::Deny).count();
let warnings = lints
.iter()
.filter(|l| l.level == LintLevel::Warn)
.count();
total_errors += errors;
total_warnings += warnings;
if explain {
eprint!("{}", linter::render_lints(&lints, &source, Some(&path)));
} else {
// Compact output: one line per lint
for lint in &lints {
let level_str = match lint.level {
LintLevel::Deny => c(colors::RED, "error"),
LintLevel::Warn => c(colors::YELLOW, "warning"),
LintLevel::Allow => continue,
};
let lint_code = c(
colors::DIM,
&format!("[{}/{}]", lint.id.category().category_name(), lint.id.name()),
);
if lint.span.start > 0 || lint.span.end > 0 {
let (line, col) =
diagnostics::offset_to_line_col(&source, lint.span.start);
eprintln!(
" {}{} {}:{}:{}: {}",
level_str, lint_code, path, line, col, lint.message
);
} else {
eprintln!(" {}{} {}: {}", level_str, lint_code, path, lint.message);
}
}
}
}
}
let elapsed = start.elapsed();
let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()));
println!();
if total_errors > 0 || total_warnings > 0 {
let mut parts = Vec::new();
if total_errors > 0 {
parts.push(c(colors::RED, &format!("{} errors", total_errors)));
}
if total_warnings > 0 {
parts.push(c(
colors::YELLOW,
&format!("{} warnings", total_warnings),
));
}
parts.push(format!("{} files clean", files_clean));
let icon = if total_errors > 0 {
c(colors::RED, "\u{2717}")
} else {
c(colors::YELLOW, "\u{26a0}")
};
println!("{} {} {}", icon, parts.join(", "), time_str);
if total_errors > 0 {
std::process::exit(1);
}
} else {
println!(
"{} {} files clean {}",
c(colors::GREEN, "\u{2713}"),
files_clean,
time_str
);
}
}
fn check_files(args: &[String]) {
use linter::{LintConfig, LintLevel, Linter};
use modules::ModuleLoader;
use std::path::Path;
use std::time::Instant;
let start = Instant::now();
let pattern = args.first().map(|s| s.as_str());
// Collect files to check
let mut files_to_check = Vec::new();
// If a specific file is given, use it
if let Some(p) = pattern {
if Path::new(p).is_file() {
files_to_check.push(std::path::PathBuf::from(p));
}
}
// If no specific file found, auto-discover
if files_to_check.is_empty() {
// Current directory (non-recursive for src/)
if Path::new("src").is_dir() {
collect_lux_files("src", pattern, &mut files_to_check);
}
// Also check current directory for top-level files
collect_lux_files_nonrecursive(".", pattern, &mut files_to_check);
// examples/ subdirectory
if Path::new("examples").is_dir() {
collect_lux_files("examples", pattern, &mut files_to_check);
}
// tests/ subdirectory
if Path::new("tests").is_dir() {
collect_lux_files("tests", pattern, &mut files_to_check);
}
}
if files_to_check.is_empty() {
println!("No .lux files found.");
println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/"));
return;
}
// Sort for consistent output
files_to_check.sort();
let mut passed = 0;
let mut failed = 0;
let mut total_warnings = 0;
let lint_config = LintConfig::default();
for file_path in &files_to_check {
let path = file_path.to_string_lossy().to_string();
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
failed += 1;
continue;
}
};
let mut loader = ModuleLoader::new();
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p,
Err(e) => {
eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
failed += 1;
continue;
}
};
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
eprintln!("{} {}", c(colors::RED, "\u{2717}"), path);
for error in errors {
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(&path)));
}
failed += 1;
} else {
// Type check passed — also run lints
let linter = Linter::new(lint_config.clone(), &source);
let lints = linter.lint(&program);
let warnings = lints.iter().filter(|l| l.level == LintLevel::Warn).count();
let errors = lints.iter().filter(|l| l.level == LintLevel::Deny).count();
if errors > 0 {
eprintln!("{} {}", c(colors::RED, "\u{2717}"), path);
for lint in &lints {
if lint.level == LintLevel::Deny {
let lint_code = c(
colors::DIM,
&format!("[{}/{}]", lint.id.category().category_name(), lint.id.name()),
);
if lint.span.start > 0 || lint.span.end > 0 {
let (line, col) = diagnostics::offset_to_line_col(&source, lint.span.start);
eprintln!(" {}{} {}:{}:{}: {}",
c(colors::RED, "error"), lint_code, path, line, col, lint.message);
} else {
eprintln!(" {}{} {}: {}",
c(colors::RED, "error"), lint_code, path, lint.message);
}
}
}
failed += 1;
} else if warnings > 0 {
println!("{} {} {}", c(colors::YELLOW, "\u{26a0}"), path,
c(colors::DIM, &format!("({} lint warnings)", warnings)));
total_warnings += warnings;
passed += 1;
} else {
println!("{} {}", c(colors::GREEN, "\u{2713}"), path);
passed += 1;
}
}
}
let elapsed = start.elapsed();
let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()));
println!();
if failed > 0 {
let mut summary = format!("{} {} passed, {} failed",
c(colors::RED, "\u{2717}"), passed, failed);
if total_warnings > 0 {
summary.push_str(&format!(", {}", c(colors::YELLOW, &format!("{} warnings", total_warnings))));
}
println!("{} {}", summary, time_str);
std::process::exit(1);
} else {
let mut summary = format!("{} {} passed",
c(colors::GREEN, "\u{2713}"), passed);
if total_warnings > 0 {
summary.push_str(&format!(", {}", c(colors::YELLOW, &format!("{} warnings", total_warnings))));
}
println!("{} {}", summary, time_str);
}
}
fn collect_lux_files_nonrecursive(dir: &str, pattern: Option<&str>, files: &mut Vec<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);
}
}
}
}
}
/// Find a C compiler. Priority: $CC env var, build-time embedded path, PATH search.
fn find_c_compiler() -> String {
// 1. Explicit env var
if let Ok(cc) = std::env::var("CC") {
if !cc.is_empty() {
return cc;
}
}
// 2. Path captured at build time (e.g. absolute nix store path)
let built_in = env!("LUX_CC_PATH");
if !built_in.is_empty() && std::path::Path::new(built_in).exists() {
return built_in.to_string();
}
// 3. Search PATH
for name in &["cc", "gcc", "clang"] {
if let Ok(output) = std::process::Command::new("which").arg(name).output() {
if output.status.success() {
if let Ok(p) = String::from_utf8(output.stdout) {
let p = p.trim();
if !p.is_empty() {
return p.to_string();
}
}
}
}
}
// 4. Last resort
"cc".to_string()
}
fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: bool) {
use codegen::c_backend::CBackend;
use modules::ModuleLoader;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
let start = Instant::now();
let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("{} Error reading file '{}': {}", c(colors::RED, "error:"), path, e);
std::process::exit(1);
}
};
// Parse with module loading
let mut loader = ModuleLoader::new();
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p,
Err(e) => {
eprintln!("{} {}", c(colors::RED, "error:"), e);
std::process::exit(1);
}
};
// Type check
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
for error in errors {
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
}
std::process::exit(1);
}
// Generate C code
let mut backend = CBackend::new();
let c_code = match backend.generate(&program, loader.module_cache()) {
Ok(code) => code,
Err(e) => {
eprintln!("{} C codegen: {}", c(colors::RED, "error:"), e);
eprintln!();
eprintln!("{}", c(colors::DIM, "Note: The C backend supports functions, closures, ADTs,"));
eprintln!("{}", c(colors::DIM, "pattern matching, lists, and Console.print."));
eprintln!("{}", c(colors::DIM, "Not yet supported: other effects, some advanced features."));
std::process::exit(1);
}
};
// Handle --emit-c: output C code instead of binary
if emit_c {
if let Some(out_path) = output_path {
if let Err(e) = std::fs::write(out_path, &c_code) {
eprintln!("{} writing '{}': {}", c(colors::RED, "error:"), out_path, e);
std::process::exit(1);
}
let elapsed = start.elapsed();
eprintln!("{} Emitted C to {} {}",
c(colors::GREEN, "\u{2713}"), bc(colors::CYAN, out_path),
c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())));
} else {
println!("{}", c_code);
}
return;
}
// Default: compile to native binary
let temp_c = std::env::temp_dir().join("lux_output.c");
// Determine output binary name
let output_bin = if let Some(out) = output_path {
Path::new(out).to_path_buf()
} else {
// Derive from source filename: foo.lux -> ./foo
let stem = file_path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("a.out");
Path::new(".").join(stem)
};
if let Err(e) = std::fs::write(&temp_c, &c_code) {
eprintln!("{} writing temp file: {}", c(colors::RED, "error:"), e);
std::process::exit(1);
}
// Find C compiler: $CC env var > embedded build-time path > PATH search
let cc = find_c_compiler();
let compile_result = Command::new(&cc)
.args(["-O2", "-o"])
.arg(&output_bin)
.arg(&temp_c)
.arg("-lm")
.output();
match compile_result {
Ok(output) => {
if !output.status.success() {
eprintln!("{} C compilation failed:", c(colors::RED, "error:"));
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
std::process::exit(1);
}
}
Err(e) => {
eprintln!("{} Failed to run C compiler '{}': {}", c(colors::RED, "error:"), cc, e);
eprintln!("{}", c(colors::DIM, "Make sure gcc or clang is installed, or set CC environment variable."));
std::process::exit(1);
}
}
let elapsed = start.elapsed();
if run_after {
// Run the compiled binary
let run_result = Command::new(&output_bin).status();
match run_result {
Ok(status) => {
std::process::exit(status.code().unwrap_or(1));
}
Err(e) => {
eprintln!("{} Failed to run compiled binary: {}", c(colors::RED, "error:"), e);
std::process::exit(1);
}
}
} else {
eprintln!("{} Compiled to {} {}",
c(colors::GREEN, "\u{2713}"),
bc(colors::CYAN, &output_bin.display().to_string()),
c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())));
}
}
fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) {
use codegen::js_backend::JsBackend;
use modules::ModuleLoader;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
let start = Instant::now();
let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("{} Error reading file '{}': {}", c(colors::RED, "error:"), path, e);
std::process::exit(1);
}
};
// Parse with module loading
let mut loader = ModuleLoader::new();
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p,
Err(e) => {
eprintln!("{} {}", c(colors::RED, "error:"), e);
std::process::exit(1);
}
};
// Type check
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
for error in errors {
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
}
std::process::exit(1);
}
// Generate JavaScript code
let mut backend = JsBackend::new();
let js_code = match backend.generate(&program) {
Ok(code) => code,
Err(e) => {
eprintln!("{} JS codegen: {}", c(colors::RED, "error:"), e);
std::process::exit(1);
}
};
// Determine output file path
let output_js = if let Some(out) = output_path {
Path::new(out).to_path_buf()
} else {
// Derive from source filename: foo.lux -> ./foo.js
let stem = file_path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
Path::new(".").join(format!("{}.js", stem))
};
// Write the JavaScript file
if let Err(e) = std::fs::write(&output_js, &js_code) {
eprintln!("{} writing '{}': {}", c(colors::RED, "error:"), output_js.display(), e);
std::process::exit(1);
}
let elapsed = start.elapsed();
if run_after {
// Run with Node.js
let node_result = Command::new("node")
.arg(&output_js)
.status();
match node_result {
Ok(status) => {
std::process::exit(status.code().unwrap_or(1));
}
Err(e) => {
eprintln!("{} Failed to run with Node.js: {}", c(colors::RED, "error:"), e);
eprintln!("{}", c(colors::DIM, "Make sure Node.js is installed."));
std::process::exit(1);
}
}
} else {
eprintln!("{} Compiled to {} {}",
c(colors::GREEN, "\u{2713}"),
bc(colors::CYAN, &output_js.display().to_string()),
c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())));
}
}
fn run_tests(args: &[String]) {
use std::path::Path;
use std::fs;
use std::time::Instant;
let start = Instant::now();
// Find test files
let pattern = args.first().map(|s| s.as_str());
// Look for test files in multiple locations
let mut test_files = Vec::new();
// Current directory
collect_test_files(".", pattern, &mut test_files);
// tests/ subdirectory
if Path::new("tests").is_dir() {
collect_test_files("tests", pattern, &mut test_files);
}
// examples/ subdirectory (for example tests)
if Path::new("examples").is_dir() {
collect_test_files("examples", pattern, &mut test_files);
}
// If a specific file is given, use that
if let Some(p) = pattern {
if Path::new(p).is_file() {
test_files.clear();
test_files.push(std::path::PathBuf::from(p));
}
}
if test_files.is_empty() {
println!("No test files found.");
println!("{}", c(colors::DIM, "Test files should be named test_*.lux or *_test.lux"));
println!("{}", c(colors::DIM, "Or contain functions named test_*"));
return;
}
println!("{}\n", bc("", "Running tests..."));
let mut total_passed = 0;
let mut total_failed = 0;
let mut all_failures = Vec::new();
for test_file in &test_files {
let path_str = test_file.to_string_lossy().to_string();
// Read and parse the file (with module loading)
let source = match fs::read_to_string(test_file) {
Ok(s) => s,
Err(e) => {
println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &e.to_string()));
total_failed += 1;
continue;
}
};
use modules::ModuleLoader;
let mut loader = ModuleLoader::new();
if let Some(parent) = test_file.parent() {
loader.add_search_path(parent.to_path_buf());
}
let program = match loader.load_source(&source, Some(test_file.as_path())) {
Ok(p) => p,
Err(e) => {
println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &format!("parse error: {}", e)));
total_failed += 1;
continue;
}
};
// Type check with module support
let mut checker = typechecker::TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, "type error"));
for err in errors {
eprintln!(" {}", err);
}
total_failed += 1;
continue;
}
// Get auto-generated migrations from typechecker
let auto_migrations = checker.get_auto_migrations().clone();
// Find test functions (functions starting with test_)
let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| {
if let ast::Declaration::Function(f) = d {
if f.name.name.starts_with("test_") {
return Some(f.name.name.clone());
}
}
None
}).collect();
if test_funcs.is_empty() {
// No test functions, run the whole file
let mut interp = Interpreter::new();
interp.register_auto_migrations(&auto_migrations);
interp.reset_test_results();
match interp.run_with_modules(&program, &loader) {
Ok(_) => {
let results = interp.get_test_results();
if results.failed == 0 && results.passed == 0 {
println!(" {} {} {}", c(colors::GREEN, "\u{2713}"), path_str, c(colors::DIM, "(no assertions)"));
total_passed += 1;
} else if results.failed == 0 {
println!(" {} {} {}", c(colors::GREEN, "\u{2713}"), path_str,
c(colors::DIM, &format!("({} assertions)", results.passed)));
total_passed += results.passed;
} else {
println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str,
c(colors::RED, &format!("{} passed, {} failed", results.passed, results.failed)));
total_passed += results.passed;
total_failed += results.failed;
for failure in &results.failures {
all_failures.push((path_str.clone(), "".to_string(), failure.clone()));
}
}
}
Err(e) => {
println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &format!("runtime error: {}", e)));
total_failed += 1;
}
}
} else {
// Run individual test functions
println!(" {}:", c(colors::DIM, &path_str));
for test_name in &test_funcs {
let mut interp = Interpreter::new();
interp.register_auto_migrations(&auto_migrations);
interp.reset_test_results();
// First run the file to define all functions and load imports
if let Err(e) = interp.run_with_modules(&program, &loader) {
println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string()));
total_failed += 1;
continue;
}
// Call the test function
let call_source = format!("let testResult = run {}() with {{}}", test_name);
let call_program = match Parser::parse_source(&call_source) {
Ok(p) => p,
Err(e) => {
println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string()));
total_failed += 1;
continue;
}
};
match interp.run(&call_program) {
Ok(_) => {
let results = interp.get_test_results();
if results.failed == 0 {
println!(" {} {}", c(colors::GREEN, "\u{2713}"), test_name);
total_passed += 1;
} else {
println!(" {} {}", c(colors::RED, "\u{2717}"), test_name);
total_failed += 1;
for failure in &results.failures {
all_failures.push((path_str.clone(), test_name.clone(), failure.clone()));
}
}
}
Err(e) => {
println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string()));
total_failed += 1;
}
}
}
}
}
// Print failure details
if !all_failures.is_empty() {
println!("\n{}\n", bc(colors::RED, "--- Failures ---"));
for (file, test, failure) in &all_failures {
if test.is_empty() {
println!("{}:", bc("", file));
} else {
println!("{} - {}:", bc("", file), bc("", test));
}
println!(" {}", failure.message);
if let Some(expected) = &failure.expected {
println!(" {} {}", c(colors::CYAN, "Expected:"), expected);
}
if let Some(actual) = &failure.actual {
println!(" {} {}", c(colors::RED, "Actual: "), actual);
}
println!();
}
}
let elapsed = start.elapsed();
let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()));
if total_failed > 0 {
println!("{} {} passed, {} failed {}",
c(colors::RED, "\u{2717}"), total_passed, total_failed, time_str);
std::process::exit(1);
} else {
println!("{} {} passed {}",
c(colors::GREEN, "\u{2713}"), total_passed, time_str);
}
}
fn collect_test_files(dir: &str, pattern: Option<&str>, files: &mut Vec<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 watch_and_rerun(path: &str, compile_args: &[String]) {
use std::time::{Duration, SystemTime};
use std::path::Path;
let file_path = Path::new(path);
if !file_path.exists() {
eprintln!("File not found: {}", path);
std::process::exit(1);
}
println!();
println!("Watching {} for changes (Ctrl+C to stop)...", path);
let mut last_modified = std::fs::metadata(file_path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
loop {
std::thread::sleep(Duration::from_millis(500));
let modified = match std::fs::metadata(file_path).and_then(|m| m.modified()) {
Ok(m) => m,
Err(_) => continue,
};
if modified > last_modified {
last_modified = modified;
// Clear screen
print!("\x1B[2J\x1B[H");
println!("=== Compiling {} ===", path);
println!();
let result = std::process::Command::new(std::env::current_exe().unwrap())
.args(compile_args)
.status();
match result {
Ok(status) if status.success() => {
println!();
println!("=== Success ===");
}
Ok(_) => {
println!();
println!("=== Failed ===");
}
Err(e) => {
eprintln!("Error running compiler: {}", e);
}
}
println!();
println!("Watching for changes...");
}
}
}
fn serve_static_files(dir: &str, port: u16) {
use std::io::{Write, BufRead, BufReader};
use std::net::TcpListener;
use std::path::Path;
let root = Path::new(dir).canonicalize().unwrap_or_else(|_| {
eprintln!("{} Directory not found: {}", c(colors::RED, "error:"), dir);
std::process::exit(1);
});
let listener = match TcpListener::bind(format!("0.0.0.0:{}", port)) {
Ok(l) => l,
Err(e) => {
if e.kind() == std::io::ErrorKind::AddrInUse {
eprintln!("{} Port {} is already in use", c(colors::RED, "error:"), port);
// Try to find an available port
for try_port in (port + 1)..=(port + 3) {
if TcpListener::bind(format!("0.0.0.0:{}", try_port)).is_ok() {
eprintln!("{}", c(colors::CYAN, &format!(" Try: lux serve --port {}", try_port)));
break;
}
}
} else {
eprintln!("{} Could not bind to port {}: {}", c(colors::RED, "error:"), port, e);
}
std::process::exit(1);
}
};
println!("{}", bc("", "Lux static file server"));
println!(" {} {}", c(colors::DIM, "Serving:"), root.display());
println!(" {} {}", c(colors::DIM, "URL:"), bc(colors::CYAN, &format!("http://localhost:{}", port)));
println!();
println!("{}", c(colors::DIM, "Press Ctrl+C to stop"));
println!();
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
// Read the request line
let mut reader = BufReader::new(&stream);
let mut request_line = String::new();
if reader.read_line(&mut request_line).is_err() {
continue;
}
// Parse the request
let parts: Vec<&str> = request_line.split_whitespace().collect();
if parts.len() < 2 {
continue;
}
let method = parts[0];
let path = parts[1];
// Log the request (status is logged after response is determined)
let log_method = bc("", method);
let log_path = path.to_string();
// Only handle GET requests
if method != "GET" {
let response = "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 18\r\n\r\nMethod Not Allowed";
let _ = stream.write_all(response.as_bytes());
continue;
}
// Resolve the file path
let mut file_path = root.clone();
let request_path = if path == "/" { "/index.html" } else { path };
// Prevent directory traversal
for component in request_path.trim_start_matches('/').split('/') {
if component == ".." {
let response = "HTTP/1.1 403 Forbidden\r\nContent-Length: 9\r\n\r\nForbidden";
let _ = stream.write_all(response.as_bytes());
continue;
}
file_path.push(component);
}
// Try to serve the file
let (status, content_type, body) = if file_path.is_file() {
match std::fs::read(&file_path) {
Ok(content) => {
let mime = get_mime_type(&file_path);
("200 OK", mime, content)
}
Err(_) => {
("500 Internal Server Error", "text/plain", b"Error reading file".to_vec())
}
}
} else if file_path.is_dir() {
// Try index.html
let index_path = file_path.join("index.html");
if index_path.is_file() {
match std::fs::read(&index_path) {
Ok(content) => ("200 OK", "text/html", content),
Err(_) => ("500 Internal Server Error", "text/plain", b"Error reading file".to_vec()),
}
} else {
("404 Not Found", "text/plain", b"Not Found".to_vec())
}
} else {
// Try with .html extension
let html_path = file_path.with_extension("html");
if html_path.is_file() {
match std::fs::read(&html_path) {
Ok(content) => ("200 OK", "text/html", content),
Err(_) => ("500 Internal Server Error", "text/plain", b"Error reading file".to_vec()),
}
} else {
("404 Not Found", "text/plain", b"Not Found".to_vec())
}
};
// Log the request with colored status
let status_color = if status.starts_with("200") {
colors::GREEN
} else if status.starts_with("404") {
colors::YELLOW
} else {
colors::RED
};
let status_code = status.split_whitespace().next().unwrap_or(status);
println!(" {} {} {}", log_method, c(status_color, status_code), log_path);
// Send response
let response = format!(
"HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
status, content_type, body.len()
);
let _ = stream.write_all(response.as_bytes());
let _ = stream.write_all(&body);
}
}
fn get_mime_type(path: &std::path::Path) -> &'static str {
match path.extension().and_then(|e| e.to_str()) {
Some("html") | Some("htm") => "text/html",
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("json") => "application/json",
Some("png") => "image/png",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("gif") => "image/gif",
Some("svg") => "image/svg+xml",
Some("ico") => "image/x-icon",
Some("woff") => "font/woff",
Some("woff2") => "font/woff2",
Some("ttf") => "font/ttf",
Some("txt") => "text/plain",
Some("xml") => "application/xml",
Some("pdf") => "application/pdf",
_ => "application/octet-stream",
}
}
fn handle_pkg_command(args: &[String]) {
use package::{PackageManager, DependencySource};
use std::path::PathBuf;
if args.is_empty() {
print_pkg_help();
return;
}
// Help doesn't require being in a project
if matches!(args[0].as_str(), "help" | "--help" | "-h") {
print_pkg_help();
return;
}
// Init doesn't require being in a project (it creates one)
if args[0] == "init" {
init_pkg_here(args.get(1).map(|s| s.as_str()));
return;
}
let project_root = match PackageManager::find_project_root() {
Some(root) => root,
None => {
eprintln!("Error: Not in a Lux project (no lux.toml found)");
eprintln!("Run 'lux pkg init' to initialize a project here, or 'lux init <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: {}", c(colors::GREEN, "\u{2713}"), bc(colors::GREEN, project_name));
println!();
println!("{}", bc("", "Project structure:"));
println!(" {}", bc(colors::CYAN, &format!("{}/", project_name)));
println!(" {} {}", c(colors::DIM, "\u{251c}\u{2500}\u{2500}"), "lux.toml");
println!(" {} {}", c(colors::DIM, "\u{251c}\u{2500}\u{2500}"), bc(colors::CYAN, "src/"));
println!(" {} {} {}", c(colors::DIM, "\u{2502}"), c(colors::DIM, " \u{2514}\u{2500}\u{2500}"), "main.lux");
println!(" {} {}", c(colors::DIM, "\u{2514}\u{2500}\u{2500}"), bc(colors::CYAN, "tests/"));
println!(" {} {}", c(colors::DIM, "\u{2514}\u{2500}\u{2500}"), "test_example.lux");
println!();
println!("To get started:");
println!(" {} {}", c(colors::DIM, "$"), bc(colors::CYAN, &format!("cd {}", project_name)));
println!(" {} {}", c(colors::DIM, "$"), bc(colors::CYAN, "lux src/main.lux"));
}
/// Generate API documentation for Lux source files
fn generate_docs(args: &[String]) {
use std::path::Path;
use std::collections::HashMap;
let output_json = args.iter().any(|a| a == "--json");
let output_dir = args.iter()
.position(|a| a == "-o")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
.unwrap_or("docs");
let input_file = args.iter().find(|a| !a.starts_with('-') && *a != output_dir);
// Collect files to document
let mut files_to_doc = Vec::new();
if let Some(path) = input_file {
if Path::new(path).is_file() {
files_to_doc.push(path.to_string());
} else {
eprintln!("File not found: {}", path);
std::process::exit(1);
}
} else {
// Auto-discover files
if Path::new("src").is_dir() {
collect_lux_files_for_docs("src", &mut files_to_doc);
}
if Path::new("stdlib").is_dir() {
collect_lux_files_for_docs("stdlib", &mut files_to_doc);
}
}
if files_to_doc.is_empty() {
eprintln!("No .lux files found to document");
std::process::exit(1);
}
// Create output directory
if !output_json {
if let Err(e) = std::fs::create_dir_all(output_dir) {
eprintln!("Failed to create output directory: {}", e);
std::process::exit(1);
}
}
let mut all_docs: HashMap<String, ModuleDoc> = HashMap::new();
let mut error_count = 0;
for file_path in &files_to_doc {
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("{}: ERROR - {}", file_path, e);
error_count += 1;
continue;
}
};
match extract_module_doc(&source, file_path) {
Ok(doc) => {
let module_name = Path::new(file_path)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
all_docs.insert(module_name, doc);
}
Err(e) => {
eprintln!("{}: PARSE ERROR - {}", file_path, e);
error_count += 1;
}
}
}
if output_json {
// Output as JSON
println!("{}", docs_to_json(&all_docs));
} else {
// Generate HTML files
let index_html = generate_index_html(&all_docs);
let index_path = format!("{}/index.html", output_dir);
if let Err(e) = std::fs::write(&index_path, &index_html) {
eprintln!("Failed to write index.html: {}", e);
error_count += 1;
}
for (module_name, doc) in &all_docs {
let html = generate_module_html(module_name, doc);
let path = format!("{}/{}.html", output_dir, module_name);
if let Err(e) = std::fs::write(&path, &html) {
eprintln!("Failed to write {}: {}", path, e);
error_count += 1;
}
}
// Generate CSS
let css_path = format!("{}/style.css", output_dir);
if let Err(e) = std::fs::write(&css_path, DOC_CSS) {
eprintln!("Failed to write style.css: {}", e);
error_count += 1;
}
println!("Generated documentation in {}/", output_dir);
println!(" {} modules documented", all_docs.len());
if error_count > 0 {
println!(" {} errors", error_count);
}
}
}
fn collect_lux_files_for_docs(dir: &str, files: &mut Vec<String>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && path.extension().map(|e| e == "lux").unwrap_or(false) {
files.push(path.to_string_lossy().to_string());
} else if path.is_dir() {
collect_lux_files_for_docs(&path.to_string_lossy(), files);
}
}
}
}
#[derive(Debug, Clone)]
struct ModuleDoc {
description: Option<String>,
functions: Vec<FunctionDoc>,
types: Vec<TypeDoc>,
effects: Vec<EffectDoc>,
}
#[derive(Debug, Clone)]
struct FunctionDoc {
name: String,
signature: String,
description: Option<String>,
is_public: bool,
properties: Vec<String>,
}
#[derive(Debug, Clone)]
struct TypeDoc {
name: String,
definition: String,
description: Option<String>,
is_public: bool,
}
#[derive(Debug, Clone)]
struct EffectDoc {
name: String,
operations: Vec<String>,
description: Option<String>,
}
fn extract_module_doc(source: &str, path: &str) -> Result<ModuleDoc, String> {
use modules::ModuleLoader;
use std::path::Path;
let mut loader = ModuleLoader::new();
let file_path = Path::new(path);
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
let program = loader.load_source(source, Some(file_path))
.map_err(|e| format!("{}", e))?;
let mut module_desc: Option<String> = None;
let mut functions = Vec::new();
let mut types = Vec::new();
let mut effects = Vec::new();
let mut pending_doc: Option<String> = None;
// Extract module-level comment (first comment before any declarations)
let lines: Vec<&str> = source.lines().collect();
let mut module_comment = Vec::new();
for line in &lines {
let trimmed = line.trim();
if trimmed.starts_with("//") {
module_comment.push(trimmed.trim_start_matches('/').trim());
} else if !trimmed.is_empty() {
break;
}
}
if !module_comment.is_empty() {
module_desc = Some(module_comment.join("\n"));
}
for decl in &program.declarations {
match decl {
ast::Declaration::Function(f) => {
// Build signature
let params: Vec<String> = f.params.iter()
.map(|p| format!("{}: {}", p.name.name, format_type(&p.typ)))
.collect();
let effects_str = if f.effects.is_empty() {
String::new()
} else {
format!(" with {{{}}}", f.effects.iter().map(|e| e.name.clone()).collect::<Vec<_>>().join(", "))
};
let props: Vec<String> = f.properties.iter()
.map(|p| format!("{:?}", p).to_lowercase())
.collect();
let props_str = if props.is_empty() {
String::new()
} else {
format!(" is {}", props.join(", "))
};
let signature = format!(
"fn {}({}): {}{}{}",
f.name.name,
params.join(", "),
format_type(&f.return_type),
props_str,
effects_str
);
// Extract doc comment
let doc = extract_doc_comment(source, f.span.start);
functions.push(FunctionDoc {
name: f.name.name.clone(),
signature,
description: doc,
is_public: matches!(f.visibility, ast::Visibility::Public),
properties: props,
});
}
ast::Declaration::Type(t) => {
let doc = extract_doc_comment(source, t.span.start);
types.push(TypeDoc {
name: t.name.name.clone(),
definition: format_type_def(t),
description: doc,
is_public: matches!(t.visibility, ast::Visibility::Public),
});
}
ast::Declaration::Effect(e) => {
let doc = extract_doc_comment(source, e.span.start);
let ops: Vec<String> = e.operations.iter()
.map(|op| {
let params: Vec<String> = op.params.iter()
.map(|p| format!("{}: {}", p.name.name, format_type(&p.typ)))
.collect();
format!("{}({}): {}", op.name.name, params.join(", "), format_type(&op.return_type))
})
.collect();
effects.push(EffectDoc {
name: e.name.name.clone(),
operations: ops,
description: doc,
});
}
_ => {}
}
}
Ok(ModuleDoc {
description: module_desc,
functions,
types,
effects,
})
}
fn extract_doc_comment(source: &str, pos: usize) -> Option<String> {
// Look backwards from the declaration for doc comments
let prefix = &source[..pos];
let lines: Vec<&str> = prefix.lines().collect();
let mut doc_lines = Vec::new();
for line in lines.iter().rev() {
let trimmed = line.trim();
if trimmed.starts_with("///") {
doc_lines.push(trimmed.trim_start_matches('/').trim());
} else if trimmed.starts_with("//") {
// Regular comment, skip
continue;
} else if trimmed.is_empty() {
if !doc_lines.is_empty() {
break;
}
} else {
break;
}
}
if doc_lines.is_empty() {
None
} else {
doc_lines.reverse();
Some(doc_lines.join("\n"))
}
}
fn format_type(t: &ast::TypeExpr) -> String {
match t {
ast::TypeExpr::Named(ident) => ident.name.clone(),
ast::TypeExpr::App(base, args) => {
let args_str: Vec<String> = args.iter().map(format_type).collect();
format!("{}<{}>", format_type(base), args_str.join(", "))
}
ast::TypeExpr::Function { params, return_type, .. } => {
let params_str: Vec<String> = params.iter().map(format_type).collect();
format!("fn({}): {}", params_str.join(", "), format_type(return_type))
}
ast::TypeExpr::Tuple(types) => {
let types_str: Vec<String> = types.iter().map(format_type).collect();
format!("({})", types_str.join(", "))
}
ast::TypeExpr::Record(fields) => {
let fields_str: Vec<String> = fields.iter()
.map(|f| format!("{}: {}", f.name.name, format_type(&f.typ)))
.collect();
format!("{{ {} }}", fields_str.join(", "))
}
ast::TypeExpr::Unit => "Unit".to_string(),
ast::TypeExpr::Versioned { base, .. } => format_type(base),
}
}
fn format_type_def(t: &ast::TypeDecl) -> String {
match &t.definition {
ast::TypeDef::Alias(typ) => format!("type {} = {}", t.name.name, format_type(typ)),
ast::TypeDef::Enum(variants) => {
let variants_str: Vec<String> = variants.iter()
.map(|v| {
match &v.fields {
ast::VariantFields::Unit => v.name.name.clone(),
ast::VariantFields::Tuple(types) => {
let types_str: Vec<String> = types.iter().map(format_type).collect();
format!("{}({})", v.name.name, types_str.join(", "))
}
ast::VariantFields::Record(fields) => {
let fields_str: Vec<String> = fields.iter()
.map(|f| format!("{}: {}", f.name.name, format_type(&f.typ)))
.collect();
format!("{}{{ {} }}", v.name.name, fields_str.join(", "))
}
}
})
.collect();
format!("type {} = {}", t.name.name, variants_str.join(" | "))
}
ast::TypeDef::Record(fields) => {
let fields_str: Vec<String> = fields.iter()
.map(|f| format!("{}: {}", f.name.name, format_type(&f.typ)))
.collect();
format!("type {} = {{ {} }}", t.name.name, fields_str.join(", "))
}
}
}
fn docs_to_json(docs: &std::collections::HashMap<String, ModuleDoc>) -> String {
let mut json = String::from("{\n");
let mut first_module = true;
for (name, doc) in docs {
if !first_module {
json.push_str(",\n");
}
first_module = false;
json.push_str(&format!(" \"{}\": {{\n", escape_json(name)));
if let Some(desc) = &doc.description {
json.push_str(&format!(" \"description\": \"{}\",\n", escape_json(desc)));
}
// Functions
json.push_str(" \"functions\": [\n");
for (i, f) in doc.functions.iter().enumerate() {
json.push_str(&format!(
" {{\"name\": \"{}\", \"signature\": \"{}\", \"public\": {}, \"description\": {}}}",
escape_json(&f.name),
escape_json(&f.signature),
f.is_public,
f.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string())
));
if i < doc.functions.len() - 1 {
json.push(',');
}
json.push('\n');
}
json.push_str(" ],\n");
// Types
json.push_str(" \"types\": [\n");
for (i, t) in doc.types.iter().enumerate() {
json.push_str(&format!(
" {{\"name\": \"{}\", \"definition\": \"{}\", \"public\": {}, \"description\": {}}}",
escape_json(&t.name),
escape_json(&t.definition),
t.is_public,
t.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string())
));
if i < doc.types.len() - 1 {
json.push(',');
}
json.push('\n');
}
json.push_str(" ],\n");
// Effects
json.push_str(" \"effects\": [\n");
for (i, e) in doc.effects.iter().enumerate() {
let ops_json: Vec<String> = e.operations.iter()
.map(|o| format!("\"{}\"", escape_json(o)))
.collect();
json.push_str(&format!(
" {{\"name\": \"{}\", \"operations\": [{}], \"description\": {}}}",
escape_json(&e.name),
ops_json.join(", "),
e.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string())
));
if i < doc.effects.len() - 1 {
json.push(',');
}
json.push('\n');
}
json.push_str(" ]\n");
json.push_str(" }");
}
json.push_str("\n}");
json
}
fn escape_json(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn generate_index_html(docs: &std::collections::HashMap<String, ModuleDoc>) -> String {
let mut html = String::from(r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lux API Documentation</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Lux API Documentation</h1>
</header>
<main>
<h2>Modules</h2>
<ul class="module-list">
"#);
let mut modules: Vec<_> = docs.keys().collect();
modules.sort();
for name in modules {
let doc = &docs[name];
let desc = doc.description.as_ref()
.map(|d| d.lines().next().unwrap_or(""))
.unwrap_or("");
html.push_str(&format!(
" <li><a href=\"{}.html\">{}</a> - {}</li>\n",
name, name, html_escape(desc)
));
}
html.push_str(r#" </ul>
</main>
</body>
</html>"#);
html
}
fn generate_module_html(name: &str, doc: &ModuleDoc) -> String {
let mut html = format!(r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{} - Lux API</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<a href="index.html">Back to Index</a>
<h1>{}</h1>
</header>
<main>
"#, name, name);
if let Some(desc) = &doc.description {
html.push_str(&format!(" <div class=\"module-description\">{}</div>\n", html_escape(desc)));
}
// Types
if !doc.types.is_empty() {
html.push_str(" <section>\n <h2>Types</h2>\n");
for t in &doc.types {
let visibility = if t.is_public { "pub " } else { "" };
html.push_str(&format!(
" <div class=\"item\">\n <code class=\"signature\">{}{}</code>\n",
visibility, html_escape(&t.definition)
));
if let Some(desc) = &t.description {
html.push_str(&format!(" <p class=\"description\">{}</p>\n", html_escape(desc)));
}
html.push_str(" </div>\n");
}
html.push_str(" </section>\n");
}
// Effects
if !doc.effects.is_empty() {
html.push_str(" <section>\n <h2>Effects</h2>\n");
for e in &doc.effects {
html.push_str(&format!(
" <div class=\"item\">\n <h3>effect {}</h3>\n",
html_escape(&e.name)
));
if let Some(desc) = &e.description {
html.push_str(&format!(" <p class=\"description\">{}</p>\n", html_escape(desc)));
}
html.push_str(" <ul class=\"operations\">\n");
for op in &e.operations {
html.push_str(&format!(" <li><code>{}</code></li>\n", html_escape(op)));
}
html.push_str(" </ul>\n </div>\n");
}
html.push_str(" </section>\n");
}
// Functions
if !doc.functions.is_empty() {
html.push_str(" <section>\n <h2>Functions</h2>\n");
for f in &doc.functions {
let visibility = if f.is_public { "pub " } else { "" };
html.push_str(&format!(
" <div class=\"item\" id=\"{}\">\n <code class=\"signature\">{}{}</code>\n",
f.name, visibility, html_escape(&f.signature)
));
if let Some(desc) = &f.description {
html.push_str(&format!(" <p class=\"description\">{}</p>\n", html_escape(desc)));
}
html.push_str(" </div>\n");
}
html.push_str(" </section>\n");
}
html.push_str(r#" </main>
</body>
</html>"#);
html
}
fn html_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
}
const DOC_CSS: &str = r#"
:root {
--bg-color: #1a1a2e;
--text-color: #e0e0e0;
--link-color: #64b5f6;
--code-bg: #16213e;
--header-bg: #0f3460;
--accent: #e94560;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
line-height: 1.6;
}
header {
background-color: var(--header-bg);
padding: 1rem 2rem;
border-bottom: 2px solid var(--accent);
}
header h1 {
margin: 0;
color: white;
}
header a {
color: var(--link-color);
text-decoration: none;
font-size: 0.9rem;
}
main {
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
h2 {
color: var(--accent);
border-bottom: 1px solid var(--accent);
padding-bottom: 0.5rem;
}
h3 {
color: var(--link-color);
}
.module-list {
list-style: none;
padding: 0;
}
.module-list li {
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.module-list a {
color: var(--link-color);
text-decoration: none;
font-weight: bold;
}
.item {
background-color: var(--code-bg);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.signature {
display: block;
background-color: rgba(0,0,0,0.3);
padding: 0.5rem 1rem;
border-radius: 4px;
font-family: 'Fira Code', 'Monaco', monospace;
overflow-x: auto;
}
.description {
margin-top: 0.5rem;
color: #aaa;
}
.operations {
list-style: none;
padding-left: 1rem;
}
.operations li {
padding: 0.25rem 0;
}
.module-description {
background-color: var(--code-bg);
padding: 1rem;
border-radius: 8px;
margin-bottom: 2rem;
border-left: 3px solid var(--accent);
}
"#;
fn run_file(path: &str) {
use modules::ModuleLoader;
use std::path::Path;
let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file '{}': {}", path, e);
std::process::exit(1);
}
};
// Set up module loader with the file's directory as a search path
let mut loader = ModuleLoader::new();
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
// Load and parse the program (including any imports)
let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p,
Err(e) => {
eprintln!("Module error: {}", e);
std::process::exit(1);
}
};
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
for error in errors {
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
}
std::process::exit(1);
}
let mut interp = Interpreter::new();
match interp.run_with_modules(&program, &loader) {
Ok(value) => {
if !matches!(value, interpreter::Value::Unit) {
println!("{}", value);
}
}
Err(e) => {
let diagnostic = e.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
std::process::exit(1);
}
}
}
/// REPL helper for tab completion and syntax highlighting
struct LuxHelper {
keywords: HashSet<String>,
commands: Vec<String>,
user_defined: HashSet<String>,
last_loaded_file: Option<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",
":reload", ":r", ":trace", ":traces", ":info", ":i", ":env", ":doc", ":d",
":browse", ":b", ":ast",
]
.into_iter()
.map(String::from)
.collect();
Self {
keywords,
commands,
user_defined: HashSet::new(),
last_loaded_file: None,
}
}
fn add_definition(&mut self, name: &str) {
self.user_defined.insert(name.to_string());
}
}
impl Completer for LuxHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<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 {
helper.last_loaded_file = Some(path.to_string());
load_file(path, interp, checker, helper);
} else {
println!("Usage: :load <filename>");
}
}
":reload" | ":r" => {
if let Some(ref path) = helper.last_loaded_file.clone() {
println!("Reloading {}...", path);
// Clear environment first
*interp = Interpreter::new();
*checker = TypeChecker::new();
helper.user_defined.clear();
load_file(path, interp, checker, helper);
} else {
println!("No file to reload. Use :load <file> first.");
}
}
":ast" => {
if let Some(expr_str) = arg {
show_ast(expr_str);
} else {
println!("Usage: :ast <expression>");
}
}
":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 show_ast(expr_str: &str) {
// Wrap expression in a let to parse it
let wrapped = format!("let _expr_ = {}", expr_str);
match Parser::parse_source(&wrapped) {
Ok(program) => {
// Pretty print the AST
for decl in &program.declarations {
println!("{:#?}", decl);
}
}
Err(e) => {
println!("Parse error: {}", e);
}
}
}
fn load_file(path: &str, interp: &mut Interpreter, checker: &mut TypeChecker, helper: &mut LuxHelper) {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
println!("Error reading file '{}': {}", path, e);
return;
}
};
let program = match Parser::parse_source(&source) {
Ok(p) => p,
Err(e) => {
println!("Parse error: {}", e);
return;
}
};
if let Err(errors) = checker.check_program(&program) {
for error in errors {
println!("Type error: {}", error);
}
return;
}
// Add definitions for completion
for decl in &program.declarations {
if let Some(name) = extract_definition_name(&format!("{:?}", decl)) {
helper.add_definition(&name);
}
}
match interp.run(&program) {
Ok(_) => println!("Loaded '{}'", path),
Err(e) => println!("Runtime error: {}", e),
}
}
fn eval_input(input: &str, interp: &mut Interpreter, checker: &mut TypeChecker) {
// Try to parse as a program (declarations)
match Parser::parse_source(input) {
Ok(program) => {
// Type check
if let Err(errors) = checker.check_program(&program) {
for error in errors {
println!("Type error: {}", error);
}
return;
}
// Execute
match interp.run(&program) {
Ok(value) => {
if !matches!(value, interpreter::Value::Unit) {
println!("{}", value);
}
}
Err(e) => {
println!("Runtime error: {}", e);
}
}
}
Err(parse_err) => {
// Try wrapping as an expression
let wrapped = format!("let _result_ = {}", input.trim());
match Parser::parse_source(&wrapped) {
Ok(program) => {
if let Err(errors) = checker.check_program(&program) {
for error in errors {
println!("Type error: {}", error);
}
return;
}
match interp.run(&program) {
Ok(value) => {
println!("{}", value);
}
Err(e) => {
println!("Runtime error: {}", e);
}
}
}
Err(_) => {
// Use original error
println!("Parse error: {}", parse_err);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn eval(source: &str) -> Result<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");
}
}
// ============ Multi-line Arguments Tests ============
#[test]
fn test_multiline_function_args() {
let source = r#"
fn add(a: Int, b: Int): Int = a + b
let result = add(
1,
2
)
"#;
assert_eq!(eval(source).unwrap(), "3");
}
#[test]
fn test_multiline_function_args_with_lambda() {
let source = r#"
let xs = List.map(
[1, 2, 3],
fn(x) => x * 2
)
"#;
assert_eq!(eval(source).unwrap(), "[2, 4, 6]");
}
// ============ Tuple Index Tests ============
#[test]
fn test_tuple_index_access() {
let source = r#"
let pair = (42, "hello")
let first = pair.0
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_tuple_index_access_second() {
let source = r#"
let pair = (42, "hello")
let second = pair.1
"#;
assert_eq!(eval(source).unwrap(), "\"hello\"");
}
#[test]
fn test_tuple_index_triple() {
let source = r#"
let triple = (1, 2, 3)
let sum = triple.0 + triple.1 + triple.2
"#;
assert_eq!(eval(source).unwrap(), "6");
}
#[test]
fn test_tuple_index_in_function() {
let source = r#"
fn first(pair: (Int, String)): Int = pair.0
fn second(pair: (Int, String)): String = pair.1
let p = (42, "hello")
let result = first(p)
"#;
assert_eq!(eval(source).unwrap(), "42");
}
// Exhaustiveness checking tests
mod exhaustiveness_tests {
use super::*;
#[test]
fn test_exhaustive_bool_match() {
let source = r#"
fn check(b: Bool): Int = match b {
true => 1,
false => 0
}
let result = check(true)
"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_non_exhaustive_bool_match() {
let source = r#"
fn check(b: Bool): Int = match b {
true => 1
}
let result = check(true)
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Non-exhaustive"));
}
#[test]
fn test_exhaustive_option_match() {
let source = r#"
fn unwrap_or(opt: Option<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();
}
}
// === Map type tests ===
#[test]
fn test_map_new_and_size() {
let source = r#"
let m = Map.new()
let result = Map.size(m)
"#;
assert_eq!(eval(source).unwrap(), "0");
}
#[test]
fn test_map_set_and_get() {
let source = r#"
let m = Map.new()
let m2 = Map.set(m, "name", "Alice")
let result = Map.get(m2, "name")
"#;
assert_eq!(eval(source).unwrap(), "Some(\"Alice\")");
}
#[test]
fn test_map_get_missing() {
let source = r#"
let m = Map.new()
let result = Map.get(m, "missing")
"#;
assert_eq!(eval(source).unwrap(), "None");
}
#[test]
fn test_map_contains() {
let source = r#"
let m = Map.set(Map.new(), "x", 1)
let result = (Map.contains(m, "x"), Map.contains(m, "y"))
"#;
assert_eq!(eval(source).unwrap(), "(true, false)");
}
#[test]
fn test_map_remove() {
let source = r#"
let m = Map.set(Map.set(Map.new(), "a", 1), "b", 2)
let m2 = Map.remove(m, "a")
let result = (Map.size(m2), Map.contains(m2, "a"), Map.contains(m2, "b"))
"#;
assert_eq!(eval(source).unwrap(), "(1, false, true)");
}
#[test]
fn test_map_keys_and_values() {
let source = r#"
let m = Map.set(Map.set(Map.new(), "b", 2), "a", 1)
let result = Map.keys(m)
"#;
assert_eq!(eval(source).unwrap(), "[\"a\", \"b\"]");
}
#[test]
fn test_map_from_list() {
let source = r#"
let m = Map.fromList([("x", 10), ("y", 20)])
let result = (Map.get(m, "x"), Map.size(m))
"#;
assert_eq!(eval(source).unwrap(), "(Some(10), 2)");
}
#[test]
fn test_map_to_list() {
let source = r#"
let m = Map.set(Map.set(Map.new(), "b", 2), "a", 1)
let result = Map.toList(m)
"#;
assert_eq!(eval(source).unwrap(), "[(\"a\", 1), (\"b\", 2)]");
}
#[test]
fn test_map_merge() {
let source = r#"
let m1 = Map.fromList([("a", 1), ("b", 2)])
let m2 = Map.fromList([("b", 3), ("c", 4)])
let merged = Map.merge(m1, m2)
let result = (Map.get(merged, "a"), Map.get(merged, "b"), Map.get(merged, "c"))
"#;
assert_eq!(eval(source).unwrap(), "(Some(1), Some(3), Some(4))");
}
#[test]
fn test_map_immutability() {
let source = r#"
let m1 = Map.fromList([("a", 1)])
let m2 = Map.set(m1, "b", 2)
let result = (Map.size(m1), Map.size(m2))
"#;
assert_eq!(eval(source).unwrap(), "(1, 2)");
}
#[test]
fn test_map_is_empty() {
let source = r#"
let m1 = Map.new()
let m2 = Map.set(m1, "x", 1)
let result = (Map.isEmpty(m1), Map.isEmpty(m2))
"#;
assert_eq!(eval(source).unwrap(), "(true, false)");
}
#[test]
fn test_map_type_annotation() {
let source = r#"
fn lookup(m: Map<String, Int>, key: String): Option<Int> =
Map.get(m, key)
let m = Map.fromList([("age", 30)])
let result = lookup(m, "age")
"#;
assert_eq!(eval(source).unwrap(), "Some(30)");
}
#[test]
fn test_file_copy() {
use std::io::Write;
// Create a temp file, copy it, verify contents
let dir = std::env::temp_dir().join("lux_test_file_copy");
let _ = std::fs::create_dir_all(&dir);
let src = dir.join("src.txt");
let dst = dir.join("dst.txt");
std::fs::File::create(&src).unwrap().write_all(b"hello copy").unwrap();
let _ = std::fs::remove_file(&dst);
let source = format!(r#"
fn main(): Unit with {{File}} =
File.copy("{}", "{}")
let _ = run main() with {{}}
let result = "done"
"#, src.display(), dst.display());
let result = eval(&source);
assert!(result.is_ok(), "File.copy failed: {:?}", result);
let contents = std::fs::read_to_string(&dst).unwrap();
assert_eq!(contents, "hello copy");
// Cleanup
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_effectful_callback_propagation() {
// WISH-7: effectful callbacks in List.forEach should propagate effects
// This should type-check successfully because Console effect is inferred
let source = r#"
fn printAll(items: List<String>): Unit =
List.forEach(items, fn(x: String): Unit => Console.print(x))
let result = "ok"
"#;
let result = eval(source);
assert!(result.is_ok(), "Effectful callback should type-check: {:?}", result);
}
#[test]
fn test_effectful_callback_in_map() {
// Effectful callback in List.map should propagate effects
let source = r#"
fn readAll(paths: List<String>): List<String> =
List.map(paths, fn(p: String): String => File.read(p))
let result = "ok"
"#;
let result = eval(source);
assert!(result.is_ok(), "Effectful callback in map should type-check: {:?}", result);
}
}