7 Commits

Author SHA1 Message Date
81e58cf3d5 Fix Option<String> pattern match double-dereference in C codegen
LuxString is typedef char* but the codegen treated it as a struct type,
generating *(LuxString*)(field0) instead of (LuxString)(field0). This
caused a heap-buffer-overflow on any Option<String> pattern match since
it read the string contents as a memory address.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:59:47 -05:00
92d443e475 chore: bump version to 0.1.13 2026-02-20 20:41:01 -05:00
fe30206cd0 add cargo lock 2026-02-20 20:40:55 -05:00
563d62f526 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>
2026-02-20 20:38:36 -05:00
e9ec1bb84d feat: add handler declaration codegen to JS backend
Handler declarations now emit as JavaScript objects with operation
methods. Each operation defines resume as an identity function,
matching the simple handler model used by the interpreter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:31:10 -05:00
e46afd98eb feat: auto-invoke let main in JS backend
The JS backend now detects `let main = fn() => ...` patterns and
auto-invokes them at the end of the generated code, matching the
interpreter's behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:24:47 -05:00
64f33e4e4b feat: add List.get support to JS backend
List.get(list, index) now correctly compiles to JavaScript, returning
Lux.Some(value) for valid indices and Lux.None() for out-of-bounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:22:15 -05:00
6 changed files with 459 additions and 15 deletions

2
Cargo.lock generated
View File

