diff --git a/src/codegen/js_backend.rs b/src/codegen/js_backend.rs index db5f6f7..a495ad0 100644 --- a/src/codegen/js_backend.rs +++ b/src/codegen/js_backend.rs @@ -31,6 +31,7 @@ //! ``` use crate::ast::*; +use crate::modules::Module; use std::collections::{HashMap, HashSet}; /// JavaScript code generation errors @@ -75,6 +76,10 @@ pub struct JsBackend { extern_fns: HashMap, /// Extern let names mapped to their JS names extern_lets: HashMap, + /// Module functions: (alias, fn_name) → mangled JS name + module_functions: HashMap<(String, String), String>, + /// Known module aliases + imported_modules: HashSet, } impl JsBackend { @@ -99,13 +104,18 @@ impl JsBackend { used_effects: HashSet::new(), extern_fns: HashMap::new(), extern_lets: HashMap::new(), + module_functions: HashMap::new(), + imported_modules: HashSet::new(), } } /// Generate JavaScript code from a Lux program - pub fn generate(&mut self, program: &Program) -> Result { + pub fn generate(&mut self, program: &Program, modules: &HashMap) -> Result { self.output.clear(); + // Process imported modules before the main program + self.process_imported_modules(program, modules)?; + // First pass: collect all function names, types, and effects for decl in &program.declarations { match decl { @@ -143,6 +153,9 @@ impl JsBackend { // Emit runtime helpers (tree-shaken based on used effects) self.emit_runtime(); + // Emit imported module code (type constructors and functions) + self.emit_module_code(modules, program)?; + // Emit type constructors for decl in &program.declarations { if let Declaration::Type(t) = decl { @@ -323,6 +336,216 @@ impl JsBackend { } } + /// Process imported modules: collect function names, types, and mappings + fn process_imported_modules( + &mut self, + program: &Program, + modules: &HashMap, + ) -> Result<(), JsGenError> { + let mut processed = HashSet::new(); + for import in &program.imports { + let module_path = import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + let alias = if let Some(ref a) = import.alias { + a.name.clone() + } else { + import.path.segments.last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| module_path.clone()) + }; + self.imported_modules.insert(alias.clone()); + self.process_single_module(&alias, &module_path, modules, &mut processed)?; + } + Ok(()) + } + + /// Process a single module and its transitive imports + fn process_single_module( + &mut self, + alias: &str, + module_path: &str, + modules: &HashMap, + processed: &mut HashSet, + ) -> Result<(), JsGenError> { + if processed.contains(module_path) { + return Ok(()); + } + processed.insert(module_path.to_string()); + + let module = match modules.get(module_path) { + Some(m) => m, + None => return Ok(()), // Module not found — might be a built-in + }; + + // Process transitive imports first + for sub_import in &module.program.imports { + let sub_path = sub_import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + let sub_alias = sub_import.path.segments.last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| sub_path.clone()); + self.process_single_module(&sub_alias, &sub_path, modules, processed)?; + } + + // Collect types from the module + for decl in &module.program.declarations { + if let Declaration::Type(t) = decl { + self.collect_type(t)?; + } + } + + // Collect functions from the module + for decl in &module.program.declarations { + if let Declaration::Function(f) = decl { + if f.visibility == Visibility::Public || module.exports.contains(&f.name.name) { + let mangled = format!("{}_{}_lux", alias, f.name.name); + self.functions.insert(mangled.clone()); + self.module_functions.insert( + (alias.to_string(), f.name.name.clone()), + mangled, + ); + + if !f.effects.is_empty() { + self.effectful_functions.insert(format!("{}_{}_lux", alias, f.name.name)); + } + } + } + } + + Ok(()) + } + + /// Emit code for imported modules (type constructors, functions, handlers, let bindings) + fn emit_module_code( + &mut self, + modules: &HashMap, + program: &Program, + ) -> Result<(), JsGenError> { + if self.module_functions.is_empty() { + return Ok(()); + } + + self.writeln("// === Imported Modules ==="); + self.writeln(""); + + let mut processed = HashSet::new(); + for import in &program.imports { + let module_path = import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + let alias = if let Some(ref a) = import.alias { + a.name.clone() + } else { + import.path.segments.last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| module_path.clone()) + }; + self.emit_module_code_recursive(&alias, &module_path, modules, &mut processed)?; + } + + Ok(()) + } + + fn emit_module_code_recursive( + &mut self, + alias: &str, + module_path: &str, + modules: &HashMap, + processed: &mut HashSet, + ) -> Result<(), JsGenError> { + if processed.contains(module_path) { + return Ok(()); + } + processed.insert(module_path.to_string()); + + let module = match modules.get(module_path) { + Some(m) => m.clone(), + None => return Ok(()), + }; + + // Process transitive imports first + for sub_import in &module.program.imports { + let sub_path = sub_import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + let sub_alias = sub_import.path.segments.last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| sub_path.clone()); + self.emit_module_code_recursive(&sub_alias, &sub_path, modules, processed)?; + } + + // Emit type constructors + for decl in &module.program.declarations { + if let Declaration::Type(t) = decl { + self.emit_type_constructors(t)?; + } + } + + // Emit functions with module-prefixed names + for decl in &module.program.declarations { + if let Declaration::Function(f) = decl { + if f.visibility == Visibility::Public || module.exports.contains(&f.name.name) { + let mangled = format!("{}_{}_lux", alias, f.name.name); + self.emit_function_with_name(f, &mangled)?; + } + } + } + + Ok(()) + } + + /// Emit a function with a custom mangled name (for module functions) + fn emit_function_with_name( + &mut self, + func: &FunctionDecl, + mangled_name: &str, + ) -> Result<(), JsGenError> { + 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 {}({}) {{", + mangled_name, + params.join(", ") + )); + self.indent += 1; + + // Set context for effect handling + let prev_has_handlers = self.has_handlers; + self.has_handlers = is_effectful; + + // Save and clear var substitutions for this function scope + let saved_substitutions = self.var_substitutions.clone(); + 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.var_substitutions = saved_substitutions; + + self.indent -= 1; + self.writeln("}"); + self.writeln(""); + + Ok(()) + } + /// Emit the Lux runtime, tree-shaken based on used effects fn emit_runtime(&mut self) { let uses_console = self.used_effects.contains("Console"); @@ -1315,6 +1538,34 @@ impl JsBackend { if module_name.name == "Ref" { return self.emit_ref_operation(&field.name, args); } + + // Check for user-defined module function + let key = (module_name.name.clone(), field.name.clone()); + if let Some(js_name) = self.module_functions.get(&key).cloned() { + let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); + let args_str = arg_strs?.join(", "); + let is_effectful = self.effectful_functions.contains(&js_name); + + if is_effectful && self.has_handlers { + let handlers_name = self.var_substitutions + .get("handlers") + .cloned() + .unwrap_or_else(|| "handlers".to_string()); + return if args_str.is_empty() { + Ok(format!("{}({})", js_name, handlers_name)) + } else { + Ok(format!("{}({}, {})", js_name, handlers_name, args_str)) + }; + } else if is_effectful { + return if args_str.is_empty() { + Ok(format!("{}(Lux.defaultHandlers)", js_name)) + } else { + Ok(format!("{}(Lux.defaultHandlers, {})", js_name, args_str)) + }; + } else { + return Ok(format!("{}({})", js_name, args_str)); + } + } } } @@ -1471,6 +1722,34 @@ impl JsBackend { return self.emit_html_operation(&operation.name, args); } + // Check for user-defined module function + let key = (effect.name.clone(), operation.name.clone()); + if let Some(js_name) = self.module_functions.get(&key).cloned() { + let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); + let args_str = arg_strs?.join(", "); + let is_effectful = self.effectful_functions.contains(&js_name); + + if is_effectful && self.has_handlers { + let handlers_name = self.var_substitutions + .get("handlers") + .cloned() + .unwrap_or_else(|| "handlers".to_string()); + return if args_str.is_empty() { + Ok(format!("{}({})", js_name, handlers_name)) + } else { + Ok(format!("{}({}, {})", js_name, handlers_name, args_str)) + }; + } else if is_effectful { + return if args_str.is_empty() { + Ok(format!("{}(Lux.defaultHandlers)", js_name)) + } else { + Ok(format!("{}(Lux.defaultHandlers, {})", js_name, args_str)) + }; + } else { + return Ok(format!("{}({})", js_name, args_str)); + } + } + let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); let args_str = arg_strs?.join(", "); @@ -3026,7 +3305,7 @@ mod tests { let mut backend = JsBackend::new(); let js_code = backend - .generate(&program) + .generate(&program, &std::collections::HashMap::new()) .map_err(|e| format!("Codegen error: {}", e))?; let output = Command::new("node") @@ -4286,7 +4565,7 @@ line3" let program = Parser::parse_source(source).expect("Should parse"); let mut backend = JsBackend::new(); - let js_code = backend.generate(&program).expect("Should generate"); + let js_code = backend.generate(&program, &std::collections::HashMap::new()).expect("Should generate"); // Core runtime is always present assert!(js_code.contains("const Lux = {"), "Lux object should be defined"); @@ -4318,7 +4597,7 @@ line3" let program = Parser::parse_source(source).expect("Should parse"); let mut backend = JsBackend::new(); - let js_code = backend.generate(&program).expect("Should generate"); + let js_code = backend.generate(&program, &std::collections::HashMap::new()).expect("Should generate"); assert!(js_code.contains("Console:"), "Console handler should exist"); assert!(js_code.contains("Dom:"), "Dom handler should exist"); @@ -4341,7 +4620,7 @@ line3" let program = Parser::parse_source(source).expect("Should parse"); let mut backend = JsBackend::new(); - let js_code = backend.generate(&program).expect("Should generate"); + let js_code = backend.generate(&program, &std::collections::HashMap::new()).expect("Should generate"); // Only Console should be present assert!(js_code.contains("Console:"), "Console handler should exist"); diff --git a/src/main.rs b/src/main.rs index eca6308..1833b8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -997,7 +997,7 @@ fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) { // Generate JavaScript code let mut backend = JsBackend::new(); - let js_code = match backend.generate(&program) { + let js_code = match backend.generate(&program, loader.module_cache()) { Ok(code) => code, Err(e) => { eprintln!("{} JS codegen: {}", c(colors::RED, "error:"), e); @@ -4253,7 +4253,7 @@ c")"#; let tokens = Lexer::new(source).tokenize().unwrap(); let program = Parser::new(tokens).parse_program().unwrap(); let mut backend = JsBackend::new(); - let js = backend.generate(&program).unwrap(); + let js = backend.generate(&program, &std::collections::HashMap::new()).unwrap(); // getElementById should appear as-is (no _lux suffix) assert!(js.contains("getElementById("), "JS should call getElementById directly: {}", js); @@ -4280,7 +4280,7 @@ c")"#; let tokens = Lexer::new(source).tokenize().unwrap(); let program = Parser::new(tokens).parse_program().unwrap(); let mut backend = JsBackend::new(); - let js = backend.generate(&program).unwrap(); + let js = backend.generate(&program, &std::collections::HashMap::new()).unwrap(); assert!(js.contains("Lux.Some"), "JS should contain Lux.Some for List.get: {}", js); assert!(js.contains("Lux.None"), "JS should contain Lux.None for List.get: {}", js); @@ -4301,7 +4301,7 @@ c")"#; let tokens = Lexer::new(source).tokenize().unwrap(); let program = Parser::new(tokens).parse_program().unwrap(); let mut backend = JsBackend::new(); - let js = backend.generate(&program).unwrap(); + let js = backend.generate(&program, &std::collections::HashMap::new()).unwrap(); // Should contain the let binding assert!(js.contains("const main"), "JS should contain 'const main': {}", js); @@ -4338,7 +4338,7 @@ c")"#; let tokens = Lexer::new(source).tokenize().unwrap(); let program = Parser::new(tokens).parse_program().unwrap(); let mut backend = JsBackend::new(); - let js = backend.generate(&program).unwrap(); + let js = backend.generate(&program, &std::collections::HashMap::new()).unwrap(); // Handler should be emitted as a const object assert!(js.contains("const consoleLogger_lux"), "JS should contain handler const: {}", js);