Add Random and Time effects for random number generation and time-based operations. These effects can be used in any effectful code block. Random effect operations: - Random.int(min, max) - random integer in range [min, max] - Random.float() - random float in range [0.0, 1.0) - Random.bool() - random boolean Time effect operations: - Time.now() - current Unix timestamp in milliseconds - Time.sleep(ms) - sleep for specified milliseconds Changes: - Add rand crate dependency - Add Random and Time effect definitions to types.rs - Add effects to built-in effects list in typechecker - Implement effect handlers in interpreter - Add 4 new tests for Random and Time effects - Add examples/random.lux demonstrating usage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1673 lines
52 KiB
Rust
1673 lines
52 KiB
Rust
//! Lux - A functional programming language with first-class effects
|
|
|
|
mod ast;
|
|
mod diagnostics;
|
|
mod exhaustiveness;
|
|
mod interpreter;
|
|
mod lexer;
|
|
mod lsp;
|
|
mod modules;
|
|
mod parser;
|
|
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
|
|
: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" => {
|
|
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");
|
|
println!(" lux --lsp Start LSP server (for IDE integration)");
|
|
println!(" lux --help Show this help");
|
|
}
|
|
"--version" | "-v" => {
|
|
println!("Lux {}", VERSION);
|
|
}
|
|
path => {
|
|
// Run a file
|
|
run_file(path);
|
|
}
|
|
}
|
|
} else {
|
|
// Start REPL
|
|
run_repl();
|
|
}
|
|
}
|
|
|
|
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",
|
|
]
|
|
.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))
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
_ => {
|
|
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_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""#);
|
|
}
|
|
|
|
// 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"));
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
severity: Severity::Error,
|
|
title: "Type Mismatch".to_string(),
|
|
message: "Expected Int but got String".to_string(),
|
|
span: Span { start: 56, end: 61 },
|
|
hints: vec!["The second argument should be an Int.".to_string()],
|
|
};
|
|
|
|
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, "Unknown Name");
|
|
assert!(diag.hints.iter().any(|h| h.contains("spelling")));
|
|
}
|
|
|
|
#[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");
|
|
}
|
|
}
|
|
}
|