init lux
This commit is contained in:
791
src/main.rs
Normal file
791
src/main.rs
Normal file
@@ -0,0 +1,791 @@
|
||||
//! Lux - A functional programming language with first-class effects
|
||||
|
||||
mod ast;
|
||||
mod interpreter;
|
||||
mod lexer;
|
||||
mod modules;
|
||||
mod parser;
|
||||
mod schema;
|
||||
mod typechecker;
|
||||
mod types;
|
||||
|
||||
use interpreter::Interpreter;
|
||||
use parser::Parser;
|
||||
use std::io::{self, Write};
|
||||
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
|
||||
:clear Clear the environment
|
||||
:load <file> Load and execute a file
|
||||
:trace on/off Enable/disable effect tracing
|
||||
:traces Show recorded effect traces
|
||||
|
||||
Examples:
|
||||
> let x = 42
|
||||
> x + 1
|
||||
43
|
||||
|
||||
> fn double(n: Int): Int = n * 2
|
||||
> double(21)
|
||||
42
|
||||
|
||||
> Console.print("Hello, world!")
|
||||
Hello, world!
|
||||
|
||||
Debugging:
|
||||
> :trace on
|
||||
> Console.print("test")
|
||||
> :traces
|
||||
[ 0.123ms] Console.print("test") → ()
|
||||
"#;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
if args.len() > 1 {
|
||||
// Run a file
|
||||
run_file(&args[1]);
|
||||
} 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 {
|
||||
eprintln!("Type error: {}", error);
|
||||
}
|
||||
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) => {
|
||||
eprintln!("Runtime error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_repl() {
|
||||
println!("Lux v{}", VERSION);
|
||||
println!("Type :help for help, :quit to exit\n");
|
||||
|
||||
let mut interp = Interpreter::new();
|
||||
let mut checker = TypeChecker::new();
|
||||
let mut buffer = String::new();
|
||||
let mut continuation = false;
|
||||
|
||||
loop {
|
||||
// Print prompt
|
||||
let prompt = if continuation { "... " } else { "lux> " };
|
||||
print!("{}", prompt);
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
// Read input
|
||||
let mut line = String::new();
|
||||
match io::stdin().read_line(&mut line) {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("Error reading input: {}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let line = line.trim_end();
|
||||
|
||||
// Handle commands
|
||||
if !continuation && line.starts_with(':') {
|
||||
handle_command(line, &mut interp, &mut checker);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Accumulate input
|
||||
buffer.push_str(line);
|
||||
buffer.push('\n');
|
||||
|
||||
// Check for continuation (simple heuristic: unbalanced braces)
|
||||
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;
|
||||
}
|
||||
|
||||
eval_input(&input, &mut interp, &mut checker);
|
||||
}
|
||||
|
||||
println!("\nGoodbye!");
|
||||
}
|
||||
|
||||
fn handle_command(line: &str, interp: &mut Interpreter, checker: &mut TypeChecker) {
|
||||
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>");
|
||||
}
|
||||
}
|
||||
":clear" => {
|
||||
*interp = Interpreter::new();
|
||||
*checker = TypeChecker::new();
|
||||
println!("Environment cleared.");
|
||||
}
|
||||
":load" | ":l" => {
|
||||
if let Some(path) = arg {
|
||||
load_file(path, interp, checker);
|
||||
} 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_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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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"]"#);
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user