@@ -776,7 +776,7 @@ dependencies = [
[[package]]
name = "lux"
version = "0.1.11"
version = "0.1.12"
dependencies = [
"glob",
"lsp-server",

View File

@@ -1,6 +1,6 @@
[package]
name = "lux"
version = "0.1.12"
version = "0.1.13"
edition = "2021"
description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
license = "MIT"

View File

@@ -44,7 +44,7 @@
printf "\n"
printf " \033[1;35m \033[0m\n"
printf " \033[1;35m \033[0m\n"
printf " \033[1;35m \033[0m v0.1.12\n"
printf " \033[1;35m \033[0m v0.1.13\n"
printf "\n"
printf " Functional language with first-class effects\n"
printf "\n"
@@ -62,7 +62,7 @@
packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "lux";
version = "0.1.12";
version = "0.1.13";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
@@ -79,7 +79,7 @@
};
in muslPkgs.rustPlatform.buildRustPackage {
pname = "lux";
version = "0.1.12";
version = "0.1.13";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;

View File

@@ -5316,11 +5316,12 @@ impl CBackend {
if Self::is_primitive_c_type(&actual_type) {
// For primitive types stored as boxed void*, dereference
self.writeln(&format!("{} {} = *({}*)({});", actual_type, var_name, actual_type, c_expr));
} else if !actual_type.ends_with('*') && actual_type != "void" {
} else if actual_type == "LuxString" || actual_type.ends_with('*') || actual_type == "void" {
// Pointer types (including LuxString which is typedef char*): simple cast
self.writeln(&format!("{} {} = ({})({});", actual_type, var_name, actual_type, c_expr));
} else {
// Struct types: cast to pointer and dereference
self.writeln(&format!("{} {} = *({}*)({});", actual_type, var_name, actual_type, c_expr));
} else {
self.writeln(&format!("{} {} = ({})({});", actual_type, var_name, actual_type, c_expr));
}
self.var_types.insert(var_name.clone(), actual_type);
} else if actual_type.ends_with('*') && actual_type != "void*" {

View File

@@ -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<String, String>,
/// Extern let names mapped to their JS names
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 {
@@ -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<String, JsGenError> {
pub fn generate(&mut self, program: &Program, modules: &HashMap<String, Module>) -> Result<String, JsGenError> {
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 {
@@ -157,6 +170,13 @@ impl JsBackend {
}
}
// Emit handlers
for decl in &program.declarations {
if let Declaration::Handler(h) = decl {
self.emit_handler(h)?;
}
}
// Check if any top-level let calls main (to avoid double invocation)
let has_main_call = program.declarations.iter().any(|decl| {
if let Declaration::Let(l) = decl {
@@ -187,6 +207,20 @@ impl JsBackend {
}
}
// Check for `let main = fn() => ...` pattern (not tracked in self.functions)
let has_let_main = program.declarations.iter().any(|d| {
if let Declaration::Let(l) = d {
l.name.name == "main" && matches!(&l.value, Expr::Lambda { .. })
} else {
false
}
});
if has_let_main && !self.functions.contains("main") && !has_main_call {
self.writeln("");
self.writeln("// Entry point (let main)");
self.writeln("main();");
}
Ok(self.output.clone())
}
@@ -302,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
fn emit_runtime(&mut self) {
let uses_console = self.used_effects.contains("Console");
@@ -1066,6 +1310,55 @@ impl JsBackend {
Ok(())
}
/// Emit a handler declaration as a JS object
fn emit_handler(&mut self, handler: &HandlerDecl) -> Result<(), JsGenError> {
let handler_name = self.mangle_name(&handler.name.name);
self.writeln(&format!("const {} = {{", handler_name));
self.indent += 1;
for (i, imp) in handler.implementations.iter().enumerate() {
// Build parameter list for this operation (just the effect op params, not resume)
let params: Vec<String> = imp.params.iter().map(|p| p.name.clone()).collect();
self.writeln(&format!(
"{}: function({}) {{",
imp.op_name.name,
params.join(", ")
));
self.indent += 1;
// Set up handler context — handlers can use effects
let prev_has_handlers = self.has_handlers;
self.has_handlers = true;
let saved_substitutions = self.var_substitutions.clone();
// In the simple handler model, resume is the identity function.
// Expr::Resume nodes emit `resume(val)`, so define it in every operation.
self.writeln("const resume = (x) => x;");
let body_code = self.emit_expr(&imp.body)?;
self.writeln(&format!("return {};", body_code));
self.var_substitutions = saved_substitutions;
self.has_handlers = prev_has_handlers;
self.indent -= 1;
if i < handler.implementations.len() - 1 {
self.writeln("},");
} else {
self.writeln("}");
}
}
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)?;
@@ -1245,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<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));
}
}
}
}
@@ -1401,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<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 args_str = arg_strs?.join(", ");
@@ -1917,6 +2266,14 @@ impl JsBackend {
let func = self.emit_expr(&args[1])?;
Ok(format!("({}.forEach({}), undefined)", list, func))
}
"get" => {
let list = self.emit_expr(&args[0])?;
let idx = self.emit_expr(&args[1])?;
Ok(format!(
"((__i, __l) => (__i >= 0 && __i < __l.length) ? Lux.Some(__l[__i]) : Lux.None())({}, {})",
idx, list
))
}
_ => Err(JsGenError {
message: format!("Unknown List operation: {}", operation),
span: None,
@@ -2948,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")
@@ -4208,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");
@@ -4240,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");
@@ -4263,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");

View File

@@ -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);
@@ -4263,6 +4263,92 @@ c")"#;
assert!(js.contains("main_lux"), "main should be mangled: {}", js);
}
#[test]
fn test_list_get_js_codegen() {
use crate::codegen::js_backend::JsBackend;
use crate::parser::Parser;
use crate::lexer::Lexer;
let source = r#"
fn main(): Unit = {
let xs = [10, 20, 30]
let result = List.get(xs, 1)
()
}
"#;
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, &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);
}
#[test]
fn test_let_main_js_codegen() {
use crate::codegen::js_backend::JsBackend;
use crate::parser::Parser;
use crate::lexer::Lexer;
let source = r#"
let main = fn() => {
print("hello from let main")
}
"#;
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, &std::collections::HashMap::new()).unwrap();
// Should contain the let binding
assert!(js.contains("const main"), "JS should contain 'const main': {}", js);
// Should auto-invoke main()
assert!(js.contains("main();"), "JS should auto-invoke main(): {}", js);
// Should NOT contain main_lux (let bindings aren't mangled)
assert!(!js.contains("main_lux"), "let main should not be mangled: {}", js);
}
#[test]
fn test_handler_js_codegen() {
use crate::codegen::js_backend::JsBackend;
use crate::parser::Parser;
use crate::lexer::Lexer;
let source = r#"
effect Log {
fn info(msg: String): Unit
fn debug(msg: String): Unit
}
handler consoleLogger: Log {
fn info(msg) = {
Console.print("[INFO] " + msg)
resume(())
}
fn debug(msg) = {
Console.print("[DEBUG] " + msg)
resume(())
}
}
"#;
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, &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);
// Should have operation methods
assert!(js.contains("info: function(msg)"), "JS should contain info operation: {}", js);
assert!(js.contains("debug: function(msg)"), "JS should contain debug operation: {}", js);
// Should define resume locally
assert!(js.contains("const resume = (x) => x"), "JS should define resume: {}", js);
}
#[test]
fn test_invalid_escape_sequence() {
let result = eval(r#"let x = "\z""#);