Compare commits
14 Commits
582d603513
...
v0.1.13
| Author | SHA1 | Date | |
|---|---|---|---|
| 92d443e475 | |||
| fe30206cd0 | |||
| 563d62f526 | |||
| e9ec1bb84d | |||
| e46afd98eb | |||
| 64f33e4e4b | |||
| 293635f415 | |||
| 694e4ec999 | |||
| 78879ca94e | |||
| 01474b401f | |||
| 169de0b3c8 | |||
| 667a94b4dc | |||
| 1b629aaae4 | |||
| 0f8babfd8b |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lux"
|
name = "lux"
|
||||||
version = "0.1.8"
|
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"
|
||||||
|
|||||||
@@ -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.8\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.8";
|
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.8";
|
version = "0.1.13";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|
||||||
|
|||||||
15
src/ast.rs
15
src/ast.rs
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
161
src/main.rs
161
src/main.rs
@@ -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;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/types.rs
40
src/types.rs
@@ -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)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user