From 10ff8dc6ad17fc562e7f509c57d40d39f4f423b6 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Sat, 14 Feb 2026 21:22:43 -0500 Subject: [PATCH] feat: add JavaScript backend for browser/Node.js compilation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/codegen/js_backend.rs | 1071 +++++++++++++++++++++++++++++++++++++ src/codegen/mod.rs | 5 +- src/main.rs | 103 +++- 3 files changed, 1176 insertions(+), 3 deletions(-) create mode 100644 src/codegen/js_backend.rs diff --git a/src/codegen/js_backend.rs b/src/codegen/js_backend.rs new file mode 100644 index 0000000..84e44d8 --- /dev/null +++ b/src/codegen/js_backend.rs @@ -0,0 +1,1071 @@ +//! JavaScript code generation backend for Lux +//! +//! Compiles Lux programs to JavaScript for browser and Node.js execution. +//! +//! ## Compilation Strategy +//! +//! Lux source → Parse → Type check → Generate JavaScript → Run in browser/Node +//! +//! ## Runtime Type Representations +//! +//! | Lux Type | JavaScript Type | +//! |----------|-----------------| +//! | Int | `number` (BigInt for large values) | +//! | Float | `number` | +//! | Bool | `boolean` | +//! | String | `string` | +//! | Unit | `undefined` | +//! | List | `Array` | +//! | Option | `{tag: "Some", value: T} \| {tag: "None"}` | +//! | Result | `{tag: "Ok", value: T} \| {tag: "Err", error: E}` | +//! | Closure | `function` (native JS closures) | +//! | ADT | `{tag: "VariantName", field0: ..., field1: ...}` | +//! +//! ## Effects +//! +//! Effects are compiled to handler objects passed as parameters: +//! ```javascript +//! async function fetchUser_lux(handlers, id) { +//! return await handlers.Http.get(`/api/users/${id}`); +//! } +//! ``` + +use crate::ast::*; +use std::collections::{HashMap, HashSet}; + +/// JavaScript code generation errors +#[derive(Debug, Clone)] +pub struct JsGenError { + pub message: String, + #[allow(dead_code)] + pub span: Option, +} + +impl std::fmt::Display for JsGenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "JS codegen error: {}", self.message) + } +} + +impl std::error::Error for JsGenError {} + +/// The JavaScript backend code generator +pub struct JsBackend { + /// Generated JavaScript code + output: String, + /// Current indentation level + indent: usize, + /// Known function names + functions: HashSet, + /// Counter for generating unique names + name_counter: usize, + /// Mapping from variant names to their parent type name + variant_to_type: HashMap, + /// Mapping from (type_name, variant_name) to field types + variant_field_types: HashMap<(String, String), Vec>, + /// Functions that use effects (have handlers parameter) + effectful_functions: HashSet, + /// Whether we're currently inside an effectful function + has_handlers: bool, + /// Variable substitutions for let binding + var_substitutions: HashMap, +} + +impl JsBackend { + pub fn new() -> Self { + // Initialize built-in type variants + let mut variant_to_type = HashMap::new(); + variant_to_type.insert("Some".to_string(), "Option".to_string()); + variant_to_type.insert("None".to_string(), "Option".to_string()); + variant_to_type.insert("Ok".to_string(), "Result".to_string()); + variant_to_type.insert("Err".to_string(), "Result".to_string()); + + Self { + output: String::new(), + indent: 0, + functions: HashSet::new(), + name_counter: 0, + variant_to_type, + variant_field_types: HashMap::new(), + effectful_functions: HashSet::new(), + has_handlers: false, + var_substitutions: HashMap::new(), + } + } + + /// Generate JavaScript code from a Lux program + pub fn generate(&mut self, program: &Program) -> Result { + self.output.clear(); + + // Emit runtime helpers + self.emit_runtime(); + + // First pass: collect all function names, types, and effects + for decl in &program.declarations { + match decl { + Declaration::Function(f) => { + self.functions.insert(f.name.name.clone()); + if !f.effects.is_empty() { + self.effectful_functions.insert(f.name.name.clone()); + } + } + Declaration::Type(t) => { + self.collect_type(t)?; + } + _ => {} + } + } + + // Emit type constructors + for decl in &program.declarations { + if let Declaration::Type(t) = decl { + self.emit_type_constructors(t)?; + } + } + + // Emit functions + for decl in &program.declarations { + if let Declaration::Function(f) = decl { + self.emit_function(f)?; + } + } + + // Emit top-level let bindings and expressions + self.writeln("// Top-level bindings"); + for decl in &program.declarations { + if let Declaration::Let(l) = decl { + self.emit_top_level_let(l)?; + } + } + + // Emit main function call if it exists + if self.functions.contains("main") { + self.writeln(""); + self.writeln("// Entry point"); + if self.effectful_functions.contains("main") { + self.writeln("main_lux(Lux.defaultHandlers);"); + } else { + self.writeln("main_lux();"); + } + } + + Ok(self.output.clone()) + } + + /// Emit the minimal Lux runtime + fn emit_runtime(&mut self) { + self.writeln("// Lux Runtime"); + self.writeln("const Lux = {"); + self.indent += 1; + + // Option helpers + self.writeln("Some: (value) => ({ tag: \"Some\", value }),"); + self.writeln("None: () => ({ tag: \"None\" }),"); + self.writeln(""); + + // Result helpers + self.writeln("Ok: (value) => ({ tag: \"Ok\", value }),"); + self.writeln("Err: (error) => ({ tag: \"Err\", error }),"); + self.writeln(""); + + // List helpers + self.writeln("Cons: (head, tail) => [head, ...tail],"); + self.writeln("Nil: () => [],"); + self.writeln(""); + + // Default handlers for effects + self.writeln("defaultHandlers: {"); + self.indent += 1; + self.writeln("Console: {"); + self.indent += 1; + self.writeln("print: (msg) => console.log(msg),"); + self.writeln("readLine: () => { throw new Error('readLine not supported in browser'); },"); + self.writeln("readInt: () => { throw new Error('readInt not supported in browser'); }"); + self.indent -= 1; + self.writeln("},"); + self.writeln("Random: {"); + self.indent += 1; + self.writeln("int: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min"); + self.indent -= 1; + self.writeln("}"); + self.indent -= 1; + self.writeln("}"); + + self.indent -= 1; + self.writeln("};"); + self.writeln(""); + } + + /// Collect type information from a type declaration + fn collect_type(&mut self, decl: &TypeDecl) -> Result<(), JsGenError> { + if let TypeDef::Enum(variants) = &decl.definition { + for variant in variants { + self.variant_to_type + .insert(variant.name.name.clone(), decl.name.name.clone()); + + // Store field types + let field_types: Vec = match &variant.fields { + VariantFields::Unit => vec![], + VariantFields::Tuple(types) => { + types.iter().map(|_| "any".to_string()).collect() + } + VariantFields::Record(fields) => { + fields.iter().map(|_| "any".to_string()).collect() + } + }; + self.variant_field_types.insert( + (decl.name.name.clone(), variant.name.name.clone()), + field_types, + ); + } + } + Ok(()) + } + + /// Emit constructor functions for ADT variants + fn emit_type_constructors(&mut self, decl: &TypeDecl) -> Result<(), JsGenError> { + if let TypeDef::Enum(variants) = &decl.definition { + self.writeln(&format!("// {} constructors", decl.name.name)); + + for variant in variants { + match &variant.fields { + VariantFields::Unit => { + // Unit variant: const None_lux = { tag: "None" }; + self.writeln(&format!( + "const {}_lux = {{ tag: \"{}\" }};", + variant.name.name, variant.name.name + )); + } + VariantFields::Tuple(types) => { + // Tuple variant: function Some_lux(value) { return { tag: "Some", field0: value }; } + let params: Vec = (0..types.len()) + .map(|i| format!("field{}", i)) + .collect(); + let fields: Vec = params + .iter() + .enumerate() + .map(|(i, p)| format!("field{}: {}", i, p)) + .collect(); + self.writeln(&format!( + "function {}_lux({}) {{ return {{ tag: \"{}\", {} }}; }}", + variant.name.name, + params.join(", "), + variant.name.name, + fields.join(", ") + )); + } + VariantFields::Record(fields) => { + // Record variant: function Foo_lux(a, b) { return { tag: "Foo", a, b }; } + let params: Vec = + fields.iter().map(|f| f.name.name.clone()).collect(); + let field_inits: Vec = + params.iter().map(|p| format!("{}: {}", p, p)).collect(); + self.writeln(&format!( + "function {}_lux({}) {{ return {{ tag: \"{}\", {} }}; }}", + variant.name.name, + params.join(", "), + variant.name.name, + field_inits.join(", ") + )); + } + } + } + self.writeln(""); + } + Ok(()) + } + + /// Emit a function declaration + fn emit_function(&mut self, func: &FunctionDecl) -> Result<(), JsGenError> { + let func_name = self.mangle_name(&func.name.name); + let is_effectful = !func.effects.is_empty(); + + // Build parameter list + let mut params: Vec = func.params.iter().map(|p| p.name.name.clone()).collect(); + + // Effectful functions get handlers as first parameter + if is_effectful { + params.insert(0, "handlers".to_string()); + } + + // Function declaration + self.writeln(&format!( + "function {}({}) {{", + func_name, + params.join(", ") + )); + self.indent += 1; + + // Set context for effect handling + let prev_has_handlers = self.has_handlers; + self.has_handlers = is_effectful; + + // Clear var substitutions for this function + self.var_substitutions.clear(); + + // Emit function body + let body_code = self.emit_expr(&func.body)?; + self.writeln(&format!("return {};", body_code)); + + self.has_handlers = prev_has_handlers; + + self.indent -= 1; + self.writeln("}"); + self.writeln(""); + + Ok(()) + } + + /// Emit a top-level let binding + fn emit_top_level_let(&mut self, let_decl: &LetDecl) -> Result<(), JsGenError> { + let val = self.emit_expr(&let_decl.value)?; + let var_name = &let_decl.name.name; + + // Check if this is a run expression (often results in undefined) + // We still want to execute it for its side effects + self.writeln(&format!("const {} = {};", var_name, val)); + + // Register the variable for future use + self.var_substitutions + .insert(var_name.clone(), var_name.clone()); + + Ok(()) + } + + /// Emit an expression and return the JavaScript code + fn emit_expr(&mut self, expr: &Expr) -> Result { + match expr { + Expr::Literal(lit) => self.emit_literal(lit), + + Expr::Var(ident) => { + // Check for variable substitution + if let Some(subst) = self.var_substitutions.get(&ident.name) { + return Ok(subst.clone()); + } + + // Check if this is a unit constructor + if let Some(type_name) = self.variant_to_type.get(&ident.name) { + // Use Lux.* for built-in types (Option, Result) + if type_name == "Option" || type_name == "Result" { + Ok(format!("Lux.{}()", ident.name)) + } else { + Ok(format!("{}_lux", ident.name)) + } + } else if self.functions.contains(&ident.name) { + // Function reference (used as value) + Ok(self.mangle_name(&ident.name)) + } else { + Ok(self.escape_js_keyword(&ident.name)) + } + } + + Expr::BinaryOp { + op, left, right, .. + } => { + let l = self.emit_expr(left)?; + let r = self.emit_expr(right)?; + + // Check for string concatenation + if matches!(op, BinaryOp::Add) { + if self.is_string_expr(left) || self.is_string_expr(right) { + return Ok(format!("({} + {})", l, r)); + } + } + + let op_str = match op { + BinaryOp::Add => "+", + BinaryOp::Sub => "-", + BinaryOp::Mul => "*", + BinaryOp::Div => "/", + BinaryOp::Mod => "%", + BinaryOp::Eq => "===", + BinaryOp::Ne => "!==", + BinaryOp::Lt => "<", + BinaryOp::Le => "<=", + BinaryOp::Gt => ">", + BinaryOp::Ge => ">=", + BinaryOp::And => "&&", + BinaryOp::Or => "||", + BinaryOp::Pipe => { + // Pipe operator: x |> f becomes f(x) + return Ok(format!("{}({})", r, l)); + } + }; + + Ok(format!("({} {} {})", l, op_str, r)) + } + + Expr::UnaryOp { op, operand, .. } => { + let val = self.emit_expr(operand)?; + let op_str = match op { + UnaryOp::Neg => "-", + UnaryOp::Not => "!", + }; + Ok(format!("({}{})", op_str, val)) + } + + Expr::If { + condition, + then_branch, + else_branch, + .. + } => { + let cond = self.emit_expr(condition)?; + let then_val = self.emit_expr(then_branch)?; + let else_val = self.emit_expr(else_branch)?; + Ok(format!("({} ? {} : {})", cond, then_val, else_val)) + } + + Expr::Let { + name, value, body, .. + } => { + let val = self.emit_expr(value)?; + let var_name = format!("{}_{}", name.name, self.fresh_name()); + + self.writeln(&format!("const {} = {};", var_name, val)); + + // Add substitution + self.var_substitutions + .insert(name.name.clone(), var_name.clone()); + + let body_result = self.emit_expr(body)?; + + // Remove substitution + self.var_substitutions.remove(&name.name); + + Ok(body_result) + } + + Expr::Call { func, args, .. } => { + // Check for List module calls + if let Expr::Field { object, field, .. } = func.as_ref() { + if let Expr::Var(module_name) = object.as_ref() { + if module_name.name == "List" { + return self.emit_list_operation(&field.name, args); + } + } + } + + // Check for built-in functions + if let Expr::Var(ident) = func.as_ref() { + if ident.name == "toString" { + let arg = self.emit_expr(&args[0])?; + return Ok(format!("String({})", arg)); + } + } + + let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); + let args_str = arg_strs?.join(", "); + + match func.as_ref() { + Expr::Var(ident) if self.functions.contains(&ident.name) => { + let js_func_name = self.mangle_name(&ident.name); + let is_effectful = self.effectful_functions.contains(&ident.name); + + if is_effectful && self.has_handlers { + // Use the handlers variable from substitutions, or default "handlers" + let handlers_name = self + .var_substitutions + .get("handlers") + .cloned() + .unwrap_or_else(|| "handlers".to_string()); + if args_str.is_empty() { + Ok(format!("{}({})", js_func_name, handlers_name)) + } else { + Ok(format!("{}({}, {})", js_func_name, handlers_name, args_str)) + } + } else if is_effectful { + if args_str.is_empty() { + Ok(format!("{}(Lux.defaultHandlers)", js_func_name)) + } else { + Ok(format!("{}(Lux.defaultHandlers, {})", js_func_name, args_str)) + } + } else { + Ok(format!("{}({})", js_func_name, args_str)) + } + } + Expr::Var(ident) if self.variant_to_type.contains_key(&ident.name) => { + // ADT constructor call + // Use Lux.* for built-in types (Option, Result) + let type_name = self.variant_to_type.get(&ident.name).unwrap(); + if type_name == "Option" || type_name == "Result" { + Ok(format!("Lux.{}({})", ident.name, args_str)) + } else { + Ok(format!("{}_lux({})", ident.name, args_str)) + } + } + _ => { + // Generic function call + let func_code = self.emit_expr(func)?; + Ok(format!("{}({})", func_code, args_str)) + } + } + } + + Expr::EffectOp { + effect, + operation, + args, + .. + } => { + let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); + let args_str = arg_strs?.join(", "); + + if self.has_handlers { + // Use the handlers variable from substitutions, or default "handlers" + let handlers_name = self + .var_substitutions + .get("handlers") + .cloned() + .unwrap_or_else(|| "handlers".to_string()); + Ok(format!( + "{}.{}.{}({})", + handlers_name, effect.name, operation.name, args_str + )) + } else { + Ok(format!( + "Lux.defaultHandlers.{}.{}({})", + effect.name, operation.name, args_str + )) + } + } + + Expr::Lambda { + params, + body, + effects, + .. + } => { + let param_names: Vec = + params.iter().map(|p| p.name.name.clone()).collect(); + + // If lambda has effects, it takes handlers + let all_params = if !effects.is_empty() { + let mut p = vec!["handlers".to_string()]; + p.extend(param_names); + p + } else { + param_names + }; + + // Save handler state + let prev_has_handlers = self.has_handlers; + self.has_handlers = !effects.is_empty(); + + let body_code = self.emit_expr(body)?; + + self.has_handlers = prev_has_handlers; + + Ok(format!( + "(function({}) {{ return {}; }})", + all_params.join(", "), + body_code + )) + } + + Expr::Match { + scrutinee, arms, .. + } => self.emit_match(scrutinee, arms), + + Expr::Block { + statements, result, .. + } => { + // Emit each statement + for stmt in statements { + match stmt { + Statement::Expr(expr) => { + let code = self.emit_expr(expr)?; + self.writeln(&format!("{};", code)); + } + Statement::Let { name, value, .. } => { + let val = self.emit_expr(value)?; + let var_name = format!("{}_{}", name.name, self.fresh_name()); + self.writeln(&format!("const {} = {};", var_name, val)); + self.var_substitutions + .insert(name.name.clone(), var_name.clone()); + } + } + } + + // Emit result + self.emit_expr(result) + } + + Expr::Record { fields, .. } => { + let field_strs: Result, _> = fields + .iter() + .map(|(name, expr)| { + let val = self.emit_expr(expr)?; + Ok(format!("{}: {}", name.name, val)) + }) + .collect(); + Ok(format!("{{ {} }}", field_strs?.join(", "))) + } + + Expr::Tuple { elements, .. } => { + let elem_strs: Result, _> = + elements.iter().map(|e| self.emit_expr(e)).collect(); + Ok(format!("[{}]", elem_strs?.join(", "))) + } + + Expr::List { elements, .. } => { + let elem_strs: Result, _> = + elements.iter().map(|e| self.emit_expr(e)).collect(); + Ok(format!("[{}]", elem_strs?.join(", "))) + } + + Expr::Field { object, field, .. } => { + let obj = self.emit_expr(object)?; + Ok(format!("{}.{}", obj, field.name)) + } + + Expr::Run { + expr, handlers, .. + } => { + // Create handler object, merging with defaults + let handlers_var = format!("_handlers_{}", self.fresh_name()); + + if handlers.is_empty() { + // No custom handlers, use defaults + self.writeln(&format!( + "const {} = Lux.defaultHandlers;", + handlers_var + )); + } else { + // Merge custom handlers with defaults + let handler_strs: Result, _> = handlers + .iter() + .map(|(name, handler_expr)| { + let handler_code = self.emit_expr(handler_expr)?; + Ok(format!("{}: {}", name.name, handler_code)) + }) + .collect(); + let custom_handlers = handler_strs?.join(", "); + self.writeln(&format!( + "const {} = {{ ...Lux.defaultHandlers, {} }};", + handlers_var, custom_handlers + )); + } + + // Set has_handlers and emit the expression + let prev_has_handlers = self.has_handlers; + self.has_handlers = true; + + // For function calls inside run, we need to pass the handlers + // Save the current handlers variable name for use in calls + let saved_substitutions = self.var_substitutions.clone(); + self.var_substitutions + .insert("handlers".to_string(), handlers_var.clone()); + + // Emit the expression with the custom handlers + let inner_code = self.emit_expr(expr)?; + + self.var_substitutions = saved_substitutions; + self.has_handlers = prev_has_handlers; + + Ok(inner_code) + } + + Expr::Resume { value, .. } => { + let val = self.emit_expr(value)?; + Ok(format!("resume({})", val)) + } + } + } + + /// Emit a match expression + fn emit_match(&mut self, scrutinee: &Expr, arms: &[MatchArm]) -> Result { + let scrutinee_code = self.emit_expr(scrutinee)?; + let scrutinee_var = format!("_match_{}", self.fresh_name()); + let result_var = format!("_result_{}", self.fresh_name()); + + self.writeln(&format!("const {} = {};", scrutinee_var, scrutinee_code)); + self.writeln(&format!("let {};", result_var)); + + for (i, arm) in arms.iter().enumerate() { + let condition = self.emit_pattern_condition(&arm.pattern, &scrutinee_var)?; + let bindings = self.emit_pattern_bindings(&arm.pattern, &scrutinee_var)?; + + if i == 0 { + self.writeln(&format!("if ({}) {{", condition)); + } else { + self.writeln(&format!("}} else if ({}) {{", condition)); + } + + self.indent += 1; + + // Emit bindings + for (name, code) in &bindings { + self.writeln(&format!("const {} = {};", name, code)); + self.var_substitutions.insert(name.clone(), name.clone()); + } + + // Emit guard if present + if let Some(guard) = &arm.guard { + let guard_code = self.emit_expr(guard)?; + self.writeln(&format!("if ({}) {{", guard_code)); + self.indent += 1; + } + + // Emit body + let body_code = self.emit_expr(&arm.body)?; + self.writeln(&format!("{} = {};", result_var, body_code)); + + if arm.guard.is_some() { + self.indent -= 1; + self.writeln("}"); + } + + // Remove bindings + for (name, _) in &bindings { + self.var_substitutions.remove(name); + } + + self.indent -= 1; + } + + self.writeln("} else {"); + self.indent += 1; + self.writeln(&format!( + "throw new Error('Non-exhaustive match on ' + JSON.stringify({}));", + scrutinee_var + )); + self.indent -= 1; + self.writeln("}"); + + Ok(result_var) + } + + /// Emit a condition that checks if a pattern matches + fn emit_pattern_condition( + &self, + pattern: &Pattern, + scrutinee: &str, + ) -> Result { + match pattern { + Pattern::Wildcard(_) => Ok("true".to_string()), + Pattern::Var(_) => Ok("true".to_string()), + Pattern::Literal(lit) => { + let lit_code = self.emit_literal(lit)?; + Ok(format!("{} === {}", scrutinee, lit_code)) + } + Pattern::Constructor { name, fields, .. } => { + let mut conditions = vec![format!("{}.tag === \"{}\"", scrutinee, name.name)]; + + for (i, field_pattern) in fields.iter().enumerate() { + // Use named fields for built-in types + let field_access = match (name.name.as_str(), i) { + ("Some", 0) => format!("{}.value", scrutinee), + ("Ok", 0) => format!("{}.value", scrutinee), + ("Err", 0) => format!("{}.error", scrutinee), + _ => format!("{}.field{}", scrutinee, i), + }; + let field_cond = self.emit_pattern_condition(field_pattern, &field_access)?; + if field_cond != "true" { + conditions.push(field_cond); + } + } + + Ok(conditions.join(" && ")) + } + Pattern::Record { fields, .. } => { + let mut conditions = vec![]; + for (field_name, field_pattern) in fields { + let field_access = format!("{}.{}", scrutinee, field_name.name); + let field_cond = self.emit_pattern_condition(field_pattern, &field_access)?; + if field_cond != "true" { + conditions.push(field_cond); + } + } + if conditions.is_empty() { + Ok("true".to_string()) + } else { + Ok(conditions.join(" && ")) + } + } + Pattern::Tuple { elements, .. } => { + let mut conditions = vec![]; + for (i, elem_pattern) in elements.iter().enumerate() { + let elem_access = format!("{}[{}]", scrutinee, i); + let elem_cond = self.emit_pattern_condition(elem_pattern, &elem_access)?; + if elem_cond != "true" { + conditions.push(elem_cond); + } + } + if conditions.is_empty() { + Ok("true".to_string()) + } else { + Ok(conditions.join(" && ")) + } + } + } + } + + /// Extract variable bindings from a pattern + fn emit_pattern_bindings( + &self, + pattern: &Pattern, + scrutinee: &str, + ) -> Result, JsGenError> { + let mut bindings = vec![]; + + match pattern { + Pattern::Wildcard(_) => {} + Pattern::Var(ident) => { + bindings.push((ident.name.clone(), scrutinee.to_string())); + } + Pattern::Literal(_) => {} + Pattern::Constructor { name, fields, .. } => { + for (i, field_pattern) in fields.iter().enumerate() { + // Use named fields for built-in types + let field_access = match (name.name.as_str(), i) { + ("Some", 0) => format!("{}.value", scrutinee), + ("Ok", 0) => format!("{}.value", scrutinee), + ("Err", 0) => format!("{}.error", scrutinee), + _ => format!("{}.field{}", scrutinee, i), + }; + let field_bindings = self.emit_pattern_bindings(field_pattern, &field_access)?; + bindings.extend(field_bindings); + } + } + Pattern::Record { fields, .. } => { + for (field_name, field_pattern) in fields { + let field_access = format!("{}.{}", scrutinee, field_name.name); + let field_bindings = + self.emit_pattern_bindings(field_pattern, &field_access)?; + bindings.extend(field_bindings); + } + } + Pattern::Tuple { elements, .. } => { + for (i, elem_pattern) in elements.iter().enumerate() { + let elem_access = format!("{}[{}]", scrutinee, i); + let elem_bindings = self.emit_pattern_bindings(elem_pattern, &elem_access)?; + bindings.extend(elem_bindings); + } + } + } + + Ok(bindings) + } + + /// Emit List module operations + fn emit_list_operation( + &mut self, + operation: &str, + args: &[Expr], + ) -> Result { + match operation { + "map" => { + let list = self.emit_expr(&args[0])?; + let func = self.emit_expr(&args[1])?; + Ok(format!("{}.map({})", list, func)) + } + "filter" => { + let list = self.emit_expr(&args[0])?; + let pred = self.emit_expr(&args[1])?; + Ok(format!("{}.filter({})", list, pred)) + } + "foldl" | "fold" => { + let list = self.emit_expr(&args[0])?; + let init = self.emit_expr(&args[1])?; + let func = self.emit_expr(&args[2])?; + Ok(format!("{}.reduce({}, {})", list, func, init)) + } + "foldr" => { + let list = self.emit_expr(&args[0])?; + let init = self.emit_expr(&args[1])?; + let func = self.emit_expr(&args[2])?; + Ok(format!("{}.reduceRight({}, {})", list, func, init)) + } + "length" => { + let list = self.emit_expr(&args[0])?; + Ok(format!("{}.length", list)) + } + "head" => { + let list = self.emit_expr(&args[0])?; + Ok(format!( + "({}.length > 0 ? Lux.Some({}[0]) : Lux.None())", + list, list + )) + } + "tail" => { + let list = self.emit_expr(&args[0])?; + Ok(format!( + "({}.length > 0 ? Lux.Some({}.slice(1)) : Lux.None())", + list, list + )) + } + "isEmpty" => { + let list = self.emit_expr(&args[0])?; + Ok(format!("{}.length === 0", list)) + } + "append" => { + let list1 = self.emit_expr(&args[0])?; + let list2 = self.emit_expr(&args[1])?; + Ok(format!("[...{}, ...{}]", list1, list2)) + } + "reverse" => { + let list = self.emit_expr(&args[0])?; + Ok(format!("[...{}].reverse()", list)) + } + "range" => { + let start = self.emit_expr(&args[0])?; + let end = self.emit_expr(&args[1])?; + Ok(format!( + "Array.from({{ length: {} - {} + 1 }}, (_, i) => {} + i)", + end, start, start + )) + } + _ => Err(JsGenError { + message: format!("Unknown List operation: {}", operation), + span: None, + }), + } + } + + /// Emit a literal value + fn emit_literal(&self, lit: &Literal) -> Result { + match &lit.kind { + LiteralKind::Int(n) => Ok(n.to_string()), + LiteralKind::Float(f) => { + if f.is_infinite() { + if *f > 0.0 { + Ok("Infinity".to_string()) + } else { + Ok("-Infinity".to_string()) + } + } else if f.is_nan() { + Ok("NaN".to_string()) + } else { + Ok(format!("{}", f)) + } + } + LiteralKind::String(s) => { + // Escape special characters for JS string + let escaped = s + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t"); + Ok(format!("\"{}\"", escaped)) + } + LiteralKind::Char(c) => { + // Chars become single-character strings in JS + Ok(format!("\"{}\"", c)) + } + LiteralKind::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()), + LiteralKind::Unit => Ok("undefined".to_string()), + } + } + + /// Check if an expression produces a string + fn is_string_expr(&self, expr: &Expr) -> bool { + match expr { + Expr::Literal(lit) => matches!(lit.kind, LiteralKind::String(_)), + Expr::Call { func, .. } => { + if let Expr::Var(ident) = func.as_ref() { + ident.name == "toString" + } else { + false + } + } + Expr::BinaryOp { op, left, right, .. } => { + matches!(op, BinaryOp::Add) + && (self.is_string_expr(left) || self.is_string_expr(right)) + } + _ => false, + } + } + + /// Mangle a Lux name to a valid JavaScript name + fn mangle_name(&self, name: &str) -> String { + format!("{}_lux", name) + } + + /// Escape JavaScript reserved keywords + fn escape_js_keyword(&self, name: &str) -> String { + match name { + "break" | "case" | "catch" | "continue" | "debugger" | "default" | "delete" | "do" + | "else" | "finally" | "for" | "function" | "if" | "in" | "instanceof" | "new" + | "return" | "switch" | "this" | "throw" | "try" | "typeof" | "var" | "void" + | "while" | "with" | "class" | "const" | "enum" | "export" | "extends" | "import" + | "super" | "implements" | "interface" | "let" | "package" | "private" + | "protected" | "public" | "static" | "yield" | "await" | "async" => { + format!("{}_", name) + } + _ => name.to_string(), + } + } + + /// Generate a fresh unique name + fn fresh_name(&mut self) -> usize { + let n = self.name_counter; + self.name_counter += 1; + n + } + + /// Write a line with current indentation + fn writeln(&mut self, s: &str) { + for _ in 0..self.indent { + self.output.push_str(" "); + } + self.output.push_str(s); + self.output.push('\n'); + } +} + +impl Default for JsBackend { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_literal_int() { + let backend = JsBackend::new(); + let lit = Literal { + kind: LiteralKind::Int(42), + span: Span::default(), + }; + assert_eq!(backend.emit_literal(&lit).unwrap(), "42"); + } + + #[test] + fn test_literal_string() { + let backend = JsBackend::new(); + let lit = Literal { + kind: LiteralKind::String("hello\nworld".to_string()), + span: Span::default(), + }; + assert_eq!(backend.emit_literal(&lit).unwrap(), "\"hello\\nworld\""); + } + + #[test] + fn test_literal_bool() { + let backend = JsBackend::new(); + let lit = Literal { + kind: LiteralKind::Bool(true), + span: Span::default(), + }; + assert_eq!(backend.emit_literal(&lit).unwrap(), "true"); + } + + #[test] + fn test_mangle_name() { + let backend = JsBackend::new(); + assert_eq!(backend.mangle_name("foo"), "foo_lux"); + assert_eq!(backend.mangle_name("main"), "main_lux"); + } + + #[test] + fn test_escape_keywords() { + let backend = JsBackend::new(); + assert_eq!(backend.escape_js_keyword("class"), "class_"); + assert_eq!(backend.escape_js_keyword("foo"), "foo"); + } +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 1c45843..50b0f53 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -2,10 +2,13 @@ //! //! This module provides compilation to various targets: //! - C: For native compilation via GCC/Clang -//! - JavaScript: For frontend/browser deployment (planned) +//! - JavaScript: For frontend/browser deployment //! - WebAssembly: For portable deployment (planned) pub mod c_backend; +pub mod js_backend; #[allow(unused_imports)] pub use c_backend::CBackend; +#[allow(unused_imports)] +pub use js_backend::JsBackend; diff --git a/src/main.rs b/src/main.rs index 481f278..3879adf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -140,20 +140,31 @@ fn main() { handle_pkg_command(&args[2..]); } "compile" => { - // Compile to native binary + // Compile to native binary or JavaScript if args.len() < 3 { eprintln!("Usage: lux compile [-o binary]"); eprintln!(" lux compile --run"); eprintln!(" lux compile --emit-c [-o file.c]"); + eprintln!(" lux compile --target js [-o file.js]"); std::process::exit(1); } let run_after = args.iter().any(|a| a == "--run"); 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() .position(|a| a == "-o") .and_then(|i| args.get(i + 1)) .map(|s| s.as_str()); - compile_to_c(&args[2], output_path, run_after, emit_c); + + if target_js { + compile_to_js(&args[2], output_path, run_after); + } else { + compile_to_c(&args[2], output_path, run_after, emit_c); + } } path => { // Run a file @@ -176,6 +187,7 @@ fn print_help() { println!(" lux compile -o app Compile to binary named 'app'"); println!(" lux compile --run Compile and execute"); println!(" lux compile --emit-c Output C code instead of binary"); + println!(" lux compile --target js Compile to JavaScript"); println!(" lux fmt Format a file (--check to verify only)"); println!(" lux check Type check without running"); 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]) { use std::path::Path; use std::fs;