From ce4344810d03dac643fe8e2c8028adddd7c92892 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Sat, 14 Feb 2026 21:28:07 -0500 Subject: [PATCH] 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 --- src/codegen/js_backend.rs | 197 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/src/codegen/js_backend.rs b/src/codegen/js_backend.rs index 84e44d8..974c6b7 100644 --- a/src/codegen/js_backend.rs +++ b/src/codegen/js_backend.rs @@ -508,6 +508,11 @@ impl JsBackend { 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, _> = args.iter().map(|a| self.emit_expr(a)).collect(); let args_str = arg_strs?.join(", "); @@ -1024,6 +1029,8 @@ impl Default for JsBackend { #[cfg(test)] mod tests { use super::*; + use crate::parser::Parser; + use std::process::Command; #[test] fn test_literal_int() { @@ -1068,4 +1075,194 @@ mod tests { assert_eq!(backend.escape_js_keyword("class"), "class_"); 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 { + 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 = + if b == 0 then None + else Some(a / b) + + fn showResult(opt: Option): 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!"); + } }