13 Commits

Author SHA1 Message Date
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
293635f415 chore: bump version to 0.1.12 2026-02-20 20:03:04 -05:00
694e4ec999 feat: add Ref cells for mutable state (Ref.new, Ref.get, Ref.set, Ref.update)
Implements WISH-013 mutable state primitives. Ref<T> is a mutable container
using existing module call syntax. Supported across interpreter, JS, and C backends.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:01:29 -05:00
78879ca94e chore: bump version to 0.1.11 2026-02-20 19:36:11 -05:00
01474b401f chore: bump version to 0.1.10 2026-02-20 19:32:56 -05:00
169de0b3c8 chore: update Cargo.lock
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:32:27 -05:00
667a94b4dc feat: add extern let declarations for JS FFI
Add support for `extern let name: Type` and `extern let name: Type = "jsName"`
syntax for declaring external JavaScript values. This follows the same pattern
as extern fn across all compiler passes: parser, typechecker, interpreter
(runtime error placeholder), JS backend (emits JS name directly without
mangling), formatter, linter, modules, and symbol table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:29:44 -05:00
1b629aaae4 feat: add 10 missing List operations to JS backend
Add find, findIndex, any, all, zip, flatten, contains, take, drop,
and forEach to the JS backend's emit_list_operation function. These
operations previously worked in the interpreter and C backend but
caused "Unknown List operation" errors when compiled to JS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:21:26 -05:00
15 changed files with 923 additions and 16 deletions

