test: add comprehensive integration tests for JS backend

Add 10 integration tests that compile Lux to JavaScript and verify
correct execution in Node.js:

- test_js_factorial: Recursion and effects
- test_js_fibonacci: Classic recursive algorithm
- test_js_adt_and_pattern_matching: Custom ADTs with match
- test_js_option_type: Built-in Option type handling
- test_js_closures: Closure creation and variable capture
- test_js_higher_order_functions: Functions as values
- test_js_list_operations: List.map, List.foldl
- test_js_pipe_operator: Pipe (|>) operator
- test_js_records: Record literal and field access
- test_js_string_concatenation: String operations

Also fix List module operations being incorrectly treated as effects
by adding special-case handling in EffectOp emission.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 21:28:07 -05:00
parent 10ff8dc6ad
commit ce4344810d

View File

@@ -508,6 +508,11 @@ impl JsBackend {
args, args,
.. ..
} => { } => {
// Special case: List module operations (not an effect)
if effect.name == "List" {
return self.emit_list_operation(&operation.name, args);
}
let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect(); let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect();
let args_str = arg_strs?.join(", "); let args_str = arg_strs?.join(", ");
@@ -1024,6 +1029,8 @@ impl Default for JsBackend {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::parser::Parser;
use std::process::Command;
#[test] #[test]
fn test_literal_int() { fn test_literal_int() {
@@ -1068,4 +1075,194 @@ mod tests {
assert_eq!(backend.escape_js_keyword("class"), "class_"); assert_eq!(backend.escape_js_keyword("class"), "class_");
assert_eq!(backend.escape_js_keyword("foo"), "foo"); assert_eq!(backend.escape_js_keyword("foo"), "foo");
} }
/// Helper to compile Lux source to JS and run in Node.js
fn compile_and_run(source: &str) -> Result<String, String> {
let program = Parser::parse_source(source).map_err(|e| format!("Parse error: {}", e))?;
let mut backend = JsBackend::new();
let js_code = backend
.generate(&program)
.map_err(|e| format!("Codegen error: {}", e))?;
let output = Command::new("node")
.arg("-e")
.arg(&js_code)
.output()
.map_err(|e| format!("Node.js error: {}", e))?;
if !output.status.success() {
return Err(format!(
"Node.js execution failed:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
#[test]
fn test_js_factorial() {
let source = r#"
fn factorial(n: Int): Int =
if n <= 1 then 1
else n * factorial(n - 1)
fn main(): Unit with {Console} =
Console.print("Result: " + toString(factorial(5)))
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Result: 120");
}
#[test]
fn test_js_fibonacci() {
let source = r#"
fn fib(n: Int): Int =
if n <= 1 then n
else fib(n - 1) + fib(n - 2)
fn main(): Unit with {Console} =
Console.print("fib(10) = " + toString(fib(10)))
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "fib(10) = 55");
}
#[test]
fn test_js_adt_and_pattern_matching() {
let source = r#"
type Tree =
| Leaf(Int)
| Node(Tree, Tree)
fn sumTree(tree: Tree): Int =
match tree {
Leaf(n) => n,
Node(left, right) => sumTree(left) + sumTree(right)
}
let tree = Node(Node(Leaf(1), Leaf(2)), Leaf(3))
fn main(): Unit with {Console} =
Console.print("Sum: " + toString(sumTree(tree)))
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Sum: 6");
}
#[test]
fn test_js_option_type() {
let source = r#"
fn safeDivide(a: Int, b: Int): Option<Int> =
if b == 0 then None
else Some(a / b)
fn showResult(opt: Option<Int>): String =
match opt {
Some(n) => "Got: " + toString(n),
None => "None"
}
fn main(): Unit with {Console} = {
Console.print(showResult(safeDivide(10, 2)))
Console.print(showResult(safeDivide(10, 0)))
}
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Got: 5\nNone");
}
#[test]
fn test_js_closures() {
let source = r#"
fn makeAdder(x: Int): fn(Int): Int =
fn(y: Int): Int => x + y
let add5 = makeAdder(5)
fn main(): Unit with {Console} =
Console.print("5 + 10 = " + toString(add5(10)))
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "5 + 10 = 15");
}
#[test]
fn test_js_higher_order_functions() {
let source = r#"
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
fn double(x: Int): Int = x * 2
fn main(): Unit with {Console} =
Console.print("double(21) = " + toString(apply(double, 21)))
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "double(21) = 42");
}
#[test]
fn test_js_list_operations() {
let source = r#"
let nums = [1, 2, 3, 4, 5]
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
let sum = List.foldl(doubled, 0, fn(acc: Int, x: Int): Int => acc + x)
fn main(): Unit with {Console} =
Console.print("Sum of doubled: " + toString(sum))
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Sum of doubled: 30");
}
#[test]
fn test_js_pipe_operator() {
let source = r#"
fn double(x: Int): Int = x * 2
fn addOne(x: Int): Int = x + 1
let result = 5 |> double |> addOne
fn main(): Unit with {Console} =
Console.print("Result: " + toString(result))
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Result: 11");
}
#[test]
fn test_js_records() {
let source = r#"
let point = { x: 10, y: 20 }
let sum = point.x + point.y
fn main(): Unit with {Console} =
Console.print("Sum: " + toString(sum))
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Sum: 30");
}
#[test]
fn test_js_string_concatenation() {
let source = r#"
let name = "World"
let greeting = "Hello, " + name + "!"
fn main(): Unit with {Console} =
Console.print(greeting)
"#;
let output = compile_and_run(source).expect("Should compile and run");
assert_eq!(output, "Hello, World!");
}
} }