REPL improvements: - Syntax highlighting for keywords (magenta), types (blue), strings (green), numbers (yellow), comments (gray) - :doc command to show documentation for functions - :browse command to list module exports - Added docs for List, String, Option, Result, Console, Random, File, Http, Time, Sql, Postgres, Test modules Example usage: lux> :doc List.map lux> :browse String lux> :doc fn Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
4114 lines
138 KiB
Rust
4114 lines
138 KiB
Rust
//! Lux - A functional programming language with first-class effects
|
|
|
|
mod analysis;
|
|
mod ast;
|
|
mod codegen;
|
|
mod debugger;
|
|
mod diagnostics;
|
|
mod exhaustiveness;
|
|
mod formatter;
|
|
mod interpreter;
|
|
mod lexer;
|
|
mod lsp;
|
|
mod modules;
|
|
mod package;
|
|
mod parser;
|
|
mod registry;
|
|
mod schema;
|
|
mod typechecker;
|
|
mod types;
|
|
|
|
use diagnostics::render;
|
|
use interpreter::Interpreter;
|
|
use parser::Parser;
|
|
use rustyline::completion::{Completer, Pair};
|
|
use rustyline::error::ReadlineError;
|
|
use rustyline::highlight::Highlighter;
|
|
use rustyline::hint::Hinter;
|
|
use rustyline::history::DefaultHistory;
|
|
use rustyline::validate::Validator;
|
|
use rustyline::{Config, Editor, Helper};
|
|
use std::borrow::Cow;
|
|
use std::collections::HashSet;
|
|
use typechecker::TypeChecker;
|
|
|
|
const VERSION: &str = "0.1.0";
|
|
|
|
const HELP: &str = r#"
|
|
Lux - A functional language with first-class effects
|
|
|
|
Commands:
|
|
:help, :h Show this help
|
|
:quit, :q Exit the REPL
|
|
:type <expr> Show the type of an expression
|
|
:info <name> Show info about a binding
|
|
:doc <name> Show documentation for a function/type
|
|
:browse <mod> List exports of a module (List, String, Option, etc.)
|
|
:env Show user-defined bindings
|
|
:clear Clear the environment
|
|
:load <file> Load and execute a file
|
|
:trace on/off Enable/disable effect tracing
|
|
:traces Show recorded effect traces
|
|
|
|
Keyboard:
|
|
Tab Autocomplete
|
|
Ctrl-C Cancel current input
|
|
Ctrl-D Exit
|
|
Up/Down Browse history
|
|
Ctrl-R Search history
|
|
|
|
Examples:
|
|
> let x = 42
|
|
> x + 1
|
|
43
|
|
|
|
> fn double(n: Int): Int = n * 2
|
|
> :type double
|
|
double : fn(Int) -> Int
|
|
> double(21)
|
|
42
|
|
|
|
> match Some(5) { Some(x) => x, None => 0 }
|
|
5
|
|
"#;
|
|
|
|
fn main() {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
|
|
if args.len() > 1 {
|
|
match args[1].as_str() {
|
|
"--lsp" => {
|
|
// Run LSP server
|
|
if let Err(e) = lsp::LspServer::run() {
|
|
eprintln!("LSP server error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"--help" | "-h" => {
|
|
print_help();
|
|
}
|
|
"--version" | "-v" => {
|
|
println!("Lux {}", VERSION);
|
|
}
|
|
"fmt" => {
|
|
// Format files (auto-discovers if no file specified)
|
|
format_files(&args[2..]);
|
|
}
|
|
"test" => {
|
|
// Run tests
|
|
run_tests(&args[2..]);
|
|
}
|
|
"watch" => {
|
|
// Watch mode
|
|
if args.len() < 3 {
|
|
eprintln!("Usage: lux watch <file.lux>");
|
|
std::process::exit(1);
|
|
}
|
|
watch_file(&args[2]);
|
|
}
|
|
"init" => {
|
|
// Initialize a new project
|
|
init_project(args.get(2).map(|s| s.as_str()));
|
|
}
|
|
"check" => {
|
|
// Type check files (auto-discovers if no file specified)
|
|
check_files(&args[2..]);
|
|
}
|
|
"debug" => {
|
|
// Start debugger
|
|
if args.len() < 3 {
|
|
eprintln!("Usage: lux debug <file.lux>");
|
|
std::process::exit(1);
|
|
}
|
|
if let Err(e) = debugger::Debugger::run(&args[2]) {
|
|
eprintln!("Debugger error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"pkg" => {
|
|
// Package manager
|
|
handle_pkg_command(&args[2..]);
|
|
}
|
|
"registry" => {
|
|
// Run package registry server
|
|
let storage = args.iter()
|
|
.position(|a| a == "--storage" || a == "-s")
|
|
.and_then(|i| args.get(i + 1))
|
|
.map(|s| s.as_str())
|
|
.unwrap_or("./lux-registry");
|
|
|
|
let bind = args.iter()
|
|
.position(|a| a == "--bind" || a == "-b")
|
|
.and_then(|i| args.get(i + 1))
|
|
.map(|s| s.as_str())
|
|
.unwrap_or("127.0.0.1:8080");
|
|
|
|
if let Err(e) = registry::run_registry_server(storage, bind) {
|
|
eprintln!("Registry server error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"compile" => {
|
|
// Compile to native binary or JavaScript
|
|
if args.len() < 3 {
|
|
eprintln!("Usage: lux compile <file.lux> [-o binary]");
|
|
eprintln!(" lux compile <file.lux> --run");
|
|
eprintln!(" lux compile <file.lux> --emit-c [-o file.c]");
|
|
eprintln!(" lux compile <file.lux> --target js [-o file.js]");
|
|
std::process::exit(1);
|
|
}
|
|
let run_after = args.iter().any(|a| a == "--run");
|
|
let emit_c = args.iter().any(|a| a == "--emit-c");
|
|
let target_js = args.iter()
|
|
.position(|a| a == "--target")
|
|
.and_then(|i| args.get(i + 1))
|
|
.map(|s| s.as_str() == "js")
|
|
.unwrap_or(false);
|
|
let output_path = args.iter()
|
|
.position(|a| a == "-o")
|
|
.and_then(|i| args.get(i + 1))
|
|
.map(|s| s.as_str());
|
|
|
|
if target_js {
|
|
compile_to_js(&args[2], output_path, run_after);
|
|
} else {
|
|
compile_to_c(&args[2], output_path, run_after, emit_c);
|
|
}
|
|
}
|
|
path => {
|
|
// Run a file
|
|
run_file(path);
|
|
}
|
|
}
|
|
} else {
|
|
// Start REPL
|
|
run_repl();
|
|
}
|
|
}
|
|
|
|
fn print_help() {
|
|
println!("Lux {} - A functional language with first-class effects", VERSION);
|
|
println!();
|
|
println!("Usage:");
|
|
println!(" lux Start the REPL");
|
|
println!(" lux <file.lux> Run a file (interpreter)");
|
|
println!(" lux compile <file.lux> Compile to native binary");
|
|
println!(" lux compile <f> -o app Compile to binary named 'app'");
|
|
println!(" lux compile <f> --run Compile and execute");
|
|
println!(" lux compile <f> --emit-c Output C code instead of binary");
|
|
println!(" lux compile <f> --target js Compile to JavaScript");
|
|
println!(" lux fmt [file] [--check] Format files (auto-discovers if no file given)");
|
|
println!(" lux check [file] Type check files (auto-discovers if no file given)");
|
|
println!(" lux test [pattern] Run tests (optional pattern filter)");
|
|
println!(" lux watch <file.lux> Watch and re-run on changes");
|
|
println!(" lux debug <file.lux> Start interactive debugger");
|
|
println!(" lux init [name] Initialize a new project");
|
|
println!(" lux pkg <command> Package manager (install, add, remove, list, update)");
|
|
println!(" lux registry Start package registry server");
|
|
println!(" -s, --storage <dir> Storage directory (default: ./lux-registry)");
|
|
println!(" -b, --bind <addr> Bind address (default: 127.0.0.1:8080)");
|
|
println!(" lux --lsp Start LSP server (for IDE integration)");
|
|
println!(" lux --help Show this help");
|
|
println!(" lux --version Show version");
|
|
}
|
|
|
|
fn format_files(args: &[String]) {
|
|
use formatter::{format, FormatConfig};
|
|
use std::path::Path;
|
|
|
|
let check_only = args.iter().any(|a| a == "--check");
|
|
let pattern = args.iter().find(|a| !a.starts_with('-')).map(|s| s.as_str());
|
|
|
|
// Collect files to format
|
|
let mut files_to_format = Vec::new();
|
|
|
|
// If a specific file is given, use it
|
|
if let Some(p) = pattern {
|
|
if Path::new(p).is_file() {
|
|
files_to_format.push(std::path::PathBuf::from(p));
|
|
}
|
|
}
|
|
|
|
// If no specific file found, auto-discover
|
|
if files_to_format.is_empty() {
|
|
// Current directory (non-recursive for src/)
|
|
if Path::new("src").is_dir() {
|
|
collect_lux_files("src", pattern, &mut files_to_format);
|
|
}
|
|
|
|
// Also check current directory for top-level files
|
|
collect_lux_files_nonrecursive(".", pattern, &mut files_to_format);
|
|
|
|
// examples/ subdirectory
|
|
if Path::new("examples").is_dir() {
|
|
collect_lux_files("examples", pattern, &mut files_to_format);
|
|
}
|
|
|
|
// tests/ subdirectory
|
|
if Path::new("tests").is_dir() {
|
|
collect_lux_files("tests", pattern, &mut files_to_format);
|
|
}
|
|
}
|
|
|
|
if files_to_format.is_empty() {
|
|
println!("No .lux files found.");
|
|
println!("Looking in: ., src/, examples/, tests/");
|
|
return;
|
|
}
|
|
|
|
// Sort for consistent output
|
|
files_to_format.sort();
|
|
|
|
let config = FormatConfig::default();
|
|
let mut formatted_count = 0;
|
|
let mut unchanged_count = 0;
|
|
let mut error_count = 0;
|
|
let mut would_reformat = Vec::new();
|
|
|
|
for file_path in &files_to_format {
|
|
let path = file_path.to_string_lossy().to_string();
|
|
|
|
let source = match std::fs::read_to_string(file_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("{}: ERROR - {}", path, e);
|
|
error_count += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let formatted = match format(&source, &config) {
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
eprintln!("{}: FORMAT ERROR - {}", path, e);
|
|
error_count += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
if check_only {
|
|
if source != formatted {
|
|
would_reformat.push(path);
|
|
} else {
|
|
unchanged_count += 1;
|
|
}
|
|
} else if source != formatted {
|
|
if let Err(e) = std::fs::write(file_path, &formatted) {
|
|
eprintln!("{}: WRITE ERROR - {}", path, e);
|
|
error_count += 1;
|
|
continue;
|
|
}
|
|
println!("Formatted {}", path);
|
|
formatted_count += 1;
|
|
} else {
|
|
unchanged_count += 1;
|
|
}
|
|
}
|
|
|
|
println!();
|
|
if check_only {
|
|
if !would_reformat.is_empty() {
|
|
println!("Files that would be reformatted:");
|
|
for path in &would_reformat {
|
|
println!(" {}", path);
|
|
}
|
|
println!();
|
|
println!("{} would be reformatted, {} already formatted", would_reformat.len(), unchanged_count);
|
|
std::process::exit(1);
|
|
} else {
|
|
println!("All {} files are correctly formatted", unchanged_count);
|
|
}
|
|
} else {
|
|
println!("{} formatted, {} unchanged, {} errors", formatted_count, unchanged_count, error_count);
|
|
}
|
|
|
|
if error_count > 0 {
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn check_files(args: &[String]) {
|
|
use modules::ModuleLoader;
|
|
use std::path::Path;
|
|
|
|
let pattern = args.first().map(|s| s.as_str());
|
|
|
|
// Collect files to check
|
|
let mut files_to_check = Vec::new();
|
|
|
|
// If a specific file is given, use it
|
|
if let Some(p) = pattern {
|
|
if Path::new(p).is_file() {
|
|
files_to_check.push(std::path::PathBuf::from(p));
|
|
}
|
|
}
|
|
|
|
// If no specific file found, auto-discover
|
|
if files_to_check.is_empty() {
|
|
// Current directory (non-recursive for src/)
|
|
if Path::new("src").is_dir() {
|
|
collect_lux_files("src", pattern, &mut files_to_check);
|
|
}
|
|
|
|
// Also check current directory for top-level files
|
|
collect_lux_files_nonrecursive(".", pattern, &mut files_to_check);
|
|
|
|
// examples/ subdirectory
|
|
if Path::new("examples").is_dir() {
|
|
collect_lux_files("examples", pattern, &mut files_to_check);
|
|
}
|
|
|
|
// tests/ subdirectory
|
|
if Path::new("tests").is_dir() {
|
|
collect_lux_files("tests", pattern, &mut files_to_check);
|
|
}
|
|
}
|
|
|
|
if files_to_check.is_empty() {
|
|
println!("No .lux files found.");
|
|
println!("Looking in: ., src/, examples/, tests/");
|
|
return;
|
|
}
|
|
|
|
// Sort for consistent output
|
|
files_to_check.sort();
|
|
|
|
let mut passed = 0;
|
|
let mut failed = 0;
|
|
|
|
for file_path in &files_to_check {
|
|
let path = file_path.to_string_lossy().to_string();
|
|
let source = match std::fs::read_to_string(file_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("{}: ERROR - {}", path, e);
|
|
failed += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let mut loader = ModuleLoader::new();
|
|
if let Some(parent) = file_path.parent() {
|
|
loader.add_search_path(parent.to_path_buf());
|
|
}
|
|
|
|
let program = match loader.load_source(&source, Some(file_path)) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("{}: MODULE ERROR - {}", path, e);
|
|
failed += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let mut checker = TypeChecker::new();
|
|
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
|
|
eprintln!("{}: FAILED", path);
|
|
for error in errors {
|
|
let diagnostic = error.to_diagnostic();
|
|
eprint!("{}", render(&diagnostic, &source, Some(&path)));
|
|
}
|
|
failed += 1;
|
|
} else {
|
|
println!("{}: OK", path);
|
|
passed += 1;
|
|
}
|
|
}
|
|
|
|
println!();
|
|
println!("Checked {} files: {} passed, {} failed", passed + failed, passed, failed);
|
|
|
|
if failed > 0 {
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn collect_lux_files_nonrecursive(dir: &str, pattern: Option<&str>, files: &mut Vec<std::path::PathBuf>) {
|
|
use std::fs;
|
|
|
|
for entry in fs::read_dir(dir).into_iter().flatten().flatten() {
|
|
let path = entry.path();
|
|
if path.is_file() && path.extension().map(|e| e == "lux").unwrap_or(false) {
|
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
|
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
|
files.push(path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: bool) {
|
|
use codegen::c_backend::CBackend;
|
|
use modules::ModuleLoader;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
let file_path = Path::new(path);
|
|
let source = match std::fs::read_to_string(file_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("Error reading file '{}': {}", path, e);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Parse with module loading
|
|
let mut loader = ModuleLoader::new();
|
|
if let Some(parent) = file_path.parent() {
|
|
loader.add_search_path(parent.to_path_buf());
|
|
}
|
|
|
|
let program = match loader.load_source(&source, Some(file_path)) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Module error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Type check
|
|
let mut checker = TypeChecker::new();
|
|
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
|
|
for error in errors {
|
|
let diagnostic = error.to_diagnostic();
|
|
eprint!("{}", render(&diagnostic, &source, Some(path)));
|
|
}
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Generate C code
|
|
let mut backend = CBackend::new();
|
|
let c_code = match backend.generate(&program) {
|
|
Ok(code) => code,
|
|
Err(e) => {
|
|
eprintln!("C codegen error: {}", e);
|
|
eprintln!();
|
|
eprintln!("Note: The C backend supports functions, closures, ADTs,");
|
|
eprintln!("pattern matching, lists, and Console.print.");
|
|
eprintln!();
|
|
eprintln!("Not yet supported: other effects, some advanced features.");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Handle --emit-c: output C code instead of binary
|
|
if emit_c {
|
|
if let Some(out_path) = output_path {
|
|
if let Err(e) = std::fs::write(out_path, &c_code) {
|
|
eprintln!("Error writing file '{}': {}", out_path, e);
|
|
std::process::exit(1);
|
|
}
|
|
eprintln!("Wrote C code to {}", out_path);
|
|
} else {
|
|
println!("{}", c_code);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Default: compile to native binary
|
|
let temp_c = std::env::temp_dir().join("lux_output.c");
|
|
|
|
// Determine output binary name
|
|
let output_bin = if let Some(out) = output_path {
|
|
Path::new(out).to_path_buf()
|
|
} else {
|
|
// Derive from source filename: foo.lux -> ./foo
|
|
let stem = file_path.file_stem()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("a.out");
|
|
Path::new(".").join(stem)
|
|
};
|
|
|
|
if let Err(e) = std::fs::write(&temp_c, &c_code) {
|
|
eprintln!("Error writing temp file: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Find C compiler
|
|
let cc = std::env::var("CC").unwrap_or_else(|_| "cc".to_string());
|
|
|
|
let compile_result = Command::new(&cc)
|
|
.args(["-O2", "-o"])
|
|
.arg(&output_bin)
|
|
.arg(&temp_c)
|
|
.output();
|
|
|
|
match compile_result {
|
|
Ok(output) => {
|
|
if !output.status.success() {
|
|
eprintln!("C compilation failed:");
|
|
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to run C compiler '{}': {}", cc, e);
|
|
eprintln!("Make sure gcc or clang is installed, or set CC environment variable.");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
// Keep temp file for debugging
|
|
eprintln!("C file: {}", temp_c.display());
|
|
// let _ = std::fs::remove_file(&temp_c);
|
|
|
|
if run_after {
|
|
// Run the compiled binary
|
|
let run_result = Command::new(&output_bin).status();
|
|
match run_result {
|
|
Ok(status) => {
|
|
std::process::exit(status.code().unwrap_or(1));
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to run compiled binary: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
} else {
|
|
// Just print where the binary is
|
|
eprintln!("Compiled to {}", output_bin.display());
|
|
}
|
|
}
|
|
|
|
fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) {
|
|
use codegen::js_backend::JsBackend;
|
|
use modules::ModuleLoader;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
let file_path = Path::new(path);
|
|
let source = match std::fs::read_to_string(file_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("Error reading file '{}': {}", path, e);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Parse with module loading
|
|
let mut loader = ModuleLoader::new();
|
|
if let Some(parent) = file_path.parent() {
|
|
loader.add_search_path(parent.to_path_buf());
|
|
}
|
|
|
|
let program = match loader.load_source(&source, Some(file_path)) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Module error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Type check
|
|
let mut checker = TypeChecker::new();
|
|
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
|
|
for error in errors {
|
|
let diagnostic = error.to_diagnostic();
|
|
eprint!("{}", render(&diagnostic, &source, Some(path)));
|
|
}
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Generate JavaScript code
|
|
let mut backend = JsBackend::new();
|
|
let js_code = match backend.generate(&program) {
|
|
Ok(code) => code,
|
|
Err(e) => {
|
|
eprintln!("JS codegen error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Determine output file path
|
|
let output_js = if let Some(out) = output_path {
|
|
Path::new(out).to_path_buf()
|
|
} else {
|
|
// Derive from source filename: foo.lux -> ./foo.js
|
|
let stem = file_path.file_stem()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("output");
|
|
Path::new(".").join(format!("{}.js", stem))
|
|
};
|
|
|
|
// Write the JavaScript file
|
|
if let Err(e) = std::fs::write(&output_js, &js_code) {
|
|
eprintln!("Error writing file '{}': {}", output_js.display(), e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
if run_after {
|
|
// Run with Node.js
|
|
let node_result = Command::new("node")
|
|
.arg(&output_js)
|
|
.status();
|
|
|
|
match node_result {
|
|
Ok(status) => {
|
|
std::process::exit(status.code().unwrap_or(1));
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to run with Node.js: {}", e);
|
|
eprintln!("Make sure Node.js is installed.");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
} else {
|
|
eprintln!("Compiled to {}", output_js.display());
|
|
}
|
|
}
|
|
|
|
fn run_tests(args: &[String]) {
|
|
use std::path::Path;
|
|
use std::fs;
|
|
|
|
// Find test files
|
|
let pattern = args.first().map(|s| s.as_str());
|
|
|
|
// Look for test files in multiple locations
|
|
let mut test_files = Vec::new();
|
|
|
|
// Current directory
|
|
collect_test_files(".", pattern, &mut test_files);
|
|
|
|
// tests/ subdirectory
|
|
if Path::new("tests").is_dir() {
|
|
collect_test_files("tests", pattern, &mut test_files);
|
|
}
|
|
|
|
// examples/ subdirectory (for example tests)
|
|
if Path::new("examples").is_dir() {
|
|
collect_test_files("examples", pattern, &mut test_files);
|
|
}
|
|
|
|
// If a specific file is given, use that
|
|
if let Some(p) = pattern {
|
|
if Path::new(p).is_file() {
|
|
test_files.clear();
|
|
test_files.push(std::path::PathBuf::from(p));
|
|
}
|
|
}
|
|
|
|
if test_files.is_empty() {
|
|
println!("No test files found.");
|
|
println!("Test files should be named test_*.lux or *_test.lux");
|
|
println!("Or contain functions named test_*");
|
|
return;
|
|
}
|
|
|
|
println!("Running tests...\n");
|
|
|
|
let mut total_passed = 0;
|
|
let mut total_failed = 0;
|
|
let mut all_failures = Vec::new();
|
|
|
|
for test_file in &test_files {
|
|
let path_str = test_file.to_string_lossy().to_string();
|
|
|
|
// Read and parse the file
|
|
let source = match fs::read_to_string(test_file) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
println!(" {} ... ERROR: {}", path_str, e);
|
|
total_failed += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let program = match Parser::parse_source(&source) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
println!(" {} ... PARSE ERROR: {}", path_str, e);
|
|
total_failed += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
// Type check
|
|
let mut checker = typechecker::TypeChecker::new();
|
|
if let Err(errors) = checker.check_program(&program) {
|
|
println!(" {} ... TYPE ERROR", path_str);
|
|
for err in errors {
|
|
eprintln!(" {}", err);
|
|
}
|
|
total_failed += 1;
|
|
continue;
|
|
}
|
|
|
|
// Get auto-generated migrations from typechecker
|
|
let auto_migrations = checker.get_auto_migrations().clone();
|
|
|
|
// Find test functions (functions starting with test_)
|
|
let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| {
|
|
if let ast::Declaration::Function(f) = d {
|
|
if f.name.name.starts_with("test_") {
|
|
return Some(f.name.name.clone());
|
|
}
|
|
}
|
|
None
|
|
}).collect();
|
|
|
|
if test_funcs.is_empty() {
|
|
// No test functions, run the whole file
|
|
let mut interp = Interpreter::new();
|
|
interp.register_auto_migrations(&auto_migrations);
|
|
interp.reset_test_results();
|
|
|
|
match interp.run(&program) {
|
|
Ok(_) => {
|
|
let results = interp.get_test_results();
|
|
if results.failed == 0 && results.passed == 0 {
|
|
// No Test assertions, just check it runs
|
|
println!(" {} ... OK (no assertions)", path_str);
|
|
total_passed += 1;
|
|
} else if results.failed == 0 {
|
|
println!(" {} ... OK ({} assertions)", path_str, results.passed);
|
|
total_passed += results.passed;
|
|
} else {
|
|
println!(" {} ... FAILED ({} passed, {} failed)", path_str, results.passed, results.failed);
|
|
total_passed += results.passed;
|
|
total_failed += results.failed;
|
|
for failure in &results.failures {
|
|
all_failures.push((path_str.clone(), "".to_string(), failure.clone()));
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
println!(" {} ... RUNTIME ERROR: {}", path_str, e);
|
|
total_failed += 1;
|
|
}
|
|
}
|
|
} else {
|
|
// Run individual test functions
|
|
println!(" {}:", path_str);
|
|
|
|
for test_name in &test_funcs {
|
|
let mut interp = Interpreter::new();
|
|
interp.register_auto_migrations(&auto_migrations);
|
|
interp.reset_test_results();
|
|
|
|
// First run the file to define all functions
|
|
if let Err(e) = interp.run(&program) {
|
|
println!(" {} ... ERROR: {}", test_name, e);
|
|
total_failed += 1;
|
|
continue;
|
|
}
|
|
|
|
// Call the test function
|
|
let call_source = format!("let testResult = run {}() with {{}}", test_name);
|
|
let call_program = match Parser::parse_source(&call_source) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
println!(" {} ... ERROR: {}", test_name, e);
|
|
total_failed += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
match interp.run(&call_program) {
|
|
Ok(_) => {
|
|
let results = interp.get_test_results();
|
|
if results.failed == 0 {
|
|
println!(" {} ... OK", test_name);
|
|
total_passed += 1;
|
|
} else {
|
|
println!(" {} ... FAILED", test_name);
|
|
total_failed += 1;
|
|
for failure in &results.failures {
|
|
all_failures.push((path_str.clone(), test_name.clone(), failure.clone()));
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
println!(" {} ... ERROR: {}", test_name, e);
|
|
total_failed += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Print failure details
|
|
if !all_failures.is_empty() {
|
|
println!("\n--- Failures ---\n");
|
|
for (file, test, failure) in &all_failures {
|
|
if test.is_empty() {
|
|
println!("{}:", file);
|
|
} else {
|
|
println!("{} - {}:", file, test);
|
|
}
|
|
println!(" {}", failure.message);
|
|
if let Some(expected) = &failure.expected {
|
|
println!(" Expected: {}", expected);
|
|
}
|
|
if let Some(actual) = &failure.actual {
|
|
println!(" Actual: {}", actual);
|
|
}
|
|
println!();
|
|
}
|
|
}
|
|
|
|
println!("Results: {} passed, {} failed", total_passed, total_failed);
|
|
|
|
if total_failed > 0 {
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn collect_test_files(dir: &str, pattern: Option<&str>, files: &mut Vec<std::path::PathBuf>) {
|
|
use std::fs;
|
|
|
|
for entry in fs::read_dir(dir).into_iter().flatten().flatten() {
|
|
let path = entry.path();
|
|
if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
|
if name.starts_with("test_") || name.ends_with("_test.lux") {
|
|
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
|
files.push(path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn collect_lux_files(dir: &str, pattern: Option<&str>, files: &mut Vec<std::path::PathBuf>) {
|
|
use std::fs;
|
|
|
|
for entry in fs::read_dir(dir).into_iter().flatten().flatten() {
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
// Recursively search subdirectories
|
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
|
// Skip hidden directories and common non-source dirs
|
|
if !name.starts_with('.') && name != "target" && name != "node_modules" {
|
|
collect_lux_files(path.to_str().unwrap_or(""), pattern, files);
|
|
}
|
|
}
|
|
} else if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
|
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
|
files.push(path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn watch_file(path: &str) {
|
|
use std::time::{Duration, SystemTime};
|
|
use std::path::Path;
|
|
|
|
let file_path = Path::new(path);
|
|
if !file_path.exists() {
|
|
eprintln!("File not found: {}", path);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
println!("Watching {} for changes (Ctrl+C to stop)...", path);
|
|
println!();
|
|
|
|
let mut last_modified = SystemTime::UNIX_EPOCH;
|
|
|
|
loop {
|
|
let metadata = match std::fs::metadata(file_path) {
|
|
Ok(m) => m,
|
|
Err(_) => {
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
|
|
|
if modified > last_modified {
|
|
last_modified = modified;
|
|
|
|
// Clear screen
|
|
print!("\x1B[2J\x1B[H");
|
|
|
|
println!("=== Running {} ===", path);
|
|
println!();
|
|
|
|
// Run the file
|
|
let result = std::process::Command::new(std::env::current_exe().unwrap())
|
|
.arg(path)
|
|
.status();
|
|
|
|
match result {
|
|
Ok(status) if status.success() => {
|
|
println!();
|
|
println!("=== Success ===");
|
|
}
|
|
Ok(_) => {
|
|
println!();
|
|
println!("=== Failed ===");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error running file: {}", e);
|
|
}
|
|
}
|
|
|
|
println!();
|
|
println!("Watching for changes...");
|
|
}
|
|
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
}
|
|
}
|
|
|
|
fn handle_pkg_command(args: &[String]) {
|
|
use package::{PackageManager, DependencySource};
|
|
use std::path::PathBuf;
|
|
|
|
if args.is_empty() {
|
|
print_pkg_help();
|
|
return;
|
|
}
|
|
|
|
// Help doesn't require being in a project
|
|
if matches!(args[0].as_str(), "help" | "--help" | "-h") {
|
|
print_pkg_help();
|
|
return;
|
|
}
|
|
|
|
// Init doesn't require being in a project (it creates one)
|
|
if args[0] == "init" {
|
|
init_pkg_here(args.get(1).map(|s| s.as_str()));
|
|
return;
|
|
}
|
|
|
|
let project_root = match PackageManager::find_project_root() {
|
|
Some(root) => root,
|
|
None => {
|
|
eprintln!("Error: Not in a Lux project (no lux.toml found)");
|
|
eprintln!("Run 'lux pkg init' to initialize a project here, or 'lux init <name>' to create a new project");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
let pkg = PackageManager::new(&project_root);
|
|
|
|
match args[0].as_str() {
|
|
"install" | "i" => {
|
|
if let Err(e) = pkg.install() {
|
|
eprintln!("Error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"add" => {
|
|
if args.len() < 2 {
|
|
eprintln!("Usage: lux pkg add <package> [version] [--git <url>] [--path <path>]");
|
|
std::process::exit(1);
|
|
}
|
|
let name = &args[1];
|
|
let mut version = "0.0.0".to_string();
|
|
let mut source = DependencySource::Registry;
|
|
|
|
let mut i = 2;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--git" => {
|
|
if i + 1 < args.len() {
|
|
let url = args[i + 1].clone();
|
|
let branch = if i + 3 < args.len() && args[i + 2] == "--branch" {
|
|
i += 2;
|
|
Some(args[i + 1].clone())
|
|
} else {
|
|
None
|
|
};
|
|
source = DependencySource::Git { url, branch };
|
|
i += 2;
|
|
} else {
|
|
eprintln!("--git requires a URL");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"--path" => {
|
|
if i + 1 < args.len() {
|
|
source = DependencySource::Path {
|
|
path: PathBuf::from(&args[i + 1]),
|
|
};
|
|
i += 2;
|
|
} else {
|
|
eprintln!("--path requires a path");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
v if !v.starts_with('-') => {
|
|
version = v.to_string();
|
|
i += 1;
|
|
}
|
|
_ => i += 1,
|
|
}
|
|
}
|
|
|
|
if let Err(e) = pkg.add(name, &version, source) {
|
|
eprintln!("Error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"remove" | "rm" => {
|
|
if args.len() < 2 {
|
|
eprintln!("Usage: lux pkg remove <package>");
|
|
std::process::exit(1);
|
|
}
|
|
if let Err(e) = pkg.remove(&args[1]) {
|
|
eprintln!("Error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"list" | "ls" => {
|
|
if let Err(e) = pkg.list() {
|
|
eprintln!("Error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"update" => {
|
|
if let Err(e) = pkg.update() {
|
|
eprintln!("Error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"clean" => {
|
|
if let Err(e) = pkg.clean() {
|
|
eprintln!("Error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
"search" => {
|
|
if args.len() < 2 {
|
|
eprintln!("Usage: lux pkg search <query>");
|
|
std::process::exit(1);
|
|
}
|
|
search_registry(&args[1]);
|
|
}
|
|
"publish" => {
|
|
publish_package(&project_root);
|
|
}
|
|
"help" | "--help" | "-h" => {
|
|
print_pkg_help();
|
|
}
|
|
unknown => {
|
|
eprintln!("Unknown package command: {}", unknown);
|
|
print_pkg_help();
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn search_registry(query: &str) {
|
|
let registry_url = std::env::var("LUX_REGISTRY_URL")
|
|
.unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string());
|
|
|
|
println!("Searching for '{}' in {}...", query, registry_url);
|
|
|
|
// Make HTTP request to registry
|
|
let url = format!("{}/api/v1/search?q={}", registry_url, query);
|
|
|
|
// Use a simple HTTP client (could use reqwest in production)
|
|
match simple_http_get(&url) {
|
|
Ok(response) => {
|
|
// Parse JSON response and display results
|
|
if response.contains("\"packages\":[]") {
|
|
println!("No packages found matching '{}'", query);
|
|
} else {
|
|
println!("\nFound packages:");
|
|
// Simple parsing of package names from JSON
|
|
for line in response.lines() {
|
|
if line.contains("\"name\":") {
|
|
if let Some(start) = line.find("\"name\":") {
|
|
let rest = &line[start + 8..];
|
|
if let Some(end) = rest.find('"') {
|
|
let name = &rest[..end];
|
|
println!(" {}", name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to connect to registry: {}", e);
|
|
eprintln!("Make sure the registry server is running or check LUX_REGISTRY_URL");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn publish_package(project_root: &std::path::Path) {
|
|
use std::fs;
|
|
|
|
let registry_url = std::env::var("LUX_REGISTRY_URL")
|
|
.unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string());
|
|
|
|
// Load manifest
|
|
let manifest_path = project_root.join("lux.toml");
|
|
if !manifest_path.exists() {
|
|
eprintln!("No lux.toml found");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let manifest_content = fs::read_to_string(&manifest_path).unwrap();
|
|
|
|
// Extract project info
|
|
let mut name = String::new();
|
|
let mut version = String::new();
|
|
|
|
for line in manifest_content.lines() {
|
|
let line = line.trim();
|
|
if line.starts_with("name") {
|
|
if let Some(eq) = line.find('=') {
|
|
name = line[eq+1..].trim().trim_matches('"').to_string();
|
|
}
|
|
} else if line.starts_with("version") {
|
|
if let Some(eq) = line.find('=') {
|
|
version = line[eq+1..].trim().trim_matches('"').to_string();
|
|
}
|
|
}
|
|
}
|
|
|
|
if name.is_empty() || version.is_empty() {
|
|
eprintln!("Invalid lux.toml: missing name or version");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
println!("Publishing {} v{} to {}...", name, version, registry_url);
|
|
|
|
// Create tarball of the package
|
|
let tarball_name = format!("{}-{}.tar.gz", name, version);
|
|
let tarball_path = project_root.join(&tarball_name);
|
|
|
|
// Use tar command to create tarball
|
|
let status = std::process::Command::new("tar")
|
|
.arg("-czf")
|
|
.arg(&tarball_path)
|
|
.arg("--exclude=.lux_packages")
|
|
.arg("--exclude=.git")
|
|
.arg("--exclude=target")
|
|
.arg("-C")
|
|
.arg(project_root)
|
|
.arg(".")
|
|
.status();
|
|
|
|
match status {
|
|
Ok(s) if s.success() => {
|
|
println!("Created package tarball: {}", tarball_name);
|
|
println!();
|
|
println!("To publish, upload the tarball to the registry:");
|
|
println!(" curl -X POST {}/api/v1/publish -F 'package=@{}'",
|
|
registry_url, tarball_name);
|
|
|
|
// Clean up tarball
|
|
// fs::remove_file(&tarball_path).ok();
|
|
}
|
|
Ok(_) => {
|
|
eprintln!("Failed to create package tarball");
|
|
std::process::exit(1);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to run tar: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn simple_http_get(url: &str) -> Result<String, String> {
|
|
use std::io::{Read, Write};
|
|
use std::net::TcpStream;
|
|
|
|
// Parse URL
|
|
let url = url.strip_prefix("http://").or_else(|| url.strip_prefix("https://")).unwrap_or(url);
|
|
let (host_port, path) = if let Some(slash) = url.find('/') {
|
|
(&url[..slash], &url[slash..])
|
|
} else {
|
|
(url, "/")
|
|
};
|
|
|
|
let (host, port) = if let Some(colon) = host_port.find(':') {
|
|
(&host_port[..colon], host_port[colon+1..].parse::<u16>().unwrap_or(80))
|
|
} else {
|
|
(host_port, 80)
|
|
};
|
|
|
|
let mut stream = TcpStream::connect((host, port))
|
|
.map_err(|e| format!("Connection failed: {}", e))?;
|
|
|
|
let request = format!(
|
|
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
|
|
path, host
|
|
);
|
|
|
|
stream.write_all(request.as_bytes())
|
|
.map_err(|e| format!("Write failed: {}", e))?;
|
|
|
|
let mut response = String::new();
|
|
stream.read_to_string(&mut response)
|
|
.map_err(|e| format!("Read failed: {}", e))?;
|
|
|
|
// Extract body (after \r\n\r\n)
|
|
if let Some(body_start) = response.find("\r\n\r\n") {
|
|
Ok(response[body_start + 4..].to_string())
|
|
} else {
|
|
Ok(response)
|
|
}
|
|
}
|
|
|
|
fn print_pkg_help() {
|
|
println!("Lux Package Manager");
|
|
println!();
|
|
println!("Usage: lux pkg <command> [options]");
|
|
println!();
|
|
println!("Commands:");
|
|
println!(" init [name] Initialize a lux.toml in the current directory");
|
|
println!(" install, i Install all dependencies from lux.toml");
|
|
println!(" add <pkg> Add a dependency");
|
|
println!(" Options:");
|
|
println!(" [version] Specify version (default: 0.0.0)");
|
|
println!(" --git <url> Install from git repository");
|
|
println!(" --branch <name> Git branch (with --git)");
|
|
println!(" --path <path> Install from local path");
|
|
println!(" remove, rm Remove a dependency");
|
|
println!(" list, ls List dependencies and their status");
|
|
println!(" update Update all dependencies");
|
|
println!(" clean Remove installed packages");
|
|
println!(" search <query> Search for packages in the registry");
|
|
println!(" publish Publish package to the registry");
|
|
println!();
|
|
println!("Registry Configuration:");
|
|
println!(" Set LUX_REGISTRY_URL to use a custom registry (default: https://pkgs.lux-lang.org)");
|
|
println!();
|
|
println!("Examples:");
|
|
println!(" lux pkg init");
|
|
println!(" lux pkg init my-project");
|
|
println!(" lux pkg install");
|
|
println!(" lux pkg add http 1.0.0");
|
|
println!(" lux pkg add mylib --git https://github.com/user/mylib");
|
|
println!(" lux pkg add local-lib --path ../lib");
|
|
println!(" lux pkg remove http");
|
|
println!(" lux pkg search json");
|
|
println!(" lux pkg publish");
|
|
}
|
|
|
|
fn init_pkg_here(name: Option<&str>) {
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
let lux_toml = Path::new("lux.toml");
|
|
|
|
if lux_toml.exists() {
|
|
eprintln!("lux.toml already exists in this directory");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Get project name from argument or current directory name
|
|
let project_name = name.map(String::from).unwrap_or_else(|| {
|
|
std::env::current_dir()
|
|
.ok()
|
|
.and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string()))
|
|
.unwrap_or_else(|| "my-project".to_string())
|
|
});
|
|
|
|
// Create lux.toml
|
|
let toml_content = format!(
|
|
r#"[project]
|
|
name = "{}"
|
|
version = "0.1.0"
|
|
description = "A Lux project"
|
|
|
|
[dependencies]
|
|
# Add dependencies here
|
|
# example = "1.0.0"
|
|
# mylib = {{ git = "https://github.com/user/mylib" }}
|
|
# local = {{ path = "../local-lib" }}
|
|
"#,
|
|
project_name
|
|
);
|
|
|
|
if let Err(e) = fs::write(lux_toml, toml_content) {
|
|
eprintln!("Failed to create lux.toml: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
println!("Initialized Lux project: {}", project_name);
|
|
println!();
|
|
println!("Created lux.toml");
|
|
println!();
|
|
println!("Next steps:");
|
|
println!(" lux pkg add <package> Add a dependency");
|
|
println!(" lux pkg install Install dependencies");
|
|
}
|
|
|
|
fn init_project(name: Option<&str>) {
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
let project_name = name.unwrap_or("my-lux-project");
|
|
let project_dir = Path::new(project_name);
|
|
|
|
if project_dir.exists() {
|
|
eprintln!("Directory '{}' already exists", project_name);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Create project structure
|
|
fs::create_dir_all(project_dir.join("src")).unwrap();
|
|
fs::create_dir_all(project_dir.join("tests")).unwrap();
|
|
|
|
// Create lux.toml
|
|
let toml_content = format!(
|
|
r#"[project]
|
|
name = "{}"
|
|
version = "0.1.0"
|
|
description = "A Lux project"
|
|
|
|
[dependencies]
|
|
# Add dependencies here
|
|
# example = "1.0.0"
|
|
"#,
|
|
project_name
|
|
);
|
|
fs::write(project_dir.join("lux.toml"), toml_content).unwrap();
|
|
|
|
// Create main.lux
|
|
let main_content = r#"// Main entry point
|
|
|
|
fn main(): Unit with {Console} = {
|
|
Console.print("Hello from Lux!")
|
|
}
|
|
|
|
let output = run main() with {}
|
|
"#;
|
|
fs::write(project_dir.join("src").join("main.lux"), main_content).unwrap();
|
|
|
|
// Create a test file
|
|
let test_content = r#"// Example test file
|
|
|
|
fn testAddition(): Bool = {
|
|
let result = 2 + 2
|
|
result == 4
|
|
}
|
|
|
|
fn main(): Unit with {Console} = {
|
|
if testAddition() then
|
|
Console.print("Test passed!")
|
|
else
|
|
Console.print("Test failed!")
|
|
}
|
|
|
|
let output = run main() with {}
|
|
"#;
|
|
fs::write(project_dir.join("tests").join("test_example.lux"), test_content).unwrap();
|
|
|
|
// Create .gitignore
|
|
let gitignore_content = r#"# Lux build artifacts
|
|
/target/
|
|
*.luxc
|
|
|
|
# Editor files
|
|
.vscode/
|
|
.idea/
|
|
*.swp
|
|
*~
|
|
"#;
|
|
fs::write(project_dir.join(".gitignore"), gitignore_content).unwrap();
|
|
|
|
println!("Created new Lux project: {}", project_name);
|
|
println!();
|
|
println!("Project structure:");
|
|
println!(" {}/", project_name);
|
|
println!(" ├── lux.toml");
|
|
println!(" ├── src/");
|
|
println!(" │ └── main.lux");
|
|
println!(" └── tests/");
|
|
println!(" └── test_example.lux");
|
|
println!();
|
|
println!("To get started:");
|
|
println!(" cd {}", project_name);
|
|
println!(" lux src/main.lux");
|
|
}
|
|
|
|
fn run_file(path: &str) {
|
|
use modules::ModuleLoader;
|
|
use std::path::Path;
|
|
|
|
let file_path = Path::new(path);
|
|
let source = match std::fs::read_to_string(file_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("Error reading file '{}': {}", path, e);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Set up module loader with the file's directory as a search path
|
|
let mut loader = ModuleLoader::new();
|
|
if let Some(parent) = file_path.parent() {
|
|
loader.add_search_path(parent.to_path_buf());
|
|
}
|
|
|
|
// Load and parse the program (including any imports)
|
|
let program = match loader.load_source(&source, Some(file_path)) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Module error: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
let mut checker = TypeChecker::new();
|
|
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
|
|
for error in errors {
|
|
let diagnostic = error.to_diagnostic();
|
|
eprint!("{}", render(&diagnostic, &source, Some(path)));
|
|
}
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let mut interp = Interpreter::new();
|
|
match interp.run_with_modules(&program, &loader) {
|
|
Ok(value) => {
|
|
if !matches!(value, interpreter::Value::Unit) {
|
|
println!("{}", value);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
let diagnostic = e.to_diagnostic();
|
|
eprint!("{}", render(&diagnostic, &source, Some(path)));
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// REPL helper for tab completion and syntax highlighting
|
|
struct LuxHelper {
|
|
keywords: HashSet<String>,
|
|
commands: Vec<String>,
|
|
user_defined: HashSet<String>,
|
|
}
|
|
|
|
impl LuxHelper {
|
|
fn new() -> Self {
|
|
let keywords: HashSet<String> = vec![
|
|
"fn", "let", "if", "then", "else", "match", "with", "effect", "handler",
|
|
"handle", "resume", "type", "import", "pub", "is", "pure", "total",
|
|
"idempotent", "deterministic", "commutative", "where", "assume", "true",
|
|
"false", "None", "Some", "Ok", "Err", "Int", "Float", "String", "Bool",
|
|
"Char", "Unit", "Option", "Result", "List",
|
|
]
|
|
.into_iter()
|
|
.map(String::from)
|
|
.collect();
|
|
|
|
let commands = vec![
|
|
":help", ":h", ":quit", ":q", ":type", ":t", ":clear", ":load", ":l",
|
|
":trace", ":traces", ":info", ":i", ":env", ":doc", ":d", ":browse", ":b",
|
|
]
|
|
.into_iter()
|
|
.map(String::from)
|
|
.collect();
|
|
|
|
Self {
|
|
keywords,
|
|
commands,
|
|
user_defined: HashSet::new(),
|
|
}
|
|
}
|
|
|
|
fn add_definition(&mut self, name: &str) {
|
|
self.user_defined.insert(name.to_string());
|
|
}
|
|
}
|
|
|
|
impl Completer for LuxHelper {
|
|
type Candidate = Pair;
|
|
|
|
fn complete(
|
|
&self,
|
|
line: &str,
|
|
pos: usize,
|
|
_ctx: &rustyline::Context<'_>,
|
|
) -> rustyline::Result<(usize, Vec<Pair>)> {
|
|
// Find the start of the current word
|
|
let start = line[..pos]
|
|
.rfind(|c: char| c.is_whitespace() || "(){}[],.;:".contains(c))
|
|
.map(|i| i + 1)
|
|
.unwrap_or(0);
|
|
|
|
let prefix = &line[start..pos];
|
|
|
|
if prefix.is_empty() {
|
|
return Ok((pos, Vec::new()));
|
|
}
|
|
|
|
let mut completions = Vec::new();
|
|
|
|
// Complete commands
|
|
if prefix.starts_with(':') {
|
|
for cmd in &self.commands {
|
|
if cmd.starts_with(prefix) {
|
|
completions.push(Pair {
|
|
display: cmd.clone(),
|
|
replacement: cmd.clone(),
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
// Complete keywords
|
|
for kw in &self.keywords {
|
|
if kw.starts_with(prefix) {
|
|
completions.push(Pair {
|
|
display: kw.clone(),
|
|
replacement: kw.clone(),
|
|
});
|
|
}
|
|
}
|
|
// Complete user-defined names
|
|
for name in &self.user_defined {
|
|
if name.starts_with(prefix) {
|
|
completions.push(Pair {
|
|
display: name.clone(),
|
|
replacement: name.clone(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok((start, completions))
|
|
}
|
|
}
|
|
|
|
impl Hinter for LuxHelper {
|
|
type Hint = String;
|
|
|
|
fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option<String> {
|
|
None
|
|
}
|
|
}
|
|
|
|
impl Highlighter for LuxHelper {
|
|
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
|
|
Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
|
|
}
|
|
|
|
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
|
|
let mut result = String::with_capacity(line.len() * 2);
|
|
let mut chars = line.char_indices().peekable();
|
|
|
|
while let Some((i, c)) = chars.next() {
|
|
if c == '"' {
|
|
// String literal - highlight in green
|
|
result.push_str("\x1b[32m\"");
|
|
let mut escaped = false;
|
|
for (_, ch) in chars.by_ref() {
|
|
result.push(ch);
|
|
if escaped {
|
|
escaped = false;
|
|
} else if ch == '\\' {
|
|
escaped = true;
|
|
} else if ch == '"' {
|
|
break;
|
|
}
|
|
}
|
|
result.push_str("\x1b[0m");
|
|
} else if c == ':' && i == 0 {
|
|
// Command - highlight in cyan
|
|
result.push_str("\x1b[36m:");
|
|
for (_, ch) in chars.by_ref() {
|
|
if ch.is_whitespace() {
|
|
result.push_str("\x1b[0m");
|
|
result.push(ch);
|
|
break;
|
|
}
|
|
result.push(ch);
|
|
}
|
|
// Continue with the rest
|
|
for (_, ch) in chars.by_ref() {
|
|
result.push(ch);
|
|
}
|
|
} else if c.is_ascii_digit() || (c == '-' && chars.peek().map(|(_, ch)| ch.is_ascii_digit()).unwrap_or(false)) {
|
|
// Number - highlight in yellow
|
|
result.push_str("\x1b[33m");
|
|
result.push(c);
|
|
while let Some(&(_, ch)) = chars.peek() {
|
|
if ch.is_ascii_digit() || ch == '.' {
|
|
result.push(ch);
|
|
chars.next();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
result.push_str("\x1b[0m");
|
|
} else if c.is_alphabetic() || c == '_' {
|
|
// Identifier or keyword
|
|
let start = i;
|
|
let mut end = i + c.len_utf8();
|
|
while let Some(&(j, ch)) = chars.peek() {
|
|
if ch.is_alphanumeric() || ch == '_' {
|
|
end = j + ch.len_utf8();
|
|
chars.next();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
let word = &line[start..end];
|
|
|
|
// Check if it's a keyword
|
|
if self.keywords.contains(word) {
|
|
result.push_str("\x1b[35m"); // Magenta for keywords
|
|
result.push_str(word);
|
|
result.push_str("\x1b[0m");
|
|
} else if word.starts_with(char::is_uppercase) {
|
|
result.push_str("\x1b[34m"); // Blue for types/constructors
|
|
result.push_str(word);
|
|
result.push_str("\x1b[0m");
|
|
} else {
|
|
result.push_str(word);
|
|
}
|
|
} else if c == '/' && chars.peek().map(|(_, ch)| *ch == '/').unwrap_or(false) {
|
|
// Comment - highlight in gray
|
|
result.push_str("\x1b[90m");
|
|
result.push(c);
|
|
for (_, ch) in chars.by_ref() {
|
|
result.push(ch);
|
|
}
|
|
result.push_str("\x1b[0m");
|
|
} else {
|
|
result.push(c);
|
|
}
|
|
}
|
|
|
|
Cow::Owned(result)
|
|
}
|
|
|
|
fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl Validator for LuxHelper {}
|
|
impl Helper for LuxHelper {}
|
|
|
|
fn get_history_path() -> Option<std::path::PathBuf> {
|
|
std::env::var_os("HOME").map(|home| {
|
|
std::path::PathBuf::from(home).join(".lux_history")
|
|
})
|
|
}
|
|
|
|
fn run_repl() {
|
|
println!("Lux v{}", VERSION);
|
|
println!("Type :help for help, :quit to exit\n");
|
|
|
|
let config = Config::builder()
|
|
.history_ignore_space(true)
|
|
.completion_type(rustyline::CompletionType::List)
|
|
.edit_mode(rustyline::EditMode::Emacs)
|
|
.build();
|
|
|
|
let helper = LuxHelper::new();
|
|
let mut rl: Editor<LuxHelper, DefaultHistory> = Editor::with_config(config).unwrap();
|
|
rl.set_helper(Some(helper));
|
|
|
|
// Load history
|
|
if let Some(history_path) = get_history_path() {
|
|
if let Some(parent) = history_path.parent() {
|
|
let _ = std::fs::create_dir_all(parent);
|
|
}
|
|
let _ = rl.load_history(&history_path);
|
|
}
|
|
|
|
let mut interp = Interpreter::new();
|
|
let mut checker = TypeChecker::new();
|
|
let mut buffer = String::new();
|
|
let mut continuation = false;
|
|
|
|
loop {
|
|
let prompt = if continuation { "... " } else { "lux> " };
|
|
|
|
match rl.readline(prompt) {
|
|
Ok(line) => {
|
|
let line = line.trim_end().to_string();
|
|
|
|
// Don't add empty lines or continuations to history
|
|
if !line.is_empty() && !continuation {
|
|
let _ = rl.add_history_entry(line.as_str());
|
|
}
|
|
|
|
// Handle commands
|
|
if !continuation && line.starts_with(':') {
|
|
handle_command(&line, &mut interp, &mut checker, rl.helper_mut().unwrap());
|
|
continue;
|
|
}
|
|
|
|
// Accumulate input
|
|
buffer.push_str(&line);
|
|
buffer.push('\n');
|
|
|
|
// Check for continuation (unbalanced braces/parens)
|
|
let open_braces = buffer.chars().filter(|c| *c == '{').count();
|
|
let close_braces = buffer.chars().filter(|c| *c == '}').count();
|
|
let open_parens = buffer.chars().filter(|c| *c == '(').count();
|
|
let close_parens = buffer.chars().filter(|c| *c == ')').count();
|
|
|
|
if open_braces > close_braces || open_parens > close_parens {
|
|
continuation = true;
|
|
continue;
|
|
}
|
|
|
|
continuation = false;
|
|
let input = std::mem::take(&mut buffer);
|
|
|
|
if input.trim().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Track new definitions for completion
|
|
if let Some(name) = extract_definition_name(&input) {
|
|
rl.helper_mut().unwrap().add_definition(&name);
|
|
}
|
|
|
|
eval_input(&input, &mut interp, &mut checker);
|
|
}
|
|
Err(ReadlineError::Interrupted) => {
|
|
// Ctrl-C: clear buffer
|
|
buffer.clear();
|
|
continuation = false;
|
|
println!("^C");
|
|
}
|
|
Err(ReadlineError::Eof) => {
|
|
// Ctrl-D: exit
|
|
break;
|
|
}
|
|
Err(err) => {
|
|
eprintln!("Error: {:?}", err);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save history
|
|
if let Some(history_path) = get_history_path() {
|
|
let _ = rl.save_history(&history_path);
|
|
}
|
|
|
|
println!("\nGoodbye!");
|
|
}
|
|
|
|
fn extract_definition_name(input: &str) -> Option<String> {
|
|
let input = input.trim();
|
|
if input.starts_with("fn ") {
|
|
input.split_whitespace().nth(1).and_then(|s| {
|
|
s.split('(').next().map(String::from)
|
|
})
|
|
} else if input.starts_with("let ") {
|
|
input.split_whitespace().nth(1).and_then(|s| {
|
|
s.split([':', '=']).next().map(|s| s.trim().to_string())
|
|
})
|
|
} else if input.starts_with("type ") {
|
|
input.split_whitespace().nth(1).and_then(|s| {
|
|
s.split(['<', '=']).next().map(|s| s.trim().to_string())
|
|
})
|
|
} else if input.starts_with("effect ") {
|
|
input.split_whitespace().nth(1).map(|s| {
|
|
s.trim_end_matches('{').trim().to_string()
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn handle_command(
|
|
line: &str,
|
|
interp: &mut Interpreter,
|
|
checker: &mut TypeChecker,
|
|
helper: &mut LuxHelper,
|
|
) {
|
|
let parts: Vec<&str> = line.splitn(2, ' ').collect();
|
|
let cmd = parts[0];
|
|
let arg = parts.get(1).map(|s| s.trim());
|
|
|
|
match cmd {
|
|
":help" | ":h" => {
|
|
println!("{}", HELP);
|
|
}
|
|
":quit" | ":q" => {
|
|
println!("Goodbye!");
|
|
std::process::exit(0);
|
|
}
|
|
":type" | ":t" => {
|
|
if let Some(expr_str) = arg {
|
|
show_type(expr_str, checker);
|
|
} else {
|
|
println!("Usage: :type <expression>");
|
|
}
|
|
}
|
|
":info" | ":i" => {
|
|
if let Some(name) = arg {
|
|
show_info(name, checker);
|
|
} else {
|
|
println!("Usage: :info <name>");
|
|
}
|
|
}
|
|
":env" => {
|
|
show_environment(checker, helper);
|
|
}
|
|
":clear" => {
|
|
*interp = Interpreter::new();
|
|
*checker = TypeChecker::new();
|
|
helper.user_defined.clear();
|
|
println!("Environment cleared.");
|
|
}
|
|
":load" | ":l" => {
|
|
if let Some(path) = arg {
|
|
load_file(path, interp, checker, helper);
|
|
} else {
|
|
println!("Usage: :load <filename>");
|
|
}
|
|
}
|
|
":trace" => match arg {
|
|
Some("on") => {
|
|
interp.enable_tracing();
|
|
println!("Effect tracing enabled.");
|
|
}
|
|
Some("off") => {
|
|
interp.trace_effects = false;
|
|
println!("Effect tracing disabled.");
|
|
}
|
|
_ => {
|
|
println!("Usage: :trace on|off");
|
|
}
|
|
},
|
|
":traces" => {
|
|
if interp.get_traces().is_empty() {
|
|
println!("No effect traces recorded. Use :trace on to enable tracing.");
|
|
} else {
|
|
interp.print_traces();
|
|
}
|
|
}
|
|
":doc" | ":d" => {
|
|
if let Some(name) = arg {
|
|
show_doc(name);
|
|
} else {
|
|
println!("Usage: :doc <name>");
|
|
}
|
|
}
|
|
":browse" | ":b" => {
|
|
if let Some(module) = arg {
|
|
browse_module(module);
|
|
} else {
|
|
println!("Usage: :browse <module>");
|
|
println!("Available modules: List, String, Option, Result, Console, Random, File, Http");
|
|
}
|
|
}
|
|
_ => {
|
|
println!("Unknown command: {}", cmd);
|
|
println!("Type :help for help");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn show_info(name: &str, checker: &TypeChecker) {
|
|
// Look up in the type environment
|
|
if let Some(scheme) = checker.lookup(name) {
|
|
println!("{} : {}", name, scheme);
|
|
} else {
|
|
println!("'{}' is not defined.", name);
|
|
}
|
|
}
|
|
|
|
fn show_environment(checker: &TypeChecker, helper: &LuxHelper) {
|
|
println!("User-defined bindings:");
|
|
if helper.user_defined.is_empty() {
|
|
println!(" (none)");
|
|
} else {
|
|
for name in &helper.user_defined {
|
|
if let Some(scheme) = checker.lookup(name) {
|
|
println!(" {} : {}", name, scheme);
|
|
} else {
|
|
println!(" {} : <unknown>", name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn show_doc(name: &str) {
|
|
// Built-in documentation for common functions
|
|
let doc = match name {
|
|
// List functions
|
|
"List.length" => Some(("List.length : List<T> -> Int", "Returns the number of elements in a list.")),
|
|
"List.head" => Some(("List.head : List<T> -> Option<T>", "Returns the first element of a list, or None if empty.")),
|
|
"List.tail" => Some(("List.tail : List<T> -> Option<List<T>>", "Returns all elements except the first, or None if empty.")),
|
|
"List.map" => Some(("List.map : (List<A>, fn(A) -> B) -> List<B>", "Applies a function to each element, returning a new list.")),
|
|
"List.filter" => Some(("List.filter : (List<T>, fn(T) -> Bool) -> List<T>", "Returns elements for which the predicate returns true.")),
|
|
"List.fold" => Some(("List.fold : (List<T>, U, fn(U, T) -> U) -> U", "Reduces a list to a single value using an accumulator function.")),
|
|
"List.reverse" => Some(("List.reverse : List<T> -> List<T>", "Returns a new list with elements in reverse order.")),
|
|
"List.concat" => Some(("List.concat : (List<T>, List<T>) -> List<T>", "Concatenates two lists.")),
|
|
"List.take" => Some(("List.take : (List<T>, Int) -> List<T>", "Returns the first n elements.")),
|
|
"List.drop" => Some(("List.drop : (List<T>, Int) -> List<T>", "Returns all elements after the first n.")),
|
|
"List.get" => Some(("List.get : (List<T>, Int) -> Option<T>", "Returns the element at index n, or None if out of bounds.")),
|
|
"List.contains" => Some(("List.contains : (List<T>, T) -> Bool", "Returns true if the list contains the element.")),
|
|
"List.all" => Some(("List.all : (List<T>, fn(T) -> Bool) -> Bool", "Returns true if all elements satisfy the predicate.")),
|
|
"List.any" => Some(("List.any : (List<T>, fn(T) -> Bool) -> Bool", "Returns true if any element satisfies the predicate.")),
|
|
"List.find" => Some(("List.find : (List<T>, fn(T) -> Bool) -> Option<T>", "Returns the first element satisfying the predicate.")),
|
|
"List.sort" => Some(("List.sort : List<Int> -> List<Int>", "Sorts a list of integers in ascending order.")),
|
|
"List.range" => Some(("List.range : (Int, Int) -> List<Int>", "Creates a list of integers from start to end (exclusive).")),
|
|
|
|
// String functions
|
|
"String.length" => Some(("String.length : String -> Int", "Returns the number of characters in a string.")),
|
|
"String.concat" => Some(("String.concat : (String, String) -> String", "Concatenates two strings.")),
|
|
"String.substring" => Some(("String.substring : (String, Int, Int) -> String", "Returns a substring from start to end index.")),
|
|
"String.split" => Some(("String.split : (String, String) -> List<String>", "Splits a string by a delimiter.")),
|
|
"String.join" => Some(("String.join : (List<String>, String) -> String", "Joins a list of strings with a delimiter.")),
|
|
"String.trim" => Some(("String.trim : String -> String", "Removes leading and trailing whitespace.")),
|
|
"String.contains" => Some(("String.contains : (String, String) -> Bool", "Returns true if the string contains the substring.")),
|
|
"String.replace" => Some(("String.replace : (String, String, String) -> String", "Replaces all occurrences of a pattern.")),
|
|
"String.startsWith" => Some(("String.startsWith : (String, String) -> Bool", "Returns true if the string starts with the prefix.")),
|
|
"String.endsWith" => Some(("String.endsWith : (String, String) -> Bool", "Returns true if the string ends with the suffix.")),
|
|
"String.toUpper" => Some(("String.toUpper : String -> String", "Converts to uppercase.")),
|
|
"String.toLower" => Some(("String.toLower : String -> String", "Converts to lowercase.")),
|
|
|
|
// Option functions
|
|
"Option.map" => Some(("Option.map : (Option<A>, fn(A) -> B) -> Option<B>", "Applies a function to the value inside Some, or returns None.")),
|
|
"Option.flatMap" => Some(("Option.flatMap : (Option<A>, fn(A) -> Option<B>) -> Option<B>", "Like map, but the function returns an Option.")),
|
|
"Option.getOrElse" => Some(("Option.getOrElse : (Option<T>, T) -> T", "Returns the value inside Some, or the default if None.")),
|
|
"Option.isSome" => Some(("Option.isSome : Option<T> -> Bool", "Returns true if the value is Some.")),
|
|
"Option.isNone" => Some(("Option.isNone : Option<T> -> Bool", "Returns true if the value is None.")),
|
|
|
|
// Result functions
|
|
"Result.map" => Some(("Result.map : (Result<A, E>, fn(A) -> B) -> Result<B, E>", "Applies a function to the Ok value.")),
|
|
"Result.flatMap" => Some(("Result.flatMap : (Result<A, E>, fn(A) -> Result<B, E>) -> Result<B, E>", "Like map, but the function returns a Result.")),
|
|
"Result.getOrElse" => Some(("Result.getOrElse : (Result<T, E>, T) -> T", "Returns the Ok value, or the default if Err.")),
|
|
"Result.isOk" => Some(("Result.isOk : Result<T, E> -> Bool", "Returns true if the value is Ok.")),
|
|
"Result.isErr" => Some(("Result.isErr : Result<T, E> -> Bool", "Returns true if the value is Err.")),
|
|
|
|
// Effects
|
|
"Console.print" => Some(("Console.print : String -> Unit", "Prints a string to the console.")),
|
|
"Console.readLine" => Some(("Console.readLine : () -> String", "Reads a line from standard input.")),
|
|
"Random.int" => Some(("Random.int : (Int, Int) -> Int", "Generates a random integer in the range [min, max].")),
|
|
"Random.float" => Some(("Random.float : () -> Float", "Generates a random float in [0, 1).")),
|
|
"Random.bool" => Some(("Random.bool : () -> Bool", "Generates a random boolean.")),
|
|
"File.read" => Some(("File.read : String -> String", "Reads the contents of a file.")),
|
|
"File.write" => Some(("File.write : (String, String) -> Unit", "Writes content to a file.")),
|
|
"File.exists" => Some(("File.exists : String -> Bool", "Returns true if the file exists.")),
|
|
"Http.get" => Some(("Http.get : String -> String", "Performs an HTTP GET request.")),
|
|
"Http.post" => Some(("Http.post : (String, String) -> String", "Performs an HTTP POST request with a body.")),
|
|
"Time.now" => Some(("Time.now : () -> Int", "Returns the current Unix timestamp in milliseconds.")),
|
|
"Sql.open" => Some(("Sql.open : String -> SqlConn", "Opens a SQLite database file.")),
|
|
"Sql.openMemory" => Some(("Sql.openMemory : () -> SqlConn", "Opens an in-memory SQLite database.")),
|
|
"Sql.query" => Some(("Sql.query : (SqlConn, String) -> List<Row>", "Executes a SQL query and returns all rows.")),
|
|
"Sql.execute" => Some(("Sql.execute : (SqlConn, String) -> Int", "Executes a SQL statement and returns affected rows.")),
|
|
"Postgres.connect" => Some(("Postgres.connect : String -> Int", "Connects to a PostgreSQL database.")),
|
|
"Postgres.query" => Some(("Postgres.query : (Int, String) -> List<Row>", "Executes a SQL query on PostgreSQL.")),
|
|
"Postgres.execute" => Some(("Postgres.execute : (Int, String) -> Int", "Executes a SQL statement on PostgreSQL.")),
|
|
|
|
// Language constructs
|
|
"fn" => Some(("fn name(args): Type = body", "Defines a function.")),
|
|
"let" => Some(("let name = value", "Binds a value to a name.")),
|
|
"if" => Some(("if condition then expr1 else expr2", "Conditional expression.")),
|
|
"match" => Some(("match value { pattern => expr, ... }", "Pattern matching expression.")),
|
|
"effect" => Some(("effect Name { fn op(...): Type }", "Defines an effect with operations.")),
|
|
"handler" => Some(("handler { op => body }", "Defines an effect handler.")),
|
|
"with" => Some(("fn f(): T with {Effect1, Effect2}", "Declares effects a function may perform.")),
|
|
"type" => Some(("type Name<T> = ...", "Defines a type alias.")),
|
|
"run" => Some(("run expr with { handler }", "Executes an expression with effect handlers.")),
|
|
|
|
_ => None,
|
|
};
|
|
|
|
match doc {
|
|
Some((sig, desc)) => {
|
|
println!("\x1b[1m{}\x1b[0m", sig);
|
|
println!();
|
|
println!(" {}", desc);
|
|
}
|
|
None => {
|
|
println!("No documentation for '{}'", name);
|
|
println!("Try :doc List.map or :browse List");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn browse_module(module: &str) {
|
|
let exports: Vec<(&str, &str)> = match module {
|
|
"List" => vec![
|
|
("length", "List<T> -> Int"),
|
|
("head", "List<T> -> Option<T>"),
|
|
("tail", "List<T> -> Option<List<T>>"),
|
|
("get", "(List<T>, Int) -> Option<T>"),
|
|
("map", "(List<A>, fn(A) -> B) -> List<B>"),
|
|
("filter", "(List<T>, fn(T) -> Bool) -> List<T>"),
|
|
("fold", "(List<T>, U, fn(U, T) -> U) -> U"),
|
|
("reverse", "List<T> -> List<T>"),
|
|
("concat", "(List<T>, List<T>) -> List<T>"),
|
|
("take", "(List<T>, Int) -> List<T>"),
|
|
("drop", "(List<T>, Int) -> List<T>"),
|
|
("contains", "(List<T>, T) -> Bool"),
|
|
("all", "(List<T>, fn(T) -> Bool) -> Bool"),
|
|
("any", "(List<T>, fn(T) -> Bool) -> Bool"),
|
|
("find", "(List<T>, fn(T) -> Bool) -> Option<T>"),
|
|
("sort", "List<Int> -> List<Int>"),
|
|
("range", "(Int, Int) -> List<Int>"),
|
|
("forEach", "(List<T>, fn(T) -> Unit) -> Unit"),
|
|
],
|
|
"String" => vec![
|
|
("length", "String -> Int"),
|
|
("concat", "(String, String) -> String"),
|
|
("substring", "(String, Int, Int) -> String"),
|
|
("split", "(String, String) -> List<String>"),
|
|
("join", "(List<String>, String) -> String"),
|
|
("trim", "String -> String"),
|
|
("contains", "(String, String) -> Bool"),
|
|
("replace", "(String, String, String) -> String"),
|
|
("startsWith", "(String, String) -> Bool"),
|
|
("endsWith", "(String, String) -> Bool"),
|
|
("toUpper", "String -> String"),
|
|
("toLower", "String -> String"),
|
|
("lines", "String -> List<String>"),
|
|
],
|
|
"Option" => vec![
|
|
("map", "(Option<A>, fn(A) -> B) -> Option<B>"),
|
|
("flatMap", "(Option<A>, fn(A) -> Option<B>) -> Option<B>"),
|
|
("getOrElse", "(Option<T>, T) -> T"),
|
|
("isSome", "Option<T> -> Bool"),
|
|
("isNone", "Option<T> -> Bool"),
|
|
],
|
|
"Result" => vec![
|
|
("map", "(Result<A, E>, fn(A) -> B) -> Result<B, E>"),
|
|
("flatMap", "(Result<A, E>, fn(A) -> Result<B, E>) -> Result<B, E>"),
|
|
("getOrElse", "(Result<T, E>, T) -> T"),
|
|
("isOk", "Result<T, E> -> Bool"),
|
|
("isErr", "Result<T, E> -> Bool"),
|
|
],
|
|
"Console" => vec![
|
|
("print", "String -> Unit"),
|
|
("readLine", "() -> String"),
|
|
],
|
|
"Random" => vec![
|
|
("int", "(Int, Int) -> Int"),
|
|
("float", "() -> Float"),
|
|
("bool", "() -> Bool"),
|
|
],
|
|
"File" => vec![
|
|
("read", "String -> String"),
|
|
("write", "(String, String) -> Unit"),
|
|
("exists", "String -> Bool"),
|
|
("delete", "String -> Unit"),
|
|
],
|
|
"Http" => vec![
|
|
("get", "String -> String"),
|
|
("post", "(String, String) -> String"),
|
|
],
|
|
"Time" => vec![
|
|
("now", "() -> Int"),
|
|
],
|
|
"Sql" => vec![
|
|
("open", "String -> SqlConn"),
|
|
("openMemory", "() -> SqlConn"),
|
|
("close", "SqlConn -> Unit"),
|
|
("query", "(SqlConn, String) -> List<Row>"),
|
|
("queryOne", "(SqlConn, String) -> Option<Row>"),
|
|
("execute", "(SqlConn, String) -> Int"),
|
|
("beginTx", "SqlConn -> Unit"),
|
|
("commit", "SqlConn -> Unit"),
|
|
("rollback", "SqlConn -> Unit"),
|
|
],
|
|
"Postgres" => vec![
|
|
("connect", "String -> Int"),
|
|
("close", "Int -> Unit"),
|
|
("query", "(Int, String) -> List<Row>"),
|
|
("queryOne", "(Int, String) -> Option<Row>"),
|
|
("execute", "(Int, String) -> Int"),
|
|
("beginTx", "Int -> Unit"),
|
|
("commit", "Int -> Unit"),
|
|
("rollback", "Int -> Unit"),
|
|
],
|
|
"Test" => vec![
|
|
("assert", "(Bool, String) -> Unit"),
|
|
("assertEqual", "(T, T) -> Unit"),
|
|
("assertTrue", "Bool -> Unit"),
|
|
("assertFalse", "Bool -> Unit"),
|
|
("fail", "String -> Unit"),
|
|
],
|
|
_ => {
|
|
println!("Unknown module: {}", module);
|
|
println!("Available modules: List, String, Option, Result, Console, Random, File, Http, Time, Sql, Postgres, Test");
|
|
return;
|
|
}
|
|
};
|
|
|
|
println!("\x1b[1mmodule {}\x1b[0m", module);
|
|
println!();
|
|
for (name, sig) in exports {
|
|
println!(" \x1b[34m{}.{}\x1b[0m : {}", module, name, sig);
|
|
}
|
|
}
|
|
|
|
fn show_type(expr_str: &str, checker: &mut TypeChecker) {
|
|
// Wrap expression in a let to parse it
|
|
let wrapped = format!("let _expr_ = {}", expr_str);
|
|
|
|
match Parser::parse_source(&wrapped) {
|
|
Ok(program) => {
|
|
if let Err(errors) = checker.check_program(&program) {
|
|
for error in errors {
|
|
println!("Type error: {}", error);
|
|
}
|
|
} else {
|
|
println!("(type checking passed)");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
println!("Parse error: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn load_file(path: &str, interp: &mut Interpreter, checker: &mut TypeChecker, helper: &mut LuxHelper) {
|
|
let source = match std::fs::read_to_string(path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
println!("Error reading file '{}': {}", path, e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let program = match Parser::parse_source(&source) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
println!("Parse error: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
if let Err(errors) = checker.check_program(&program) {
|
|
for error in errors {
|
|
println!("Type error: {}", error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Add definitions for completion
|
|
for decl in &program.declarations {
|
|
if let Some(name) = extract_definition_name(&format!("{:?}", decl)) {
|
|
helper.add_definition(&name);
|
|
}
|
|
}
|
|
|
|
match interp.run(&program) {
|
|
Ok(_) => println!("Loaded '{}'", path),
|
|
Err(e) => println!("Runtime error: {}", e),
|
|
}
|
|
}
|
|
|
|
fn eval_input(input: &str, interp: &mut Interpreter, checker: &mut TypeChecker) {
|
|
// Try to parse as a program (declarations)
|
|
match Parser::parse_source(input) {
|
|
Ok(program) => {
|
|
// Type check
|
|
if let Err(errors) = checker.check_program(&program) {
|
|
for error in errors {
|
|
println!("Type error: {}", error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Execute
|
|
match interp.run(&program) {
|
|
Ok(value) => {
|
|
if !matches!(value, interpreter::Value::Unit) {
|
|
println!("{}", value);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
println!("Runtime error: {}", e);
|
|
}
|
|
}
|
|
}
|
|
Err(parse_err) => {
|
|
// Try wrapping as an expression
|
|
let wrapped = format!("let _result_ = {}", input.trim());
|
|
match Parser::parse_source(&wrapped) {
|
|
Ok(program) => {
|
|
if let Err(errors) = checker.check_program(&program) {
|
|
for error in errors {
|
|
println!("Type error: {}", error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
match interp.run(&program) {
|
|
Ok(value) => {
|
|
println!("{}", value);
|
|
}
|
|
Err(e) => {
|
|
println!("Runtime error: {}", e);
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
// Use original error
|
|
println!("Parse error: {}", parse_err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn eval(source: &str) -> Result<String, String> {
|
|
let program = Parser::parse_source(source).map_err(|e| e.to_string())?;
|
|
|
|
let mut checker = TypeChecker::new();
|
|
checker.check_program(&program).map_err(|errors| {
|
|
errors
|
|
.iter()
|
|
.map(|e| e.to_string())
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
})?;
|
|
|
|
let mut interp = Interpreter::new();
|
|
let value = interp.run(&program).map_err(|e| e.to_string())?;
|
|
Ok(format!("{}", value))
|
|
}
|
|
|
|
#[test]
|
|
fn test_arithmetic() {
|
|
assert_eq!(eval("let x = 1 + 2").unwrap(), "3");
|
|
assert_eq!(eval("let x = 10 - 3").unwrap(), "7");
|
|
assert_eq!(eval("let x = 4 * 5").unwrap(), "20");
|
|
assert_eq!(eval("let x = 15 / 3").unwrap(), "5");
|
|
}
|
|
|
|
#[test]
|
|
fn test_function() {
|
|
let source = r#"
|
|
fn add(a: Int, b: Int): Int = a + b
|
|
let result = add(3, 4)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "7");
|
|
}
|
|
|
|
#[test]
|
|
fn test_if_expr() {
|
|
let source = r#"
|
|
fn max(a: Int, b: Int): Int = if a > b then a else b
|
|
let result = max(5, 3)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "5");
|
|
}
|
|
|
|
#[test]
|
|
fn test_recursion() {
|
|
let source = r#"
|
|
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
|
let result = factorial(5)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "120");
|
|
}
|
|
|
|
#[test]
|
|
fn test_lambda() {
|
|
let source = r#"
|
|
let double = fn(x: Int): Int => x * 2
|
|
let result = double(21)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_records() {
|
|
let source = r#"
|
|
let person = { name: "Alice", age: 30 }
|
|
let result = person.age
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "30");
|
|
}
|
|
|
|
#[test]
|
|
fn test_lists() {
|
|
let source = "let nums = [1, 2, 3]";
|
|
assert_eq!(eval(source).unwrap(), "[1, 2, 3]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tuples() {
|
|
let source = "let pair = (42, \"hello\")";
|
|
assert_eq!(eval(source).unwrap(), "(42, \"hello\")");
|
|
}
|
|
|
|
#[test]
|
|
fn test_block() {
|
|
let source = r#"
|
|
let result = {
|
|
let x = 10
|
|
let y = 20
|
|
x + y
|
|
}
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "30");
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe() {
|
|
let source = r#"
|
|
fn double(x: Int): Int = x * 2
|
|
fn add_one(x: Int): Int = x + 1
|
|
let result = 5 |> double |> add_one
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "11");
|
|
}
|
|
|
|
// ============ Standard Library Tests ============
|
|
|
|
// List tests
|
|
#[test]
|
|
fn test_list_length() {
|
|
assert_eq!(eval("let x = List.length([1, 2, 3])").unwrap(), "3");
|
|
assert_eq!(eval("let x = List.length([])").unwrap(), "0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_reverse() {
|
|
assert_eq!(
|
|
eval("let x = List.reverse([1, 2, 3])").unwrap(),
|
|
"[3, 2, 1]"
|
|
);
|
|
assert_eq!(eval("let x = List.reverse([])").unwrap(), "[]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_range() {
|
|
assert_eq!(eval("let x = List.range(0, 5)").unwrap(), "[0, 1, 2, 3, 4]");
|
|
assert_eq!(eval("let x = List.range(3, 3)").unwrap(), "[]");
|
|
assert_eq!(eval("let x = List.range(-2, 2)").unwrap(), "[-2, -1, 0, 1]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_head() {
|
|
assert_eq!(eval("let x = List.head([1, 2, 3])").unwrap(), "Some(1)");
|
|
assert_eq!(eval("let x = List.head([])").unwrap(), "None");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_tail() {
|
|
assert_eq!(
|
|
eval("let x = List.tail([1, 2, 3])").unwrap(),
|
|
"Some([2, 3])"
|
|
);
|
|
assert_eq!(eval("let x = List.tail([1])").unwrap(), "Some([])");
|
|
assert_eq!(eval("let x = List.tail([])").unwrap(), "None");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_concat() {
|
|
assert_eq!(
|
|
eval("let x = List.concat([1, 2], [3, 4])").unwrap(),
|
|
"[1, 2, 3, 4]"
|
|
);
|
|
assert_eq!(eval("let x = List.concat([], [1])").unwrap(), "[1]");
|
|
assert_eq!(eval("let x = List.concat([1], [])").unwrap(), "[1]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_get() {
|
|
assert_eq!(
|
|
eval("let x = List.get([10, 20, 30], 0)").unwrap(),
|
|
"Some(10)"
|
|
);
|
|
assert_eq!(
|
|
eval("let x = List.get([10, 20, 30], 2)").unwrap(),
|
|
"Some(30)"
|
|
);
|
|
assert_eq!(eval("let x = List.get([10, 20, 30], 5)").unwrap(), "None");
|
|
assert_eq!(eval("let x = List.get([10, 20, 30], -1)").unwrap(), "None");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_map() {
|
|
let source = r#"
|
|
fn double(x: Int): Int = x * 2
|
|
let result = List.map([1, 2, 3], double)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "[2, 4, 6]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_map_lambda() {
|
|
let source = "let x = List.map([1, 2, 3], fn(x: Int): Int => x * x)";
|
|
assert_eq!(eval(source).unwrap(), "[1, 4, 9]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_filter() {
|
|
let source = "let x = List.filter([1, 2, 3, 4, 5], fn(x: Int): Bool => x > 2)";
|
|
assert_eq!(eval(source).unwrap(), "[3, 4, 5]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_filter_all() {
|
|
let source = "let x = List.filter([1, 2, 3], fn(x: Int): Bool => x > 10)";
|
|
assert_eq!(eval(source).unwrap(), "[]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_fold() {
|
|
let source = "let x = List.fold([1, 2, 3, 4], 0, fn(acc: Int, x: Int): Int => acc + x)";
|
|
assert_eq!(eval(source).unwrap(), "10");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_fold_product() {
|
|
let source = "let x = List.fold([1, 2, 3, 4], 1, fn(acc: Int, x: Int): Int => acc * x)";
|
|
assert_eq!(eval(source).unwrap(), "24");
|
|
}
|
|
|
|
// String tests
|
|
#[test]
|
|
fn test_string_length() {
|
|
assert_eq!(eval(r#"let x = String.length("hello")"#).unwrap(), "5");
|
|
assert_eq!(eval(r#"let x = String.length("")"#).unwrap(), "0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_split() {
|
|
assert_eq!(
|
|
eval(r#"let x = String.split("a,b,c", ",")"#).unwrap(),
|
|
r#"["a", "b", "c"]"#
|
|
);
|
|
assert_eq!(
|
|
eval(r#"let x = String.split("hello", ",")"#).unwrap(),
|
|
r#"["hello"]"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_join() {
|
|
assert_eq!(
|
|
eval(r#"let x = String.join(["a", "b", "c"], "-")"#).unwrap(),
|
|
r#""a-b-c""#
|
|
);
|
|
assert_eq!(
|
|
eval(r#"let x = String.join(["hello"], ",")"#).unwrap(),
|
|
r#""hello""#
|
|
);
|
|
assert_eq!(eval(r#"let x = String.join([], ",")"#).unwrap(), r#""""#);
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_trim() {
|
|
assert_eq!(
|
|
eval(r#"let x = String.trim(" hello ")"#).unwrap(),
|
|
r#""hello""#
|
|
);
|
|
assert_eq!(
|
|
eval(r#"let x = String.trim("hello")"#).unwrap(),
|
|
r#""hello""#
|
|
);
|
|
assert_eq!(eval(r#"let x = String.trim(" ")"#).unwrap(), r#""""#);
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_contains() {
|
|
assert_eq!(
|
|
eval(r#"let x = String.contains("hello world", "world")"#).unwrap(),
|
|
"true"
|
|
);
|
|
assert_eq!(
|
|
eval(r#"let x = String.contains("hello", "xyz")"#).unwrap(),
|
|
"false"
|
|
);
|
|
assert_eq!(
|
|
eval(r#"let x = String.contains("hello", "")"#).unwrap(),
|
|
"true"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_replace() {
|
|
assert_eq!(
|
|
eval(r#"let x = String.replace("hello", "l", "L")"#).unwrap(),
|
|
r#""heLLo""#
|
|
);
|
|
assert_eq!(
|
|
eval(r#"let x = String.replace("aaa", "a", "b")"#).unwrap(),
|
|
r#""bbb""#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_chars() {
|
|
assert_eq!(eval(r#"let x = String.chars("hi")"#).unwrap(), "['h', 'i']");
|
|
assert_eq!(eval(r#"let x = String.chars("")"#).unwrap(), "[]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_lines() {
|
|
// Note: Using actual newline in the string
|
|
let source = r#"let x = String.lines("a
|
|
b
|
|
c")"#;
|
|
assert_eq!(eval(source).unwrap(), r#"["a", "b", "c"]"#);
|
|
}
|
|
|
|
// String interpolation tests
|
|
#[test]
|
|
fn test_string_interpolation_simple() {
|
|
let source = r#"
|
|
let name = "World"
|
|
let x = "Hello, {name}!"
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), r#""Hello, World!""#);
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_interpolation_numbers() {
|
|
let source = r#"
|
|
let n = 42
|
|
let x = "The answer is {n}"
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), r#""The answer is 42""#);
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_interpolation_expressions() {
|
|
let source = r#"
|
|
let a = 2
|
|
let b = 3
|
|
let x = "{a} + {b} = {a + b}"
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), r#""2 + 3 = 5""#);
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_interpolation_escaped_braces() {
|
|
let source = r#"let x = "literal \{braces\}""#;
|
|
assert_eq!(eval(source).unwrap(), r#""literal {braces}""#);
|
|
}
|
|
|
|
// Option tests
|
|
#[test]
|
|
fn test_option_constructors() {
|
|
assert_eq!(eval("let x = Some(42)").unwrap(), "Some(42)");
|
|
assert_eq!(eval("let x = None").unwrap(), "None");
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_is_some() {
|
|
assert_eq!(eval("let x = Option.isSome(Some(42))").unwrap(), "true");
|
|
assert_eq!(eval("let x = Option.isSome(None)").unwrap(), "false");
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_is_none() {
|
|
assert_eq!(eval("let x = Option.isNone(None)").unwrap(), "true");
|
|
assert_eq!(eval("let x = Option.isNone(Some(42))").unwrap(), "false");
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_get_or_else() {
|
|
assert_eq!(eval("let x = Option.getOrElse(Some(42), 0)").unwrap(), "42");
|
|
assert_eq!(eval("let x = Option.getOrElse(None, 0)").unwrap(), "0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_map() {
|
|
let source = "let x = Option.map(Some(5), fn(x: Int): Int => x * 2)";
|
|
assert_eq!(eval(source).unwrap(), "Some(10)");
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_map_none() {
|
|
let source = "let x = Option.map(None, fn(x: Int): Int => x * 2)";
|
|
assert_eq!(eval(source).unwrap(), "None");
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_flat_map() {
|
|
let source = "let x = Option.flatMap(Some(5), fn(x: Int): Option<Int> => Some(x * 2))";
|
|
assert_eq!(eval(source).unwrap(), "Some(10)");
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_flat_map_to_none() {
|
|
let source = "let x = Option.flatMap(Some(5), fn(x: Int): Option<Int> => None)";
|
|
assert_eq!(eval(source).unwrap(), "None");
|
|
}
|
|
|
|
// Result tests
|
|
#[test]
|
|
fn test_result_constructors() {
|
|
assert_eq!(eval("let x = Ok(42)").unwrap(), "Ok(42)");
|
|
assert_eq!(eval(r#"let x = Err("error")"#).unwrap(), r#"Err("error")"#);
|
|
}
|
|
|
|
#[test]
|
|
fn test_result_is_ok() {
|
|
assert_eq!(eval("let x = Result.isOk(Ok(42))").unwrap(), "true");
|
|
assert_eq!(eval(r#"let x = Result.isOk(Err("e"))"#).unwrap(), "false");
|
|
}
|
|
|
|
#[test]
|
|
fn test_result_is_err() {
|
|
assert_eq!(eval(r#"let x = Result.isErr(Err("e"))"#).unwrap(), "true");
|
|
assert_eq!(eval("let x = Result.isErr(Ok(42))").unwrap(), "false");
|
|
}
|
|
|
|
#[test]
|
|
fn test_result_get_or_else() {
|
|
assert_eq!(eval("let x = Result.getOrElse(Ok(42), 0)").unwrap(), "42");
|
|
assert_eq!(
|
|
eval(r#"let x = Result.getOrElse(Err("e"), 0)"#).unwrap(),
|
|
"0"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_result_map() {
|
|
let source = "let x = Result.map(Ok(5), fn(x: Int): Int => x * 2)";
|
|
assert_eq!(eval(source).unwrap(), "Ok(10)");
|
|
}
|
|
|
|
#[test]
|
|
fn test_result_map_err() {
|
|
let source = r#"let x = Result.map(Err("e"), fn(x: Int): Int => x * 2)"#;
|
|
assert_eq!(eval(source).unwrap(), r#"Err("e")"#);
|
|
}
|
|
|
|
// Utility function tests
|
|
#[test]
|
|
fn test_to_string() {
|
|
assert_eq!(eval("let x = toString(42)").unwrap(), r#""42""#);
|
|
assert_eq!(eval("let x = toString(true)").unwrap(), r#""true""#);
|
|
assert_eq!(eval("let x = toString([1, 2])").unwrap(), r#""[1, 2]""#);
|
|
}
|
|
|
|
#[test]
|
|
fn test_type_of() {
|
|
assert_eq!(eval("let x = typeOf(42)").unwrap(), r#""Int""#);
|
|
assert_eq!(eval("let x = typeOf(true)").unwrap(), r#""Bool""#);
|
|
assert_eq!(eval("let x = typeOf([1, 2])").unwrap(), r#""List""#);
|
|
assert_eq!(eval(r#"let x = typeOf("hello")"#).unwrap(), r#""String""#);
|
|
}
|
|
|
|
// Bug fix tests
|
|
#[test]
|
|
fn test_record_equality() {
|
|
assert_eq!(eval("let x = { a: 1, b: 2 } == { a: 1, b: 2 }").unwrap(), "true");
|
|
assert_eq!(eval("let x = { a: 1 } == { a: 2 }").unwrap(), "false");
|
|
assert_eq!(eval("let x = { a: 1, b: 2 } == { a: 1, b: 3 }").unwrap(), "false");
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_escape_sequence() {
|
|
let result = eval(r#"let x = "\z""#);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("Invalid escape sequence"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_unknown_effect_error() {
|
|
let result = eval("fn test(): Unit with {FakeEffect} = ()");
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("Unknown effect"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_circular_type_definitions() {
|
|
let result = eval("type A = B\ntype B = A");
|
|
assert!(result.is_err(), "Should detect circular type definitions");
|
|
let err = result.unwrap_err();
|
|
assert!(err.contains("Circular"), "Error should mention 'Circular': {}", err);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mutual_recursion() {
|
|
let source = r#"
|
|
fn isEven(n: Int): Bool = if n == 0 then true else isOdd(n - 1)
|
|
fn isOdd(n: Int): Bool = if n == 0 then false else isEven(n - 1)
|
|
let result = isEven(10)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "true");
|
|
}
|
|
|
|
// Pipe with stdlib tests
|
|
#[test]
|
|
fn test_pipe_with_list() {
|
|
assert_eq!(
|
|
eval("let x = [1, 2, 3] |> List.reverse").unwrap(),
|
|
"[3, 2, 1]"
|
|
);
|
|
assert_eq!(eval("let x = [1, 2, 3] |> List.length").unwrap(), "3");
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_with_string() {
|
|
assert_eq!(
|
|
eval(r#"let x = " hello " |> String.trim"#).unwrap(),
|
|
r#""hello""#
|
|
);
|
|
}
|
|
|
|
// Combined stdlib usage tests
|
|
#[test]
|
|
fn test_list_filter_even() {
|
|
let source = r#"
|
|
fn isEven(x: Int): Bool = x % 2 == 0
|
|
let result = List.filter(List.range(1, 6), isEven)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "[2, 4]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_chain() {
|
|
let source = r#"
|
|
fn times10(x: Int): Int = x * 10
|
|
let head = List.head([1, 2, 3])
|
|
let mapped = Option.map(head, times10)
|
|
let result = Option.getOrElse(mapped, 0)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "10");
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_chain_empty() {
|
|
let source = r#"
|
|
fn times10(x: Int): Int = x * 10
|
|
let head = List.head([])
|
|
let mapped = Option.map(head, times10)
|
|
let result = Option.getOrElse(mapped, 0)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "0");
|
|
}
|
|
|
|
// ============ Behavioral Types Tests ============
|
|
|
|
#[test]
|
|
fn test_behavioral_pure_function() {
|
|
// A pure function with no effects should work
|
|
let source = r#"
|
|
fn double(x: Int): Int is pure = x * 2
|
|
let result = double(21)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_total_function() {
|
|
// A total function should work
|
|
let source = r#"
|
|
fn always42(): Int is total = 42
|
|
let result = always42()
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_idempotent_function() {
|
|
// An idempotent function
|
|
let source = r#"
|
|
fn clamp(x: Int): Int is idempotent = if x < 0 then 0 else x
|
|
let result = clamp(clamp(-5))
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_multiple_properties() {
|
|
// A function with multiple properties
|
|
let source = r#"
|
|
fn identity(x: Int): Int is pure, is total = x
|
|
let result = identity(100)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "100");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_deterministic() {
|
|
let source = r#"
|
|
fn square(x: Int): Int is deterministic = x * x
|
|
let result = square(7)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "49");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_pure_with_effects_error() {
|
|
// A pure function with effects should produce a type error
|
|
let source = r#"
|
|
fn bad(x: Int): Int with {Logger} is pure = x
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("pure but has effects"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_deterministic_with_random_error() {
|
|
// A deterministic function cannot use Random effect
|
|
let source = r#"
|
|
fn bad(): Int with {Random} is deterministic = Random.int(1, 100)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("deterministic but uses non-deterministic effects"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_deterministic_with_time_error() {
|
|
// A deterministic function cannot use Time effect
|
|
let source = r#"
|
|
fn bad(): Int with {Time} is deterministic = Time.now()
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("deterministic but uses non-deterministic effects"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_commutative_add() {
|
|
// Addition is commutative
|
|
let source = r#"
|
|
fn add(a: Int, b: Int): Int is commutative = a + b
|
|
let result = add(3, 5)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "8");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_commutative_mul() {
|
|
// Multiplication is commutative
|
|
let source = r#"
|
|
fn mul(a: Int, b: Int): Int is commutative = a * b
|
|
let result = mul(3, 5)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "15");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_commutative_subtract_error() {
|
|
// Subtraction is NOT commutative
|
|
let source = r#"
|
|
fn sub(a: Int, b: Int): Int is commutative = a - b
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("commutative but its body is not"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_commutative_wrong_params_error() {
|
|
// Commutative requires exactly 2 params
|
|
let source = r#"
|
|
fn bad(a: Int): Int is commutative = a
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("has 1 parameters (expected 2)"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_idempotent_identity() {
|
|
// Identity function is idempotent
|
|
let source = r#"
|
|
fn identity(x: Int): Int is idempotent = x
|
|
let result = identity(42)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_idempotent_constant() {
|
|
// Constant function is idempotent
|
|
let source = r#"
|
|
fn always42(x: Int): Int is idempotent = 42
|
|
let result = always42(100)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_idempotent_clamping() {
|
|
// Clamping pattern is idempotent
|
|
let source = r#"
|
|
fn clampPositive(x: Int): Int is idempotent =
|
|
if x < 0 then 0 else x
|
|
let result = clampPositive(-5)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_idempotent_increment_error() {
|
|
// Increment is NOT idempotent
|
|
let source = r#"
|
|
fn increment(x: Int): Int is idempotent = x + 1
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("idempotent but could not be verified"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_total_non_recursive() {
|
|
// Non-recursive function is total
|
|
let source = r#"
|
|
fn double(x: Int): Int is total = x * 2
|
|
let result = double(21)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_total_structural_recursion() {
|
|
// Structural recursion (n - 1) is total
|
|
let source = r#"
|
|
fn factorial(n: Int): Int is total =
|
|
if n <= 1 then 1 else n * factorial(n - 1)
|
|
let result = factorial(5)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "120");
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_total_infinite_loop_error() {
|
|
// Infinite loop is NOT total
|
|
let source = r#"
|
|
fn loop(x: Int): Int is total = loop(x)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("may not terminate"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_total_with_fail_error() {
|
|
// Total function cannot use Fail effect
|
|
let source = r#"
|
|
effect Fail { fn fail(msg: String): Unit }
|
|
fn bad(x: Int): Int with {Fail} is total =
|
|
if x < 0 then { Fail.fail("negative"); 0 } else x
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("total but uses the Fail effect"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_where_clause_parsing() {
|
|
// Where clause property constraints are parsed correctly
|
|
let source = r#"
|
|
fn retry<F>(action: F, times: Int): Int where F is idempotent = times
|
|
fn idempotentFn(): Int is idempotent = 42
|
|
let x = 1
|
|
"#;
|
|
// Just verify it parses and type-checks without errors
|
|
assert!(eval(source).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_where_clause_satisfied() {
|
|
// Where clause should be satisfied when passing an idempotent function
|
|
// Note: Parameter type must be the type parameter F for constraint to apply
|
|
let source = r#"
|
|
fn runIdempotent<F>(action: F): Int where F is idempotent = 42
|
|
fn idempotentFn(): Int is idempotent = 42
|
|
let result = runIdempotent(idempotentFn)
|
|
"#;
|
|
assert!(eval(source).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_where_clause_violation() {
|
|
// Where clause should fail when passing a non-idempotent function
|
|
// The function notIdempotent doesn't declare "is idempotent", so this should fail
|
|
let source = r#"
|
|
fn runIdempotent<F>(action: F): Int where F is idempotent = 42
|
|
fn notIdempotent(): Int = 42
|
|
let result = runIdempotent(notIdempotent)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err(), "Should fail when where clause is violated");
|
|
assert!(result.unwrap_err().contains("property constraint"),
|
|
"Error should mention property constraint");
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_version_preserved() {
|
|
// Version annotations are preserved in type annotations
|
|
let source = r#"
|
|
fn processV1(x: Int @v1): Int = x
|
|
let value: Int @v1 = 42
|
|
let result = processV1(value)
|
|
"#;
|
|
assert!(eval(source).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_version_mismatch_error() {
|
|
// Version mismatch should produce an error when versions don't match
|
|
let source = r#"
|
|
fn expectV2(x: Int @v2): Int = x
|
|
let oldValue: Int @v1 = 42
|
|
let result = expectV2(oldValue)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("Version mismatch"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_version_atleast() {
|
|
// @v2+ should accept v2 or higher
|
|
let source = r#"
|
|
fn processAtLeastV2(x: Int @v2+): Int = x
|
|
let value: Int @v2 = 42
|
|
let result = processAtLeastV2(value)
|
|
"#;
|
|
assert!(eval(source).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_version_latest() {
|
|
// @latest should be compatible with any version
|
|
let source = r#"
|
|
fn processLatest(x: Int @latest): Int = x
|
|
let value: Int @v1 = 42
|
|
let result = processLatest(value)
|
|
"#;
|
|
assert!(eval(source).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_breaking_change_with_migration() {
|
|
// Breaking change with migration should not error
|
|
let source = r#"
|
|
type User @v1 { name: String }
|
|
type User @v2 {
|
|
name: String,
|
|
email: String,
|
|
from @v1 = { name: old.name, email: "unknown@example.com" }
|
|
}
|
|
let x = 1
|
|
"#;
|
|
assert!(eval(source).is_ok(), "Breaking change with migration should be allowed");
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_breaking_change_without_migration() {
|
|
// Breaking change without migration should produce an error
|
|
let source = r#"
|
|
type Config @v1 { host: String }
|
|
type Config @v2 {
|
|
host: String,
|
|
port: Int
|
|
}
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err(), "Breaking change without migration should fail");
|
|
let err = result.unwrap_err();
|
|
assert!(err.contains("Breaking changes") || err.contains("required field"),
|
|
"Error should mention breaking changes: {}", err);
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_compatible_change() {
|
|
// Compatible change (adding optional field) should not error
|
|
// Note: Currently we don't have Option types fully integrated,
|
|
// so this test verifies the basic flow
|
|
let source = r#"
|
|
type Settings @v1 { theme: String }
|
|
type Settings @v2 { theme: String }
|
|
let x = 1
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Compatible change should be allowed: {:?}", result);
|
|
}
|
|
|
|
// Built-in effect tests
|
|
mod effect_tests {
|
|
use crate::interpreter::{Interpreter, Value};
|
|
use crate::parser::Parser;
|
|
use crate::typechecker::TypeChecker;
|
|
|
|
fn run_with_effects(source: &str, initial_state: Value, reader_value: Value) -> Result<(String, String), String> {
|
|
let program = Parser::parse_source(source).map_err(|e| e.to_string())?;
|
|
let mut checker = TypeChecker::new();
|
|
checker.check_program(&program).map_err(|errors| {
|
|
errors.iter().map(|e| e.to_string()).collect::<Vec<_>>().join("\n")
|
|
})?;
|
|
let mut interp = Interpreter::new();
|
|
interp.set_state(initial_state);
|
|
interp.set_reader(reader_value);
|
|
let result = interp.run(&program).map_err(|e| e.to_string())?;
|
|
let final_state = interp.get_state();
|
|
Ok((format!("{}", result), format!("{}", final_state)))
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_get() {
|
|
let source = r#"
|
|
fn getValue(): Int with {State} = State.get()
|
|
let result = run getValue() with {}
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Int(42), Value::Unit).unwrap();
|
|
assert_eq!(result, "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_put() {
|
|
let source = r#"
|
|
fn setValue(x: Int): Unit with {State} = State.put(x)
|
|
let result = run setValue(100) with {}
|
|
"#;
|
|
let (_, final_state) = run_with_effects(source, Value::Int(0), Value::Unit).unwrap();
|
|
assert_eq!(final_state, "100");
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_get_and_put() {
|
|
let source = r#"
|
|
fn increment(): Int with {State} = {
|
|
let current = State.get()
|
|
State.put(current + 1)
|
|
State.get()
|
|
}
|
|
let result = run increment() with {}
|
|
"#;
|
|
let (result, final_state) = run_with_effects(source, Value::Int(10), Value::Unit).unwrap();
|
|
assert_eq!(result, "11");
|
|
assert_eq!(final_state, "11");
|
|
}
|
|
|
|
#[test]
|
|
fn test_reader_ask() {
|
|
let source = r#"
|
|
fn getConfig(): String with {Reader} = Reader.ask()
|
|
let result = run getConfig() with {}
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::String("config_value".to_string())).unwrap();
|
|
// Value's Display includes quotes for strings
|
|
assert_eq!(result, "\"config_value\"");
|
|
}
|
|
|
|
#[test]
|
|
fn test_fail_effect() {
|
|
let source = r#"
|
|
fn failing(): Int with {Fail} = Fail.fail("oops")
|
|
let result = run failing() with {}
|
|
"#;
|
|
let result = run_with_effects(source, Value::Unit, Value::Unit);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("oops"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_combined_effects() {
|
|
let source = r#"
|
|
fn compute(): Int with {State, Reader} = {
|
|
let config = Reader.ask()
|
|
let current = State.get()
|
|
State.put(current + 1)
|
|
current
|
|
}
|
|
let result = run compute() with {}
|
|
"#;
|
|
let (result, final_state) = run_with_effects(source, Value::Int(5), Value::String("test".to_string())).unwrap();
|
|
assert_eq!(result, "5");
|
|
assert_eq!(final_state, "6");
|
|
}
|
|
|
|
#[test]
|
|
fn test_random_int() {
|
|
let source = r#"
|
|
fn getRandomInt(): Int with {Random} = Random.int(1, 10)
|
|
let result = run getRandomInt() with {}
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
let num: i64 = result.parse().expect("Should be an integer");
|
|
assert!(num >= 1 && num <= 10, "Random int should be in range 1-10, got {}", num);
|
|
}
|
|
|
|
#[test]
|
|
fn test_random_float() {
|
|
let source = r#"
|
|
fn getRandomFloat(): Float with {Random} = Random.float()
|
|
let result = run getRandomFloat() with {}
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
let num: f64 = result.parse().expect("Should be a float");
|
|
assert!(num >= 0.0 && num < 1.0, "Random float should be in range [0, 1), got {}", num);
|
|
}
|
|
|
|
#[test]
|
|
fn test_random_bool() {
|
|
let source = r#"
|
|
fn getRandomBool(): Bool with {Random} = Random.bool()
|
|
let result = run getRandomBool() with {}
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
assert!(result == "true" || result == "false", "Random bool should be true or false, got {}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_time_now() {
|
|
let source = r#"
|
|
fn getTime(): Int with {Time} = Time.now()
|
|
let result = run getTime() with {}
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
let timestamp: i64 = result.parse().expect("Should be a timestamp");
|
|
// Timestamp should be a reasonable Unix time in milliseconds (after 2020)
|
|
assert!(timestamp > 1577836800000, "Timestamp should be after 2020");
|
|
}
|
|
|
|
#[test]
|
|
fn test_resumable_handler() {
|
|
// Test that resume works in handler bodies
|
|
let source = r#"
|
|
effect Counter {
|
|
fn increment(): Int
|
|
}
|
|
|
|
fn useCounter(): Int with {Counter} = {
|
|
let a = Counter.increment()
|
|
let b = Counter.increment()
|
|
a + b
|
|
}
|
|
|
|
handler countingHandler: Counter {
|
|
fn increment() = resume(10)
|
|
}
|
|
|
|
let result = run useCounter() with {
|
|
Counter = countingHandler
|
|
}
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
// Each increment returns 10, so a + b = 10 + 10 = 20
|
|
assert_eq!(result, "20");
|
|
}
|
|
|
|
#[test]
|
|
fn test_resume_outside_handler_fails() {
|
|
// Resume outside handler should fail at runtime
|
|
let source = r#"
|
|
fn bad(): Int = resume(42)
|
|
let result = bad()
|
|
"#;
|
|
let result = run_with_effects(source, Value::Unit, Value::Unit);
|
|
assert!(result.is_err());
|
|
let err_msg = result.unwrap_err();
|
|
assert!(err_msg.contains("outside") || err_msg.contains("Resume"),
|
|
"Error should mention resume outside handler: {}", err_msg);
|
|
}
|
|
|
|
// Schema Evolution tests
|
|
#[test]
|
|
fn test_schema_versioned() {
|
|
let source = r#"
|
|
let user = Schema.versioned("User", 1, { name: "Alice", age: 30 })
|
|
let version = Schema.getVersion(user)
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "1");
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_migrate_same_version() {
|
|
let source = r#"
|
|
let user = Schema.versioned("User", 2, { name: "Bob" })
|
|
let migrated = Schema.migrate(user, 2)
|
|
let version = Schema.getVersion(migrated)
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "2");
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_migrate_upgrade() {
|
|
let source = r#"
|
|
let user = Schema.versioned("User", 1, { name: "Charlie" })
|
|
let migrated = Schema.migrate(user, 3)
|
|
let version = Schema.getVersion(migrated)
|
|
"#;
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "3");
|
|
}
|
|
|
|
#[test]
|
|
fn test_schema_migrate_downgrade_fails() {
|
|
let source = r#"
|
|
let user = Schema.versioned("User", 3, { name: "Dave" })
|
|
let migrated = Schema.migrate(user, 1)
|
|
"#;
|
|
let result = run_with_effects(source, Value::Unit, Value::Unit);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("downgrade"));
|
|
}
|
|
|
|
// HttpServer effect type-checking tests
|
|
#[test]
|
|
fn test_http_server_typecheck() {
|
|
// Verify HttpServer effect operations type-check correctly
|
|
// We can't actually run the server in tests, but we can verify types
|
|
use crate::parser::Parser;
|
|
use crate::typechecker::TypeChecker;
|
|
|
|
let source = r#"
|
|
// Function that uses HttpServer effect
|
|
fn handleRequest(req: { method: String, path: String, body: String, headers: List<(String, String)> }): Unit with {HttpServer} =
|
|
HttpServer.respond(200, "Hello!")
|
|
|
|
fn serveOne(port: Int): Unit with {HttpServer} = {
|
|
HttpServer.listen(port)
|
|
let req = HttpServer.accept()
|
|
handleRequest(req)
|
|
HttpServer.stop()
|
|
}
|
|
"#;
|
|
|
|
let program = Parser::parse_source(source).expect("parse failed");
|
|
let mut checker = TypeChecker::new();
|
|
let result = checker.check_program(&program);
|
|
assert!(result.is_ok(), "HttpServer type checking failed: {:?}", result);
|
|
}
|
|
|
|
// Behavioral types tests
|
|
#[test]
|
|
fn test_behavioral_pure_function() {
|
|
use crate::parser::Parser;
|
|
use crate::typechecker::TypeChecker;
|
|
|
|
let source = r#"
|
|
fn add(a: Int, b: Int): Int is pure = a + b
|
|
fn square(x: Int): Int is pure = x * x
|
|
let result = add(square(3), square(4))
|
|
"#;
|
|
let program = Parser::parse_source(source).expect("parse failed");
|
|
let mut checker = TypeChecker::new();
|
|
assert!(checker.check_program(&program).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_deterministic() {
|
|
use crate::parser::Parser;
|
|
use crate::typechecker::TypeChecker;
|
|
|
|
let source = r#"
|
|
fn factorial(n: Int): Int is deterministic =
|
|
if n <= 1 then 1 else n * factorial(n - 1)
|
|
let result = factorial(5)
|
|
"#;
|
|
let program = Parser::parse_source(source).expect("parse failed");
|
|
let mut checker = TypeChecker::new();
|
|
assert!(checker.check_program(&program).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_commutative() {
|
|
use crate::parser::Parser;
|
|
use crate::typechecker::TypeChecker;
|
|
|
|
// Commutative verification works with arithmetic operators
|
|
let source = r#"
|
|
fn add(a: Int, b: Int): Int is commutative = a + b
|
|
fn mul(a: Int, b: Int): Int is commutative = a * b
|
|
let result = add(10, 20)
|
|
"#;
|
|
let program = Parser::parse_source(source).expect("parse failed");
|
|
let mut checker = TypeChecker::new();
|
|
assert!(checker.check_program(&program).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_idempotent() {
|
|
use crate::parser::Parser;
|
|
use crate::typechecker::TypeChecker;
|
|
|
|
// Idempotent verification works with abs pattern
|
|
let source = r#"
|
|
fn absolute(x: Int): Int is idempotent =
|
|
if x < 0 then 0 - x else x
|
|
let result = absolute(-42)
|
|
"#;
|
|
let program = Parser::parse_source(source).expect("parse failed");
|
|
let mut checker = TypeChecker::new();
|
|
assert!(checker.check_program(&program).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_behavioral_total() {
|
|
use crate::parser::Parser;
|
|
use crate::typechecker::TypeChecker;
|
|
|
|
let source = r#"
|
|
fn sumTo(n: Int): Int is total =
|
|
if n <= 0 then 0 else n + sumTo(n - 1)
|
|
fn power(base: Int, exp: Int): Int is total =
|
|
if exp <= 0 then 1 else base * power(base, exp - 1)
|
|
let result = sumTo(10)
|
|
"#;
|
|
let program = Parser::parse_source(source).expect("parse failed");
|
|
let mut checker = TypeChecker::new();
|
|
assert!(checker.check_program(&program).is_ok());
|
|
}
|
|
|
|
// Math module tests
|
|
#[test]
|
|
fn test_math_abs() {
|
|
let (result, _) = run_with_effects("let x = Math.abs(-42)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "42");
|
|
let (result, _) = run_with_effects("let x = Math.abs(42)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "42");
|
|
}
|
|
|
|
#[test]
|
|
fn test_math_min_max() {
|
|
let (result, _) = run_with_effects("let x = Math.min(3, 7)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "3");
|
|
let (result, _) = run_with_effects("let x = Math.max(3, 7)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "7");
|
|
}
|
|
|
|
#[test]
|
|
fn test_math_sqrt() {
|
|
let (result, _) = run_with_effects("let x = Math.sqrt(16)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "4");
|
|
}
|
|
|
|
#[test]
|
|
fn test_math_pow() {
|
|
let (result, _) = run_with_effects("let x = Math.pow(2, 10)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "1024");
|
|
}
|
|
|
|
#[test]
|
|
fn test_math_floor_ceil_round() {
|
|
let (result, _) = run_with_effects("let x = Math.floor(3.7)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "3");
|
|
let (result, _) = run_with_effects("let x = Math.ceil(3.2)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "4");
|
|
let (result, _) = run_with_effects("let x = Math.round(3.5)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "4");
|
|
}
|
|
|
|
// List module additional functions
|
|
#[test]
|
|
fn test_list_is_empty() {
|
|
let (result, _) = run_with_effects("let x = List.isEmpty([])", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "true");
|
|
let (result, _) = run_with_effects("let x = List.isEmpty([1, 2])", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "false");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_find() {
|
|
let source = "let x = List.find([1, 2, 3, 4, 5], fn(x: Int): Bool => x > 3)";
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "Some(4)");
|
|
let source = "let x = List.find([1, 2, 3], fn(x: Int): Bool => x > 10)";
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "None");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_any_all() {
|
|
let source = "let x = List.any([1, 2, 3], fn(x: Int): Bool => x > 2)";
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "true");
|
|
let source = "let x = List.all([1, 2, 3], fn(x: Int): Bool => x > 0)";
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "true");
|
|
let source = "let x = List.all([1, 2, 3], fn(x: Int): Bool => x > 2)";
|
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "false");
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_take_drop() {
|
|
let (result, _) = run_with_effects("let x = List.take([1, 2, 3, 4, 5], 3)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "[1, 2, 3]");
|
|
let (result, _) = run_with_effects("let x = List.drop([1, 2, 3, 4, 5], 2)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "[3, 4, 5]");
|
|
}
|
|
|
|
// String module additional functions
|
|
#[test]
|
|
fn test_string_starts_ends_with() {
|
|
let (result, _) = run_with_effects("let x = String.startsWith(\"hello\", \"he\")", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "true");
|
|
let (result, _) = run_with_effects("let x = String.endsWith(\"hello\", \"lo\")", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "true");
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_case_conversion() {
|
|
let (result, _) = run_with_effects("let x = String.toUpper(\"hello\")", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "\"HELLO\"");
|
|
let (result, _) = run_with_effects("let x = String.toLower(\"HELLO\")", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "\"hello\"");
|
|
}
|
|
|
|
#[test]
|
|
fn test_string_substring() {
|
|
let (result, _) = run_with_effects("let x = String.substring(\"hello world\", 0, 5)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "\"hello\"");
|
|
let (result, _) = run_with_effects("let x = String.substring(\"hello\", 2, 4)", Value::Unit, Value::Unit).unwrap();
|
|
assert_eq!(result, "\"ll\"");
|
|
}
|
|
}
|
|
|
|
// Diagnostic rendering tests
|
|
mod diagnostic_tests {
|
|
use crate::diagnostics::{render_diagnostic_plain, Diagnostic, Severity};
|
|
use crate::ast::Span;
|
|
use crate::typechecker::TypeError;
|
|
use crate::interpreter::RuntimeError;
|
|
|
|
#[test]
|
|
fn test_type_error_to_diagnostic() {
|
|
let error = TypeError {
|
|
message: "Type mismatch: expected Int, got String".to_string(),
|
|
span: Span { start: 10, end: 20 },
|
|
};
|
|
let diag = error.to_diagnostic();
|
|
|
|
assert_eq!(diag.severity, Severity::Error);
|
|
assert_eq!(diag.title, "Type Mismatch");
|
|
assert!(diag.hints.len() > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_runtime_error_to_diagnostic() {
|
|
let error = RuntimeError {
|
|
message: "Division by zero".to_string(),
|
|
span: Some(Span { start: 5, end: 10 }),
|
|
};
|
|
let diag = error.to_diagnostic();
|
|
|
|
assert_eq!(diag.severity, Severity::Error);
|
|
assert_eq!(diag.title, "Division by Zero");
|
|
assert!(diag.hints.len() > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_diagnostic_render_with_real_code() {
|
|
let source = "fn add(a: Int, b: Int): Int = a + b\nlet result = add(1, \"two\")";
|
|
let diag = Diagnostic::error(
|
|
"Type Mismatch",
|
|
"Expected Int but got String",
|
|
Span { start: 56, end: 61 },
|
|
)
|
|
.with_hint("The second argument should be an Int.");
|
|
|
|
let output = render_diagnostic_plain(&diag, source, Some("example.lux"));
|
|
|
|
assert!(output.contains("ERROR"));
|
|
assert!(output.contains("example.lux"));
|
|
assert!(output.contains("Type Mismatch"));
|
|
assert!(output.contains("\"two\""));
|
|
assert!(output.contains("Hint:"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_undefined_variable_categorization() {
|
|
let error = TypeError {
|
|
message: "Undefined variable: foobar".to_string(),
|
|
span: Span { start: 0, end: 6 },
|
|
};
|
|
let diag = error.to_diagnostic();
|
|
|
|
assert_eq!(diag.title, "Undefined Variable");
|
|
assert!(diag.hints.iter().any(|h| h.contains("spelling")));
|
|
// Check error code is set
|
|
assert!(diag.code.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_undefined_variable_suggestion() {
|
|
// Test that similar variable names are suggested
|
|
let source = r#"
|
|
let myVariable = 42
|
|
let x = myVriable
|
|
"#;
|
|
let result = super::eval(source);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err();
|
|
// The error should contain a "Did you mean?" suggestion
|
|
assert!(err.contains("Did you mean") || err.contains("myVariable"),
|
|
"Error should suggest 'myVariable': {}", err);
|
|
}
|
|
|
|
#[test]
|
|
fn test_purity_violation_categorization() {
|
|
let error = TypeError {
|
|
message: "Function 'foo' is declared as pure but has effects: {Console}".to_string(),
|
|
span: Span { start: 0, end: 10 },
|
|
};
|
|
let diag = error.to_diagnostic();
|
|
|
|
assert_eq!(diag.title, "Purity Violation");
|
|
}
|
|
}
|
|
|
|
// Exhaustiveness checking tests
|
|
mod exhaustiveness_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_exhaustive_bool_match() {
|
|
let source = r#"
|
|
fn check(b: Bool): Int = match b {
|
|
true => 1,
|
|
false => 0
|
|
}
|
|
let result = check(true)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_non_exhaustive_bool_match() {
|
|
let source = r#"
|
|
fn check(b: Bool): Int = match b {
|
|
true => 1
|
|
}
|
|
let result = check(true)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("Non-exhaustive"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_exhaustive_option_match() {
|
|
let source = r#"
|
|
fn unwrap_or(opt: Option<Int>, default: Int): Int = match opt {
|
|
Some(x) => x,
|
|
None => default
|
|
}
|
|
let result = unwrap_or(Some(42), 0)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_non_exhaustive_option_missing_none() {
|
|
let source = r#"
|
|
fn get_value(opt: Option<Int>): Int = match opt {
|
|
Some(x) => x
|
|
}
|
|
let result = get_value(Some(1))
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("Non-exhaustive"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_wildcard_is_exhaustive() {
|
|
let source = r#"
|
|
fn classify(n: Int): String = match n {
|
|
0 => "zero",
|
|
1 => "one",
|
|
_ => "many"
|
|
}
|
|
let result = classify(5)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_variable_pattern_is_exhaustive() {
|
|
let source = r#"
|
|
fn identity(n: Int): Int = match n {
|
|
x => x
|
|
}
|
|
let result = identity(42)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_redundant_arm_warning() {
|
|
let source = r#"
|
|
fn test_fn(n: Int): Int = match n {
|
|
_ => 1,
|
|
0 => 2
|
|
}
|
|
let result = test_fn(0)
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().contains("Redundant"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_exhaustive_result_match() {
|
|
let source = r#"
|
|
fn handle_result(r: Result<Int, String>): Int = match r {
|
|
Ok(n) => n,
|
|
Err(_) => 0
|
|
}
|
|
let result = handle_result(Ok(42))
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tail_call_optimization() {
|
|
// This test verifies that tail-recursive functions don't overflow the stack.
|
|
// Without TCO, a countdown from 10000 would cause a stack overflow.
|
|
let source = r#"
|
|
fn countdown(n: Int): Int = if n <= 0 then 0 else countdown(n - 1)
|
|
let result = countdown(10000)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tail_call_with_accumulator() {
|
|
// Test TCO with an accumulator pattern (common for tail-recursive sum)
|
|
let source = r#"
|
|
fn sum_to(n: Int, acc: Int): Int = if n <= 0 then acc else sum_to(n - 1, acc + n)
|
|
let result = sum_to(1000, 0)
|
|
"#;
|
|
// Sum from 1 to 1000 = 1000 * 1001 / 2 = 500500
|
|
assert_eq!(eval(source).unwrap(), "500500");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tail_call_in_match() {
|
|
// Test that TCO works through match expressions
|
|
let source = r#"
|
|
fn process(opt: Option<Int>, acc: Int): Int = match opt {
|
|
Some(n) => if n <= 0 then acc else process(Some(n - 1), acc + n),
|
|
None => acc
|
|
}
|
|
let result = process(Some(100), 0)
|
|
"#;
|
|
// Sum from 1 to 100 = 5050
|
|
assert_eq!(eval(source).unwrap(), "5050");
|
|
}
|
|
|
|
#[test]
|
|
fn test_trait_definition() {
|
|
// Test that trait declarations parse and type check correctly
|
|
let source = r#"
|
|
trait Show {
|
|
fn show(x: Int): String
|
|
}
|
|
let result = 42
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_trait_impl() {
|
|
// Test that impl declarations parse and type check correctly
|
|
let source = r#"
|
|
trait Double {
|
|
fn double(x: Int): Int
|
|
}
|
|
impl Double for Int {
|
|
fn double(x: Int): Int = x * 2
|
|
}
|
|
let result = 21
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_trait_with_super_trait() {
|
|
// Test super trait syntax
|
|
let source = r#"
|
|
trait Eq {
|
|
fn eq(a: Int, b: Int): Bool
|
|
}
|
|
trait Ord: Eq {
|
|
fn lt(a: Int, b: Int): Bool
|
|
}
|
|
let result = 42
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_impl_with_where_clause() {
|
|
// Test impl with where clause for trait constraints
|
|
let source = r#"
|
|
trait Show {
|
|
fn show(x: Int): String
|
|
}
|
|
let result = 42
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_effect_inference_function() {
|
|
// Test that effects are inferred when not explicitly declared
|
|
// This function uses Console effect without declaring it
|
|
let source = r#"
|
|
effect Console {
|
|
fn print(msg: String): Unit
|
|
}
|
|
fn greet(name: String): Unit = Console.print("Hello")
|
|
let result = 42
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success with inferred effects but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_effect_inference_lambda() {
|
|
// Test that lambda effects are inferred
|
|
let source = r#"
|
|
effect Logger {
|
|
fn log(msg: String): Unit
|
|
}
|
|
let logFn = fn(msg: String) => Logger.log(msg)
|
|
let result = 42
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success with inferred lambda effects but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_explicit_effects_validation() {
|
|
// Test that explicitly declared effects are validated against usage
|
|
let source = r#"
|
|
effect Console {
|
|
fn print(msg: String): Unit
|
|
}
|
|
fn greet(name: String): Unit with {Console} = Console.print("Hello")
|
|
let result = 42
|
|
"#;
|
|
let result = eval(source);
|
|
assert!(result.is_ok(), "Expected success with explicit effects but got: {:?}", result);
|
|
}
|
|
|
|
#[test]
|
|
fn test_doc_comments_on_function() {
|
|
// Test that doc comments are parsed and attached to functions
|
|
let source = r#"
|
|
/// Adds two numbers together.
|
|
/// Returns the sum.
|
|
fn add(a: Int, b: Int): Int = a + b
|
|
let result = add(1, 2)
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "3");
|
|
}
|
|
|
|
#[test]
|
|
fn test_doc_comments_on_type() {
|
|
// Test that doc comments are parsed and attached to types
|
|
let source = r#"
|
|
/// A point in 2D space.
|
|
type Point { x: Int, y: Int }
|
|
let p = { x: 1, y: 2 }
|
|
let result = p.x + p.y
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "3");
|
|
}
|
|
|
|
#[test]
|
|
fn test_doc_comments_multiline() {
|
|
// Test that multiple doc comment lines are combined
|
|
let source = r#"
|
|
/// First line of documentation.
|
|
/// Second line of documentation.
|
|
/// Third line of documentation.
|
|
fn documented(): Int = 42
|
|
let result = documented()
|
|
"#;
|
|
assert_eq!(eval(source).unwrap(), "42");
|
|
}
|
|
}
|
|
|
|
// Integration tests for example files
|
|
mod example_tests {
|
|
use super::*;
|
|
use std::fs;
|
|
|
|
fn check_file(path: &str) -> Result<(), String> {
|
|
let source = fs::read_to_string(path)
|
|
.map_err(|e| format!("Failed to read {}: {}", path, e))?;
|
|
|
|
let program = Parser::parse_source(&source)
|
|
.map_err(|e| format!("Parse error in {}: {}", path, e))?;
|
|
|
|
let mut checker = TypeChecker::new();
|
|
checker.check_program(&program).map_err(|errors| {
|
|
format!("Type errors in {}:\n{}", path,
|
|
errors.iter().map(|e| e.to_string()).collect::<Vec<_>>().join("\n"))
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_hello() {
|
|
check_file("examples/hello.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_factorial() {
|
|
check_file("examples/factorial.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_functional() {
|
|
check_file("examples/functional.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_pipelines() {
|
|
check_file("examples/pipelines.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_tailcall() {
|
|
check_file("examples/tailcall.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_effects() {
|
|
check_file("examples/effects.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_handlers() {
|
|
check_file("examples/handlers.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_builtin_effects() {
|
|
check_file("examples/builtin_effects.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_datatypes() {
|
|
check_file("examples/datatypes.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_generics() {
|
|
check_file("examples/generics.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_traits() {
|
|
check_file("examples/traits.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_interpolation() {
|
|
check_file("examples/interpolation.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_behavioral() {
|
|
check_file("examples/behavioral.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_behavioral_types() {
|
|
check_file("examples/behavioral_types.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_versioning() {
|
|
check_file("examples/versioning.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_schema_evolution() {
|
|
check_file("examples/schema_evolution.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_file_io() {
|
|
check_file("examples/file_io.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_json() {
|
|
check_file("examples/json.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_random() {
|
|
check_file("examples/random.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_statemachine() {
|
|
check_file("examples/statemachine.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_shell() {
|
|
check_file("examples/shell.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_http() {
|
|
check_file("examples/http.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_http_server() {
|
|
check_file("examples/http_server.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_example_jit_test() {
|
|
check_file("examples/jit_test.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_project_json_parser() {
|
|
check_file("projects/json-parser/main.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_project_markdown_converter() {
|
|
check_file("projects/markdown-converter/main.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_project_todo_app() {
|
|
check_file("projects/todo-app/main.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_project_mini_interpreter() {
|
|
check_file("projects/mini-interpreter/main.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_project_guessing_game() {
|
|
check_file("projects/guessing-game/main.lux").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_project_rest_api() {
|
|
check_file("projects/rest-api/main.lux").unwrap();
|
|
}
|
|
}
|
|
}
|