2
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@@ -223,6 +223,8 @@ pub enum Declaration {
Impl(ImplDecl), Impl(ImplDecl),
/// Extern function declaration (FFI): extern fn name(params): ReturnType /// Extern function declaration (FFI): extern fn name(params): ReturnType
ExternFn(ExternFnDecl), ExternFn(ExternFnDecl),
/// Extern let declaration (FFI): extern let name: Type
ExternLet(ExternLetDecl),
} }
/// Function declaration /// Function declaration
@@ -445,6 +447,19 @@ pub struct ExternFnDecl {
pub span: Span, pub span: Span,
} }
/// Extern let declaration (FFI)
#[derive(Debug, Clone)]
pub struct ExternLetDecl {
pub visibility: Visibility,
/// Documentation comment
pub doc: Option<String>,
pub name: Ident,
pub typ: TypeExpr,
/// Optional JS name override: extern let foo: T = "window.foo"
pub js_name: Option<String>,
pub span: Span,
}
/// Type expressions /// Type expressions
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeExpr { pub enum TypeExpr {

View File

@@ -3285,6 +3285,9 @@ impl CBackend {
if module_name.name == "Map" { if module_name.name == "Map" {
return self.emit_map_operation(&field.name, args); return self.emit_map_operation(&field.name, args);
} }
if module_name.name == "Ref" {
return self.emit_ref_operation(&field.name, args);
}
// Int module // Int module
if module_name.name == "Int" && field.name == "toString" { if module_name.name == "Int" && field.name == "toString" {
let arg = self.emit_expr(&args[0])?; let arg = self.emit_expr(&args[0])?;
@@ -3701,6 +3704,11 @@ impl CBackend {
return self.emit_map_operation(&operation.name, args); return self.emit_map_operation(&operation.name, args);
} }
// Ref module
if effect.name == "Ref" {
return self.emit_ref_operation(&operation.name, args);
}
// Built-in Console effect // Built-in Console effect
if effect.name == "Console" { if effect.name == "Console" {
if operation.name == "print" { if operation.name == "print" {
@@ -5185,6 +5193,42 @@ impl CBackend {
} }
} }
fn emit_ref_operation(&mut self, op: &str, args: &[Expr]) -> Result<String, CGenError> {
match op {
"new" => {
let val = self.emit_expr(&args[0])?;
let boxed = self.box_value(&val, None);
let temp = format!("_ref_new_{}", self.fresh_name());
self.writeln(&format!("void** {} = (void**)malloc(sizeof(void*));", temp));
self.writeln(&format!("*{} = {};", temp, boxed));
Ok(temp)
}
"get" => {
let r = self.emit_expr(&args[0])?;
Ok(format!("(*({})) /* Ref.get */", r))
}
"set" => {
let r = self.emit_expr(&args[0])?;
let val = self.emit_expr(&args[1])?;
let boxed = self.box_value(&val, None);
self.writeln(&format!("*{} = {};", r, boxed));
Ok("0 /* Unit */".to_string())
}
"update" => {
let r = self.emit_expr(&args[0])?;
let f = self.emit_expr(&args[1])?;
let temp = format!("_ref_upd_{}", self.fresh_name());
self.writeln(&format!("void* {} = ((void*(*)(void*)){})(*({}));", temp, f, r));
self.writeln(&format!("*{} = {};", r, temp));
Ok("0 /* Unit */".to_string())
}
_ => Err(CGenError {
message: format!("Unsupported Ref operation: {}", op),
span: None,
}),
}
}
fn emit_expr_with_substitution(&mut self, expr: &Expr, from: &str, to: &str) -> Result<String, CGenError> { fn emit_expr_with_substitution(&mut self, expr: &Expr, from: &str, to: &str) -> Result<String, CGenError> {
// Simple substitution - in a real implementation, this would be more sophisticated // Simple substitution - in a real implementation, this would be more sophisticated
match expr { match expr {

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
@@ -73,6 +74,12 @@ pub struct JsBackend {
used_effects: HashSet<String>, used_effects: HashSet<String>,
/// Extern function names mapped to their JS names /// Extern function names mapped to their JS names
extern_fns: HashMap<String, String>, 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 { impl JsBackend {
@@ -96,13 +103,19 @@ impl JsBackend {
var_substitutions: HashMap::new(), var_substitutions: HashMap::new(),
used_effects: HashSet::new(), used_effects: HashSet::new(),
extern_fns: HashMap::new(), extern_fns: 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 {
@@ -123,6 +136,13 @@ impl JsBackend {
self.extern_fns.insert(ext.name.name.clone(), js_name); self.extern_fns.insert(ext.name.name.clone(), js_name);
self.functions.insert(ext.name.name.clone()); self.functions.insert(ext.name.name.clone());
} }
Declaration::ExternLet(ext) => {
let js_name = ext
.js_name
.clone()
.unwrap_or_else(|| ext.name.name.clone());
self.extern_lets.insert(ext.name.name.clone(), js_name);
}
_ => {} _ => {}
} }
} }
@@ -133,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 {
@@ -147,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) // Check if any top-level let calls main (to avoid double invocation)
let has_main_call = program.declarations.iter().any(|decl| { let has_main_call = program.declarations.iter().any(|decl| {
if let Declaration::Let(l) = decl { if let Declaration::Let(l) = decl {
@@ -177,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()) Ok(self.output.clone())
} }
@@ -292,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");
@@ -1056,6 +1310,55 @@ impl JsBackend {
Ok(()) 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 /// Emit a top-level let binding
fn emit_top_level_let(&mut self, let_decl: &LetDecl) -> Result<(), JsGenError> { fn emit_top_level_let(&mut self, let_decl: &LetDecl) -> Result<(), JsGenError> {
let val = self.emit_expr(&let_decl.value)?; let val = self.emit_expr(&let_decl.value)?;
@@ -1097,6 +1400,9 @@ impl JsBackend {
} else if self.functions.contains(&ident.name) { } else if self.functions.contains(&ident.name) {
// Function reference (used as value) // Function reference (used as value)
Ok(self.mangle_name(&ident.name)) Ok(self.mangle_name(&ident.name))
} else if let Some(js_name) = self.extern_lets.get(&ident.name) {
// Extern let: use JS name directly (no mangling)
Ok(js_name.clone())
} else { } else {
Ok(self.escape_js_keyword(&ident.name)) Ok(self.escape_js_keyword(&ident.name))
} }
@@ -1229,6 +1535,37 @@ impl JsBackend {
if module_name.name == "Map" { if module_name.name == "Map" {
return self.emit_map_operation(&field.name, args); return self.emit_map_operation(&field.name, args);
} }
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));
}
}
} }
} }
@@ -1375,11 +1712,44 @@ impl JsBackend {
return self.emit_map_operation(&operation.name, args); return self.emit_map_operation(&operation.name, args);
} }
// Special case: Ref module operations (not an effect)
if effect.name == "Ref" {
return self.emit_ref_operation(&operation.name, args);
}
// Special case: Html module operations (not an effect) // Special case: Html module operations (not an effect)
if effect.name == "Html" { if effect.name == "Html" {
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(", ");
@@ -1835,6 +2205,75 @@ impl JsBackend {
let func = self.emit_expr(&args[1])?; let func = self.emit_expr(&args[1])?;
Ok(format!("[...{}].sort({})", list, func)) Ok(format!("[...{}].sort({})", list, func))
} }
"find" => {
let list = self.emit_expr(&args[0])?;
let pred = self.emit_expr(&args[1])?;
Ok(format!(
"((__l) => {{ const __r = __l.find({}); return __r !== undefined ? Lux.Some(__r) : Lux.None(); }})({})",
pred, list
))
}
"findIndex" => {
let list = self.emit_expr(&args[0])?;
let pred = self.emit_expr(&args[1])?;
Ok(format!(
"((__l) => {{ const __i = __l.findIndex({}); return __i !== -1 ? Lux.Some(__i) : Lux.None(); }})({})",
pred, list
))
}
"any" => {
let list = self.emit_expr(&args[0])?;
let pred = self.emit_expr(&args[1])?;
Ok(format!("{}.some({})", list, pred))
}
"all" => {
let list = self.emit_expr(&args[0])?;
let pred = self.emit_expr(&args[1])?;
Ok(format!("{}.every({})", list, pred))
}
"zip" => {
let list1 = self.emit_expr(&args[0])?;
let list2 = self.emit_expr(&args[1])?;
Ok(format!(
"((__a, __b) => __a.slice(0, Math.min(__a.length, __b.length)).map((__x, __i) => [__x, __b[__i]]))({})",
format!("{}, {}", list1, list2)
))
}
"flatten" => {
let list = self.emit_expr(&args[0])?;
Ok(format!("{}.flat()", list))
}
"contains" => {
let list = self.emit_expr(&args[0])?;
let elem = self.emit_expr(&args[1])?;
Ok(format!(
"{}.some(__x => JSON.stringify(__x) === JSON.stringify({}))",
list, elem
))
}
"take" => {
let list = self.emit_expr(&args[0])?;
let n = self.emit_expr(&args[1])?;
Ok(format!("{}.slice(0, Math.max(0, {}))", list, n))
}
"drop" => {
let list = self.emit_expr(&args[0])?;
let n = self.emit_expr(&args[1])?;
Ok(format!("{}.slice(Math.max(0, {}))", list, n))
}
"forEach" => {
let list = self.emit_expr(&args[0])?;
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 { _ => Err(JsGenError {
message: format!("Unknown List operation: {}", operation), message: format!("Unknown List operation: {}", operation),
span: None, span: None,
@@ -2412,6 +2851,37 @@ impl JsBackend {
} }
} }
fn emit_ref_operation(
&mut self,
operation: &str,
args: &[Expr],
) -> Result<String, JsGenError> {
match operation {
"new" => {
let val = self.emit_expr(&args[0])?;
Ok(format!("({{value: {}}})", val))
}
"get" => {
let r = self.emit_expr(&args[0])?;
Ok(format!("({}.value)", r))
}
"set" => {
let r = self.emit_expr(&args[0])?;
let val = self.emit_expr(&args[1])?;
Ok(format!("({}.value = {}, undefined)", r, val))
}
"update" => {
let r = self.emit_expr(&args[0])?;
let f = self.emit_expr(&args[1])?;
Ok(format!("({0}.value = {1}({0}.value), undefined)", r, f))
}
_ => Err(JsGenError {
message: format!("Unknown Ref operation: {}", operation),
span: None,
}),
}
}
/// Emit Html module operations for type-safe HTML construction /// Emit Html module operations for type-safe HTML construction
fn emit_html_operation( fn emit_html_operation(
&mut self, &mut self,
@@ -2835,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")
@@ -4095,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");
@@ -4127,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");
@@ -4150,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

@@ -3,7 +3,7 @@
//! Formats Lux source code according to standard style guidelines. //! Formats Lux source code according to standard style guidelines.
use crate::ast::{ use crate::ast::{
BehavioralProperty, BinaryOp, Declaration, EffectDecl, ExternFnDecl, Expr, FunctionDecl, BehavioralProperty, BinaryOp, Declaration, EffectDecl, ExternFnDecl, ExternLetDecl, Expr, FunctionDecl,
HandlerDecl, ImplDecl, ImplMethod, LetDecl, Literal, LiteralKind, Pattern, Program, Statement, HandlerDecl, ImplDecl, ImplMethod, LetDecl, Literal, LiteralKind, Pattern, Program, Statement,
TraitDecl, TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields, Visibility, TraitDecl, TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields, Visibility,
}; };
@@ -104,6 +104,7 @@ impl Formatter {
Declaration::Trait(t) => self.format_trait(t), Declaration::Trait(t) => self.format_trait(t),
Declaration::Impl(i) => self.format_impl(i), Declaration::Impl(i) => self.format_impl(i),
Declaration::ExternFn(e) => self.format_extern_fn(e), Declaration::ExternFn(e) => self.format_extern_fn(e),
Declaration::ExternLet(e) => self.format_extern_let(e),
} }
} }
@@ -152,6 +153,27 @@ impl Formatter {
self.newline(); self.newline();
} }
fn format_extern_let(&mut self, ext: &ExternLetDecl) {
let indent = self.indent();
self.write(&indent);
if ext.visibility == Visibility::Public {
self.write("pub ");
}
self.write("extern let ");
self.write(&ext.name.name);
self.write(": ");
self.write(&self.format_type_expr(&ext.typ));
// Optional JS name
if let Some(js_name) = &ext.js_name {
self.write(&format!(" = \"{}\"", js_name));
}
self.newline();
}
fn format_function(&mut self, func: &FunctionDecl) { fn format_function(&mut self, func: &FunctionDecl) {
let indent = self.indent(); let indent = self.indent();
self.write(&indent); self.write(&indent);

View File

@@ -144,6 +144,12 @@ pub enum BuiltinFn {
MapFromList, MapFromList,
MapToList, MapToList,
MapMerge, MapMerge,
// Ref operations
RefNew,
RefGet,
RefSet,
RefUpdate,
} }
/// Runtime value /// Runtime value
@@ -181,6 +187,8 @@ pub enum Value {
name: String, name: String,
arity: usize, arity: usize,
}, },
/// Mutable reference cell
Ref(Rc<RefCell<Value>>),
} }
impl Value { impl Value {
@@ -203,6 +211,7 @@ impl Value {
Value::Versioned { .. } => "Versioned", Value::Versioned { .. } => "Versioned",
Value::Json(_) => "Json", Value::Json(_) => "Json",
Value::ExternFn { .. } => "ExternFn", Value::ExternFn { .. } => "ExternFn",
Value::Ref(_) => "Ref",
} }
} }
@@ -258,6 +267,7 @@ impl Value {
t1 == t2 && v1 == v2 && Value::values_equal(val1, val2) t1 == t2 && v1 == v2 && Value::values_equal(val1, val2)
} }
(Value::Json(j1), Value::Json(j2)) => j1 == j2, (Value::Json(j1), Value::Json(j2)) => j1 == j2,
(Value::Ref(r1), Value::Ref(r2)) => Rc::ptr_eq(r1, r2),
// Functions and handlers cannot be compared for equality // Functions and handlers cannot be compared for equality
_ => false, _ => false,
} }
@@ -414,6 +424,7 @@ impl fmt::Display for Value {
} }
Value::Json(json) => write!(f, "{}", json), Value::Json(json) => write!(f, "{}", json),
Value::ExternFn { name, .. } => write!(f, "<extern fn {}>", name), Value::ExternFn { name, .. } => write!(f, "<extern fn {}>", name),
Value::Ref(cell) => write!(f, "<ref: {}>", cell.borrow()),
} }
} }
} }
@@ -1202,6 +1213,15 @@ impl Interpreter {
("merge".to_string(), Value::Builtin(BuiltinFn::MapMerge)), ("merge".to_string(), Value::Builtin(BuiltinFn::MapMerge)),
])); ]));
env.define("Map", map_module); env.define("Map", map_module);
// Ref module
let ref_module = Value::Record(HashMap::from([
("new".to_string(), Value::Builtin(BuiltinFn::RefNew)),
("get".to_string(), Value::Builtin(BuiltinFn::RefGet)),
("set".to_string(), Value::Builtin(BuiltinFn::RefSet)),
("update".to_string(), Value::Builtin(BuiltinFn::RefUpdate)),
]));
env.define("Ref", ref_module);
} }
/// Execute a program /// Execute a program
@@ -1431,6 +1451,14 @@ impl Interpreter {
Ok(Value::Unit) Ok(Value::Unit)
} }
Declaration::ExternLet(ext) => {
// Register a placeholder that errors at runtime (extern lets only work in JS)
let name = ext.name.name.clone();
self.global_env
.define(&name, Value::ExternFn { name: name.clone(), arity: 0 });
Ok(Value::Unit)
}
Declaration::Effect(_) | Declaration::Trait(_) | Declaration::Impl(_) => { Declaration::Effect(_) | Declaration::Trait(_) | Declaration::Impl(_) => {
// These are compile-time only // These are compile-time only
Ok(Value::Unit) Ok(Value::Unit)
@@ -3432,6 +3460,56 @@ impl Interpreter {
} }
Ok(EvalResult::Value(Value::Map(map1))) Ok(EvalResult::Value(Value::Map(map1)))
} }
BuiltinFn::RefNew => {
if args.len() != 1 {
return Err(err("Ref.new requires 1 argument"));
}
Ok(EvalResult::Value(Value::Ref(Rc::new(RefCell::new(args.into_iter().next().unwrap())))))
}
BuiltinFn::RefGet => {
if args.len() != 1 {
return Err(err("Ref.get requires 1 argument"));
}
match &args[0] {
Value::Ref(cell) => Ok(EvalResult::Value(cell.borrow().clone())),
v => Err(err(&format!("Ref.get expects Ref, got {}", v.type_name()))),
}
}
BuiltinFn::RefSet => {
if args.len() != 2 {
return Err(err("Ref.set requires 2 arguments: ref, value"));
}
match &args[0] {
Value::Ref(cell) => {
*cell.borrow_mut() = args[1].clone();
Ok(EvalResult::Value(Value::Unit))
}
v => Err(err(&format!("Ref.set expects Ref as first argument, got {}", v.type_name()))),
}
}
BuiltinFn::RefUpdate => {
if args.len() != 2 {
return Err(err("Ref.update requires 2 arguments: ref, fn"));
}
match &args[0] {
Value::Ref(cell) => {
let old = cell.borrow().clone();
let result = self.eval_call(args[1].clone(), vec![old], span)?;
match result {
EvalResult::Value(new_val) => {
*cell.borrow_mut() = new_val;
}
_ => return Err(err("Ref.update callback must return a value")),
}
Ok(EvalResult::Value(Value::Unit))
}
v => Err(err(&format!("Ref.update expects Ref as first argument, got {}", v.type_name()))),
}
}
} }
} }

