Compare commits
7 Commits
v0.1.12
...
81e58cf3d5
| Author | SHA1 | Date | |
|---|---|---|---|
| 81e58cf3d5 | |||
| 92d443e475 | |||
| fe30206cd0 | |||
| 563d62f526 | |||
| e9ec1bb84d | |||
| e46afd98eb | |||
| 64f33e4e4b |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -776,7 +776,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lux"
|
||||
version = "0.1.11"
|
||||
version = "0.1.12"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"lsp-server",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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*" {
|
||||
|
||||
@@ -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");
|
||||
|
||||
90
src/main.rs
90
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);
|
||||
@@ -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""#);
|
||||
|
||||
Reference in New Issue
Block a user