feat: add JavaScript backend for browser/Node.js compilation

Implement Phase 1 of the browser/frontend plan with a complete JS
code generator that compiles Lux programs to JavaScript:

- Basic types: Int, Float, Bool, String, Unit → JS primitives
- Functions and closures → native JS functions with closure capture
- Pattern matching → if/else chains with tag checks
- ADT definitions → tagged object constructors
- Built-in types (Option, Result) → Lux.Some/None/Ok/Err helpers
- Effects → handler objects passed as parameters
- List operations → Array methods (map, filter, reduce, etc.)
- Top-level let bindings and run expressions

CLI usage:
  lux compile app.lux --target js -o app.js
  lux compile app.lux --target js --run

Tested with factorial, datatypes, functional, tailcall, and hello
examples - all producing correct output when run in Node.js.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 21:22:43 -05:00
parent 9f9543a881
commit 10ff8dc6ad
3 changed files with 1176 additions and 3 deletions

1071
src/codegen/js_backend.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,13 @@
//! //!
//! This module provides compilation to various targets: //! This module provides compilation to various targets:
//! - C: For native compilation via GCC/Clang //! - C: For native compilation via GCC/Clang
//! - JavaScript: For frontend/browser deployment (planned) //! - JavaScript: For frontend/browser deployment
//! - WebAssembly: For portable deployment (planned) //! - WebAssembly: For portable deployment (planned)
pub mod c_backend; pub mod c_backend;
pub mod js_backend;
#[allow(unused_imports)] #[allow(unused_imports)]
pub use c_backend::CBackend; pub use c_backend::CBackend;
#[allow(unused_imports)]
pub use js_backend::JsBackend;

View File

@@ -140,21 +140,32 @@ fn main() {
handle_pkg_command(&args[2..]); handle_pkg_command(&args[2..]);
} }
"compile" => { "compile" => {
// Compile to native binary // Compile to native binary or JavaScript
if args.len() < 3 { if args.len() < 3 {
eprintln!("Usage: lux compile <file.lux> [-o binary]"); eprintln!("Usage: lux compile <file.lux> [-o binary]");
eprintln!(" lux compile <file.lux> --run"); eprintln!(" lux compile <file.lux> --run");
eprintln!(" lux compile <file.lux> --emit-c [-o file.c]"); eprintln!(" lux compile <file.lux> --emit-c [-o file.c]");
eprintln!(" lux compile <file.lux> --target js [-o file.js]");
std::process::exit(1); std::process::exit(1);
} }
let run_after = args.iter().any(|a| a == "--run"); let run_after = args.iter().any(|a| a == "--run");
let emit_c = args.iter().any(|a| a == "--emit-c"); 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() let output_path = args.iter()
.position(|a| a == "-o") .position(|a| a == "-o")
.and_then(|i| args.get(i + 1)) .and_then(|i| args.get(i + 1))
.map(|s| s.as_str()); .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); compile_to_c(&args[2], output_path, run_after, emit_c);
} }
}
path => { path => {
// Run a file // Run a file
run_file(path); run_file(path);
@@ -176,6 +187,7 @@ fn print_help() {
println!(" lux compile <f> -o app Compile to binary named 'app'"); println!(" lux compile <f> -o app Compile to binary named 'app'");
println!(" lux compile <f> --run Compile and execute"); println!(" lux compile <f> --run Compile and execute");
println!(" lux compile <f> --emit-c Output C code instead of binary"); 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.lux> Format a file (--check to verify only)"); println!(" lux fmt <file.lux> Format a file (--check to verify only)");
println!(" lux check <file.lux> Type check without running"); println!(" lux check <file.lux> Type check without running");
println!(" lux test [pattern] Run tests (optional pattern filter)"); println!(" lux test [pattern] Run tests (optional pattern filter)");
@@ -398,6 +410,93 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c:
} }
} }
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]) { fn run_tests(args: &[String]) {
use std::path::Path; use std::path::Path;
use std::fs; use std::fs;