View File

@@ -406,6 +406,9 @@ impl Linter {
Declaration::ExternFn(e) => { Declaration::ExternFn(e) => {
self.defined_functions.insert(e.name.name.clone()); self.defined_functions.insert(e.name.name.clone());
} }
Declaration::ExternLet(e) => {
self.define_var(&e.name.name);
}
Declaration::Let(l) => { Declaration::Let(l) => {
self.define_var(&l.name.name); self.define_var(&l.name.name);
} }

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);
@@ -2311,6 +2311,25 @@ fn extract_module_doc(source: &str, path: &str) -> Result<ModuleDoc, String> {
properties: vec![], properties: vec![],
}); });
} }
ast::Declaration::ExternLet(ext) => {
let js_note = ext.js_name.as_ref()
.map(|n| format!(" = \"{}\"", n))
.unwrap_or_default();
let signature = format!(
"extern let {}: {}{}",
ext.name.name,
format_type(&ext.typ),
js_note
);
let doc = extract_doc_comment(source, ext.span.start);
functions.push(FunctionDoc {
name: ext.name.name.clone(),
signature,
description: doc,
is_public: matches!(ext.visibility, ast::Visibility::Public),
properties: vec![],
});
}
ast::Declaration::Effect(e) => { ast::Declaration::Effect(e) => {
let doc = extract_doc_comment(source, e.span.start); let doc = extract_doc_comment(source, e.span.start);
let ops: Vec<String> = e.operations.iter() let ops: Vec<String> = e.operations.iter()
@@ -4234,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);
@@ -4244,6 +4263,92 @@ c")"#;
assert!(js.contains("main_lux"), "main should be mangled: {}", js); 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] #[test]
fn test_invalid_escape_sequence() { fn test_invalid_escape_sequence() {
let result = eval(r#"let x = "\z""#); let result = eval(r#"let x = "\z""#);
@@ -5836,6 +5941,58 @@ c")"#;
assert_eq!(eval(source).unwrap(), "Some(30)"); assert_eq!(eval(source).unwrap(), "Some(30)");
} }
// Ref cell tests
#[test]
fn test_ref_new_and_get() {
let source = r#"
let r = Ref.new(42)
let result = Ref.get(r)
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_ref_set() {
let source = r#"
let r = Ref.new(0)
let _ = Ref.set(r, 10)
let result = Ref.get(r)
"#;
assert_eq!(eval(source).unwrap(), "10");
}
#[test]
fn test_ref_update() {
let source = r#"
let r = Ref.new(5)
let _ = Ref.update(r, fn(n) => n + 1)
let result = Ref.get(r)
"#;
assert_eq!(eval(source).unwrap(), "6");
}
#[test]
fn test_ref_multiple_updates() {
let source = r#"
let counter = Ref.new(0)
let _ = Ref.set(counter, 1)
let _ = Ref.update(counter, fn(n) => n * 10)
let _ = Ref.set(counter, Ref.get(counter) + 5)
let result = Ref.get(counter)
"#;
assert_eq!(eval(source).unwrap(), "15");
}
#[test]
fn test_ref_with_string() {
let source = r#"
let r = Ref.new("hello")
let _ = Ref.set(r, "world")
let result = Ref.get(r)
"#;
assert_eq!(eval(source).unwrap(), "\"world\"");
}
#[test] #[test]
fn test_file_copy() { fn test_file_copy() {
use std::io::Write; use std::io::Write;

View File

@@ -53,6 +53,7 @@ impl Module {
Declaration::Type(t) => t.visibility == Visibility::Public, Declaration::Type(t) => t.visibility == Visibility::Public,
Declaration::Trait(t) => t.visibility == Visibility::Public, Declaration::Trait(t) => t.visibility == Visibility::Public,
Declaration::ExternFn(e) => e.visibility == Visibility::Public, Declaration::ExternFn(e) => e.visibility == Visibility::Public,
Declaration::ExternLet(e) => e.visibility == Visibility::Public,
// Effects, handlers, and impls are always public for now // Effects, handlers, and impls are always public for now
Declaration::Effect(_) | Declaration::Handler(_) | Declaration::Impl(_) => true, Declaration::Effect(_) | Declaration::Handler(_) | Declaration::Impl(_) => true,
} }
@@ -298,6 +299,9 @@ impl ModuleLoader {
Declaration::ExternFn(e) if e.visibility == Visibility::Public => { Declaration::ExternFn(e) if e.visibility == Visibility::Public => {
exports.insert(e.name.name.clone()); exports.insert(e.name.name.clone());
} }
Declaration::ExternLet(e) if e.visibility == Visibility::Public => {
exports.insert(e.name.name.clone());
}
_ => {} _ => {}
} }
} }

