feat: add module import support to JS backend

The JS backend now processes imported modules, emitting their type
constructors and functions with module-prefixed mangled names. Module
function calls (both via Expr::Call with Expr::Field and via
Expr::EffectOp) are resolved to the correct mangled names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 20:38:36 -05:00
parent e9ec1bb84d
commit 563d62f526
2 changed files with 289 additions and 10 deletions

View File

@@ -31,6 +31,7 @@
//! ``` //! ```
use crate::ast::*; use crate::ast::*;
use crate::modules::Module;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
/// JavaScript code generation errors /// JavaScript code generation errors
@@ -75,6 +76,10 @@ pub struct JsBackend {
extern_fns: HashMap<String, String>, extern_fns: HashMap<String, String>,
/// Extern let names mapped to their JS names /// Extern let names mapped to their JS names
extern_lets: HashMap<String, String>, extern_lets: HashMap<String, String>,
/// Module functions: (alias, fn_name) → mangled JS name
module_functions: HashMap<(String, String), String>,
/// Known module aliases
imported_modules: HashSet<String>,
} }
impl JsBackend { impl JsBackend {
@@ -99,13 +104,18 @@ impl JsBackend {
used_effects: HashSet::new(), used_effects: HashSet::new(),
extern_fns: HashMap::new(), extern_fns: HashMap::new(),
extern_lets: HashMap::new(), extern_lets: HashMap::new(),
module_functions: HashMap::new(),
imported_modules: HashSet::new(),
} }
} }
/// Generate JavaScript code from a Lux program /// Generate JavaScript code from a Lux program
pub fn generate(&mut self, program: &Program) -> Result<String, JsGenError> { pub fn generate(&mut self, program: &Program, modules: &HashMap<String, Module>) -> Result<String, JsGenError> {
self.output.clear(); 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 // First pass: collect all function names, types, and effects
for decl in &program.declarations { for decl in &program.declarations {
match decl { match decl {
@@ -143,6 +153,9 @@ impl JsBackend {
// Emit runtime helpers (tree-shaken based on used effects) // Emit runtime helpers (tree-shaken based on used effects)
self.emit_runtime(); self.emit_runtime();
// Emit imported module code (type constructors and functions)
self.emit_module_code(modules, program)?;
// Emit type constructors // Emit type constructors
for decl in &program.declarations { for decl in &program.declarations {
if let Declaration::Type(t) = decl { 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<String, Module>,
) -> 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::<Vec<_>>()
.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<String, Module>,
processed: &mut HashSet<String>,
) -> 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::<Vec<_>>()
.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<String, Module>,
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::<Vec<_>>()
.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<String, Module>,
processed: &mut HashSet<String>,
) -> 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::<Vec<_>>()
.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<String> = 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 /// Emit the Lux runtime, tree-shaken based on used effects
fn emit_runtime(&mut self) { fn emit_runtime(&mut self) {
let uses_console = self.used_effects.contains("Console"); let uses_console = self.used_effects.contains("Console");
@@ -1315,6 +1538,34 @@ impl JsBackend {
if module_name.name == "Ref" { if module_name.name == "Ref" {
return self.emit_ref_operation(&field.name, args); 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<Vec<_>, _> = 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); 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<Vec<_>, _> = 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<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(", ");
@@ -3026,7 +3305,7 @@ mod tests {
let mut backend = JsBackend::new(); let mut backend = JsBackend::new();
let js_code = backend let js_code = backend
.generate(&program) .generate(&program, &std::collections::HashMap::new())
.map_err(|e| format!("Codegen error: {}", e))?; .map_err(|e| format!("Codegen error: {}", e))?;
let output = Command::new("node") let output = Command::new("node")
@@ -4286,7 +4565,7 @@ line3"
let program = Parser::parse_source(source).expect("Should parse"); let program = Parser::parse_source(source).expect("Should parse");
let mut backend = JsBackend::new(); 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 // Core runtime is always present
assert!(js_code.contains("const Lux = {"), "Lux object should be defined"); 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 program = Parser::parse_source(source).expect("Should parse");
let mut backend = JsBackend::new(); 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("Console:"), "Console handler should exist");
assert!(js_code.contains("Dom:"), "Dom 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 program = Parser::parse_source(source).expect("Should parse");
let mut backend = JsBackend::new(); 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 // Only Console should be present
assert!(js_code.contains("Console:"), "Console handler should exist"); assert!(js_code.contains("Console:"), "Console handler should exist");

View File

@@ -997,7 +997,7 @@ fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) {
// Generate JavaScript code // Generate JavaScript code
let mut backend = JsBackend::new(); 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, Ok(code) => code,
Err(e) => { Err(e) => {
eprintln!("{} JS codegen: {}", c(colors::RED, "error:"), e); eprintln!("{} JS codegen: {}", c(colors::RED, "error:"), e);
@@ -4253,7 +4253,7 @@ c")"#;
let tokens = Lexer::new(source).tokenize().unwrap(); let tokens = Lexer::new(source).tokenize().unwrap();
let program = Parser::new(tokens).parse_program().unwrap(); let program = Parser::new(tokens).parse_program().unwrap();
let mut backend = JsBackend::new(); 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) // getElementById should appear as-is (no _lux suffix)
assert!(js.contains("getElementById("), "JS should call getElementById directly: {}", js); assert!(js.contains("getElementById("), "JS should call getElementById directly: {}", js);
@@ -4280,7 +4280,7 @@ c")"#;
let tokens = Lexer::new(source).tokenize().unwrap(); let tokens = Lexer::new(source).tokenize().unwrap();
let program = Parser::new(tokens).parse_program().unwrap(); let program = Parser::new(tokens).parse_program().unwrap();
let mut backend = JsBackend::new(); 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.Some"), "JS should contain Lux.Some for List.get: {}", js);
assert!(js.contains("Lux.None"), "JS should contain Lux.None 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 tokens = Lexer::new(source).tokenize().unwrap();
let program = Parser::new(tokens).parse_program().unwrap(); let program = Parser::new(tokens).parse_program().unwrap();
let mut backend = JsBackend::new(); 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 // Should contain the let binding
assert!(js.contains("const main"), "JS should contain 'const main': {}", js); assert!(js.contains("const main"), "JS should contain 'const main': {}", js);
@@ -4338,7 +4338,7 @@ c")"#;
let tokens = Lexer::new(source).tokenize().unwrap(); let tokens = Lexer::new(source).tokenize().unwrap();
let program = Parser::new(tokens).parse_program().unwrap(); let program = Parser::new(tokens).parse_program().unwrap();
let mut backend = JsBackend::new(); 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 // Handler should be emitted as a const object
assert!(js.contains("const consoleLogger_lux"), "JS should contain handler const: {}", js); assert!(js.contains("const consoleLogger_lux"), "JS should contain handler const: {}", js);