View File

@@ -238,7 +238,7 @@ impl Parser {
match self.peek_kind() { match self.peek_kind() {
TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility, doc)?)), TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility, doc)?)),
TokenKind::Extern => Ok(Declaration::ExternFn(self.parse_extern_fn_decl(visibility, doc)?)), TokenKind::Extern => self.parse_extern_decl(visibility, doc),
TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl(doc)?)), TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl(doc)?)),
TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)), TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)),
TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility, doc)?)), TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility, doc)?)),
@@ -324,6 +324,58 @@ impl Parser {
}) })
} }
/// Parse extern declaration: dispatch to extern fn or extern let
fn parse_extern_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<Declaration, ParseError> {
// Peek past 'extern' to see if it's 'fn' or 'let'
if self.pos + 1 < self.tokens.len() {
match &self.tokens[self.pos + 1].kind {
TokenKind::Fn => Ok(Declaration::ExternFn(self.parse_extern_fn_decl(visibility, doc)?)),
TokenKind::Let => Ok(Declaration::ExternLet(self.parse_extern_let_decl(visibility, doc)?)),
_ => Err(self.error("Expected 'fn' or 'let' after 'extern'")),
}
} else {
Err(self.error("Expected 'fn' or 'let' after 'extern'"))
}
}
/// Parse extern let declaration: extern let name: Type = "jsName"
fn parse_extern_let_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<ExternLetDecl, ParseError> {
let start = self.current_span();
self.expect(TokenKind::Extern)?;
self.expect(TokenKind::Let)?;
let name = self.parse_ident()?;
// Type annotation
self.expect(TokenKind::Colon)?;
let typ = self.parse_type()?;
// Optional JS name override: = "jsName"
let js_name = if self.check(TokenKind::Eq) {
self.advance();
match self.peek_kind() {
TokenKind::String(s) => {
let name = s.clone();
self.advance();
Some(name)
}
_ => return Err(self.error("Expected string literal for JS name in extern let")),
}
} else {
None
};
let span = start.merge(self.previous_span());
Ok(ExternLetDecl {
visibility,
doc,
name,
typ,
js_name,
span,
})
}
/// Parse extern function declaration: extern fn name<T>(params): ReturnType = "jsName" /// Parse extern function declaration: extern fn name<T>(params): ReturnType = "jsName"
fn parse_extern_fn_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<ExternFnDecl, ParseError> { fn parse_extern_fn_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<ExternFnDecl, ParseError> {
let start = self.current_span(); let start = self.current_span();

View File

@@ -269,6 +269,24 @@ impl SymbolTable {
let id = self.add_symbol(scope_idx, symbol); let id = self.add_symbol(scope_idx, symbol);
self.add_reference(id, ext.name.span, true, true); self.add_reference(id, ext.name.span, true, true);
} }
Declaration::ExternLet(ext) => {
let is_public = matches!(ext.visibility, Visibility::Public);
let sig = format!(
"extern let {}: {}",
ext.name.name,
self.type_expr_to_string(&ext.typ)
);
let mut symbol = self.new_symbol(
ext.name.name.clone(),
SymbolKind::Variable,
ext.span,
Some(sig),
is_public,
);
symbol.documentation = ext.doc.clone();
let id = self.add_symbol(scope_idx, symbol);
self.add_reference(id, ext.name.span, true, true);
}
} }
} }

View File

@@ -1238,6 +1238,11 @@ impl TypeChecker {
let fn_type = Type::function(param_types, return_type); let fn_type = Type::function(param_types, return_type);
self.env.bind(&ext.name.name, TypeScheme::mono(fn_type)); self.env.bind(&ext.name.name, TypeScheme::mono(fn_type));
} }
Declaration::ExternLet(ext) => {
// Register extern let with its declared type
let typ = self.resolve_type(&ext.typ);
self.env.bind(&ext.name.name, TypeScheme::mono(typ));
}
} }
} }
@@ -3021,6 +3026,9 @@ impl TypeChecker {
"Map" if resolved_args.len() == 2 => { "Map" if resolved_args.len() == 2 => {
return Type::Map(Box::new(resolved_args[0].clone()), Box::new(resolved_args[1].clone())); return Type::Map(Box::new(resolved_args[0].clone()), Box::new(resolved_args[1].clone()));
} }
"Ref" if resolved_args.len() == 1 => {
return Type::Ref(Box::new(resolved_args[0].clone()));
}
_ => {} _ => {}
} }
} }

View File

@@ -49,6 +49,8 @@ pub enum Type {
Option(Box<Type>), Option(Box<Type>),
/// Map type (sugar for App(Map, [K, V])) /// Map type (sugar for App(Map, [K, V]))
Map(Box<Type>, Box<Type>), Map(Box<Type>, Box<Type>),
/// Ref type — mutable reference cell holding a value of type T
Ref(Box<Type>),
/// Versioned type (e.g., User @v2) /// Versioned type (e.g., User @v2)
Versioned { Versioned {
base: Box<Type>, base: Box<Type>,
@@ -120,7 +122,7 @@ impl Type {
} }
Type::Tuple(elements) => elements.iter().any(|e| e.contains_var(var)), Type::Tuple(elements) => elements.iter().any(|e| e.contains_var(var)),
Type::Record(fields) => fields.iter().any(|(_, t)| t.contains_var(var)), Type::Record(fields) => fields.iter().any(|(_, t)| t.contains_var(var)),
Type::List(inner) | Type::Option(inner) => inner.contains_var(var), Type::List(inner) | Type::Option(inner) | Type::Ref(inner) => inner.contains_var(var),
Type::Map(k, v) => k.contains_var(var) || v.contains_var(var), Type::Map(k, v) => k.contains_var(var) || v.contains_var(var),
Type::Versioned { base, .. } => base.contains_var(var), Type::Versioned { base, .. } => base.contains_var(var),
_ => false, _ => false,
@@ -161,6 +163,7 @@ impl Type {
), ),
Type::List(inner) => Type::List(Box::new(inner.apply(subst))), Type::List(inner) => Type::List(Box::new(inner.apply(subst))),
Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))), Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))),
Type::Ref(inner) => Type::Ref(Box::new(inner.apply(subst))),
Type::Map(k, v) => Type::Map(Box::new(k.apply(subst)), Box::new(v.apply(subst))), Type::Map(k, v) => Type::Map(Box::new(k.apply(subst)), Box::new(v.apply(subst))),
Type::Versioned { base, version } => Type::Versioned { Type::Versioned { base, version } => Type::Versioned {
base: Box::new(base.apply(subst)), base: Box::new(base.apply(subst)),
@@ -211,7 +214,7 @@ impl Type {
} }
vars vars
} }
Type::List(inner) | Type::Option(inner) => inner.free_vars(), Type::List(inner) | Type::Option(inner) | Type::Ref(inner) => inner.free_vars(),
Type::Map(k, v) => { Type::Map(k, v) => {
let mut vars = k.free_vars(); let mut vars = k.free_vars();
vars.extend(v.free_vars()); vars.extend(v.free_vars());
@@ -288,6 +291,7 @@ impl fmt::Display for Type {
} }
Type::List(inner) => write!(f, "List<{}>", inner), Type::List(inner) => write!(f, "List<{}>", inner),
Type::Option(inner) => write!(f, "Option<{}>", inner), Type::Option(inner) => write!(f, "Option<{}>", inner),
Type::Ref(inner) => write!(f, "Ref<{}>", inner),
Type::Map(k, v) => write!(f, "Map<{}, {}>", k, v), Type::Map(k, v) => write!(f, "Map<{}, {}>", k, v),
Type::Versioned { base, version } => { Type::Versioned { base, version } => {
write!(f, "{} {}", base, version) write!(f, "{} {}", base, version)
@@ -1946,6 +1950,32 @@ impl TypeEnv {
]); ]);
env.bind("Map", TypeScheme::mono(map_module_type)); env.bind("Map", TypeScheme::mono(map_module_type));
// Ref module
let ref_inner = || Type::var();
let ref_type = || Type::Ref(Box::new(Type::var()));
let ref_module_type = Type::Record(vec![
(
"new".to_string(),
Type::function(vec![ref_inner()], ref_type()),
),
(
"get".to_string(),
Type::function(vec![ref_type()], ref_inner()),
),
(
"set".to_string(),
Type::function(vec![ref_type(), ref_inner()], Type::Unit),
),
(
"update".to_string(),
Type::function(
vec![ref_type(), Type::function(vec![ref_inner()], ref_inner())],
Type::Unit,
),
),
]);
env.bind("Ref", TypeScheme::mono(ref_module_type));
// Result module // Result module
let result_type = Type::App { let result_type = Type::App {
constructor: Box::new(Type::Named("Result".to_string())), constructor: Box::new(Type::Named("Result".to_string())),
@@ -2185,6 +2215,9 @@ impl TypeEnv {
Type::Map(k, v) => { Type::Map(k, v) => {
Type::Map(Box::new(self.expand_type_alias(k)), Box::new(self.expand_type_alias(v))) Type::Map(Box::new(self.expand_type_alias(k)), Box::new(self.expand_type_alias(v)))
} }
Type::Ref(inner) => {
Type::Ref(Box::new(self.expand_type_alias(inner)))
}
Type::Versioned { base, version } => { Type::Versioned { base, version } => {
Type::Versioned { Type::Versioned {
base: Box::new(self.expand_type_alias(base)), base: Box::new(self.expand_type_alias(base)),
@@ -2345,6 +2378,9 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
// Option // Option
(Type::Option(a), Type::Option(b)) => unify(a, b), (Type::Option(a), Type::Option(b)) => unify(a, b),
// Ref
(Type::Ref(a), Type::Ref(b)) => unify(a, b),
// Map // Map
(Type::Map(k1, v1), Type::Map(k2, v2)) => { (Type::Map(k1, v1), Type::Map(k2, v2)) => {
let s1 = unify(k1, k2)?; let s1 = unify(k1, k2)?;