Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 293635f415 | |||
| 694e4ec999 | |||
| 78879ca94e | |||
| 01474b401f | |||
| 169de0b3c8 | |||
| 667a94b4dc | |||
| 1b629aaae4 | |||
| 0f8babfd8b | |||
| 582d603513 | |||
| fbb7ddb6c3 | |||
| 400acc3f35 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -776,7 +776,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lux"
|
||||
version = "0.1.7"
|
||||
version = "0.1.11"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"lsp-server",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lux"
|
||||
version = "0.1.8"
|
||||
version = "0.1.12"
|
||||
edition = "2021"
|
||||
description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
|
||||
license = "MIT"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
printf "\n"
|
||||
printf " \033[1;35m╦ ╦ ╦╦ ╦\033[0m\n"
|
||||
printf " \033[1;35m║ ║ ║╔╣\033[0m\n"
|
||||
printf " \033[1;35m╩═╝╚═╝╩ ╩\033[0m v0.1.8\n"
|
||||
printf " \033[1;35m╩═╝╚═╝╩ ╩\033[0m v0.1.12\n"
|
||||
printf "\n"
|
||||
printf " Functional language with first-class effects\n"
|
||||
printf "\n"
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "lux";
|
||||
version = "0.1.8";
|
||||
version = "0.1.12";
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
};
|
||||
in muslPkgs.rustPlatform.buildRustPackage {
|
||||
pname = "lux";
|
||||
version = "0.1.8";
|
||||
version = "0.1.12";
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
|
||||
|
||||
32
src/ast.rs
32
src/ast.rs
@@ -221,6 +221,10 @@ pub enum Declaration {
|
||||
Trait(TraitDecl),
|
||||
/// Trait implementation: impl Trait for Type { ... }
|
||||
Impl(ImplDecl),
|
||||
/// Extern function declaration (FFI): extern fn name(params): ReturnType
|
||||
ExternFn(ExternFnDecl),
|
||||
/// Extern let declaration (FFI): extern let name: Type
|
||||
ExternLet(ExternLetDecl),
|
||||
}
|
||||
|
||||
/// Function declaration
|
||||
@@ -428,6 +432,34 @@ pub struct ImplMethod {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
/// Extern function declaration (FFI)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExternFnDecl {
|
||||
pub visibility: Visibility,
|
||||
/// Documentation comment
|
||||
pub doc: Option<String>,
|
||||
pub name: Ident,
|
||||
pub type_params: Vec<Ident>,
|
||||
pub params: Vec<Parameter>,
|
||||
pub return_type: TypeExpr,
|
||||
/// Optional JS name override: extern fn foo(...): T = "jsFoo"
|
||||
pub js_name: Option<String>,
|
||||
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
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TypeExpr {
|
||||
|
||||
@@ -3285,6 +3285,9 @@ impl CBackend {
|
||||
if module_name.name == "Map" {
|
||||
return self.emit_map_operation(&field.name, args);
|
||||
}
|
||||
if module_name.name == "Ref" {
|
||||
return self.emit_ref_operation(&field.name, args);
|
||||
}
|
||||
// Int module
|
||||
if module_name.name == "Int" && field.name == "toString" {
|
||||
let arg = self.emit_expr(&args[0])?;
|
||||
@@ -3701,6 +3704,11 @@ impl CBackend {
|
||||
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
|
||||
if effect.name == "Console" {
|
||||
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> {
|
||||
// Simple substitution - in a real implementation, this would be more sophisticated
|
||||
match expr {
|
||||
|
||||
@@ -71,6 +71,10 @@ pub struct JsBackend {
|
||||
var_substitutions: HashMap<String, String>,
|
||||
/// Effects actually used in the program (for tree-shaking runtime)
|
||||
used_effects: HashSet<String>,
|
||||
/// Extern function names mapped to their JS names
|
||||
extern_fns: HashMap<String, String>,
|
||||
/// Extern let names mapped to their JS names
|
||||
extern_lets: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl JsBackend {
|
||||
@@ -93,6 +97,8 @@ impl JsBackend {
|
||||
has_handlers: false,
|
||||
var_substitutions: HashMap::new(),
|
||||
used_effects: HashSet::new(),
|
||||
extern_fns: HashMap::new(),
|
||||
extern_lets: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +118,21 @@ impl JsBackend {
|
||||
Declaration::Type(t) => {
|
||||
self.collect_type(t)?;
|
||||
}
|
||||
Declaration::ExternFn(ext) => {
|
||||
let js_name = ext
|
||||
.js_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| ext.name.name.clone());
|
||||
self.extern_fns.insert(ext.name.name.clone(), js_name);
|
||||
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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1086,6 +1107,9 @@ impl JsBackend {
|
||||
} else if self.functions.contains(&ident.name) {
|
||||
// Function reference (used as value)
|
||||
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 {
|
||||
Ok(self.escape_js_keyword(&ident.name))
|
||||
}
|
||||
@@ -1218,6 +1242,9 @@ impl JsBackend {
|
||||
if module_name.name == "Map" {
|
||||
return self.emit_map_operation(&field.name, args);
|
||||
}
|
||||
if module_name.name == "Ref" {
|
||||
return self.emit_ref_operation(&field.name, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,6 +1391,11 @@ impl JsBackend {
|
||||
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)
|
||||
if effect.name == "Html" {
|
||||
return self.emit_html_operation(&operation.name, args);
|
||||
@@ -1824,6 +1856,67 @@ impl JsBackend {
|
||||
let func = self.emit_expr(&args[1])?;
|
||||
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))
|
||||
}
|
||||
_ => Err(JsGenError {
|
||||
message: format!("Unknown List operation: {}", operation),
|
||||
span: None,
|
||||
@@ -2401,6 +2494,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
|
||||
fn emit_html_operation(
|
||||
&mut self,
|
||||
@@ -2723,6 +2847,10 @@ impl JsBackend {
|
||||
|
||||
/// Mangle a Lux name to a valid JavaScript name
|
||||
fn mangle_name(&self, name: &str) -> String {
|
||||
// Extern functions use their JS name directly (no mangling)
|
||||
if let Some(js_name) = self.extern_fns.get(name) {
|
||||
return js_name.clone();
|
||||
}
|
||||
format!("{}_lux", name)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
//! Formats Lux source code according to standard style guidelines.
|
||||
|
||||
use crate::ast::{
|
||||
BehavioralProperty, BinaryOp, Declaration, EffectDecl, Expr, FunctionDecl, HandlerDecl,
|
||||
ImplDecl, ImplMethod, LetDecl, Literal, LiteralKind, Pattern, Program, Statement, TraitDecl,
|
||||
TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields,
|
||||
BehavioralProperty, BinaryOp, Declaration, EffectDecl, ExternFnDecl, ExternLetDecl, Expr, FunctionDecl,
|
||||
HandlerDecl, ImplDecl, ImplMethod, LetDecl, Literal, LiteralKind, Pattern, Program, Statement,
|
||||
TraitDecl, TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields, Visibility,
|
||||
};
|
||||
use crate::lexer::Lexer;
|
||||
use crate::parser::Parser;
|
||||
@@ -103,9 +103,77 @@ impl Formatter {
|
||||
Declaration::Handler(h) => self.format_handler(h),
|
||||
Declaration::Trait(t) => self.format_trait(t),
|
||||
Declaration::Impl(i) => self.format_impl(i),
|
||||
Declaration::ExternFn(e) => self.format_extern_fn(e),
|
||||
Declaration::ExternLet(e) => self.format_extern_let(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_extern_fn(&mut self, ext: &ExternFnDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
|
||||
if ext.visibility == Visibility::Public {
|
||||
self.write("pub ");
|
||||
}
|
||||
|
||||
self.write("extern fn ");
|
||||
self.write(&ext.name.name);
|
||||
|
||||
// Type parameters
|
||||
if !ext.type_params.is_empty() {
|
||||
self.write("<");
|
||||
self.write(
|
||||
&ext.type_params
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write(">");
|
||||
}
|
||||
|
||||
// Parameters
|
||||
self.write("(");
|
||||
let params: Vec<String> = ext
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, self.format_type_expr(&p.typ)))
|
||||
.collect();
|
||||
self.write(¶ms.join(", "));
|
||||
self.write("): ");
|
||||
|
||||
// Return type
|
||||
self.write(&self.format_type_expr(&ext.return_type));
|
||||
|
||||
// Optional JS name
|
||||
if let Some(js_name) = &ext.js_name {
|
||||
self.write(&format!(" = \"{}\"", js_name));
|
||||
}
|
||||
|
||||
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) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
|
||||
@@ -144,6 +144,12 @@ pub enum BuiltinFn {
|
||||
MapFromList,
|
||||
MapToList,
|
||||
MapMerge,
|
||||
|
||||
// Ref operations
|
||||
RefNew,
|
||||
RefGet,
|
||||
RefSet,
|
||||
RefUpdate,
|
||||
}
|
||||
|
||||
/// Runtime value
|
||||
@@ -176,6 +182,13 @@ pub enum Value {
|
||||
},
|
||||
/// JSON value (for JSON parsing/manipulation)
|
||||
Json(serde_json::Value),
|
||||
/// Extern function (FFI — only callable from JS backend)
|
||||
ExternFn {
|
||||
name: String,
|
||||
arity: usize,
|
||||
},
|
||||
/// Mutable reference cell
|
||||
Ref(Rc<RefCell<Value>>),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
@@ -197,6 +210,8 @@ impl Value {
|
||||
Value::Constructor { .. } => "Constructor",
|
||||
Value::Versioned { .. } => "Versioned",
|
||||
Value::Json(_) => "Json",
|
||||
Value::ExternFn { .. } => "ExternFn",
|
||||
Value::Ref(_) => "Ref",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +267,7 @@ impl Value {
|
||||
t1 == t2 && v1 == v2 && Value::values_equal(val1, val2)
|
||||
}
|
||||
(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
|
||||
_ => false,
|
||||
}
|
||||
@@ -407,6 +423,8 @@ impl fmt::Display for Value {
|
||||
write!(f, "{} @v{}", value, version)
|
||||
}
|
||||
Value::Json(json) => write!(f, "{}", json),
|
||||
Value::ExternFn { name, .. } => write!(f, "<extern fn {}>", name),
|
||||
Value::Ref(cell) => write!(f, "<ref: {}>", cell.borrow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1195,6 +1213,15 @@ impl Interpreter {
|
||||
("merge".to_string(), Value::Builtin(BuiltinFn::MapMerge)),
|
||||
]));
|
||||
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
|
||||
@@ -1405,6 +1432,33 @@ impl Interpreter {
|
||||
Ok(Value::Unit)
|
||||
}
|
||||
|
||||
Declaration::ExternFn(ext) => {
|
||||
// Register a placeholder that errors at runtime
|
||||
let name = ext.name.name.clone();
|
||||
let arity = ext.params.len();
|
||||
// Create a closure that produces a clear error
|
||||
let closure = Closure {
|
||||
params: ext.params.iter().map(|p| p.name.name.clone()).collect(),
|
||||
body: Expr::Literal(crate::ast::Literal {
|
||||
kind: crate::ast::LiteralKind::Unit,
|
||||
span: ext.span,
|
||||
}),
|
||||
env: self.global_env.clone(),
|
||||
};
|
||||
// We store an ExternFn marker value
|
||||
self.global_env
|
||||
.define(&name, Value::ExternFn { name: name.clone(), arity });
|
||||
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(_) => {
|
||||
// These are compile-time only
|
||||
Ok(Value::Unit)
|
||||
@@ -1924,6 +1978,13 @@ impl Interpreter {
|
||||
}))
|
||||
}
|
||||
Value::Builtin(builtin) => self.eval_builtin(builtin, args, span),
|
||||
Value::ExternFn { name, .. } => Err(RuntimeError {
|
||||
message: format!(
|
||||
"Extern function '{}' can only be called when compiled to JavaScript (use `lux build --target js`)",
|
||||
name
|
||||
),
|
||||
span: Some(span),
|
||||
}),
|
||||
v => Err(RuntimeError {
|
||||
message: format!("Cannot call {}", v.type_name()),
|
||||
span: Some(span),
|
||||
@@ -3399,6 +3460,56 @@ impl Interpreter {
|
||||
}
|
||||
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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ pub enum TokenKind {
|
||||
Trait, // trait (for type classes)
|
||||
Impl, // impl (for trait implementations)
|
||||
For, // for (in impl Trait for Type)
|
||||
Extern, // extern (for FFI declarations)
|
||||
|
||||
// Documentation
|
||||
DocComment(String), // /// doc comment
|
||||
@@ -152,6 +153,7 @@ impl fmt::Display for TokenKind {
|
||||
TokenKind::Trait => write!(f, "trait"),
|
||||
TokenKind::Impl => write!(f, "impl"),
|
||||
TokenKind::For => write!(f, "for"),
|
||||
TokenKind::Extern => write!(f, "extern"),
|
||||
TokenKind::DocComment(s) => write!(f, "/// {}", s),
|
||||
TokenKind::Is => write!(f, "is"),
|
||||
TokenKind::Pure => write!(f, "pure"),
|
||||
@@ -1008,6 +1010,7 @@ impl<'a> Lexer<'a> {
|
||||
"trait" => TokenKind::Trait,
|
||||
"impl" => TokenKind::Impl,
|
||||
"for" => TokenKind::For,
|
||||
"extern" => TokenKind::Extern,
|
||||
"is" => TokenKind::Is,
|
||||
"pure" => TokenKind::Pure,
|
||||
"total" => TokenKind::Total,
|
||||
|
||||
@@ -403,6 +403,12 @@ impl Linter {
|
||||
Declaration::Function(f) => {
|
||||
self.defined_functions.insert(f.name.name.clone());
|
||||
}
|
||||
Declaration::ExternFn(e) => {
|
||||
self.defined_functions.insert(e.name.name.clone());
|
||||
}
|
||||
Declaration::ExternLet(e) => {
|
||||
self.define_var(&e.name.name);
|
||||
}
|
||||
Declaration::Let(l) => {
|
||||
self.define_var(&l.name.name);
|
||||
}
|
||||
|
||||
234
src/main.rs
234
src/main.rs
@@ -2288,6 +2288,48 @@ fn extract_module_doc(source: &str, path: &str) -> Result<ModuleDoc, String> {
|
||||
is_public: matches!(t.visibility, ast::Visibility::Public),
|
||||
});
|
||||
}
|
||||
ast::Declaration::ExternFn(ext) => {
|
||||
let params: Vec<String> = ext.params.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, format_type(&p.typ)))
|
||||
.collect();
|
||||
let js_note = ext.js_name.as_ref()
|
||||
.map(|n| format!(" = \"{}\"", n))
|
||||
.unwrap_or_default();
|
||||
let signature = format!(
|
||||
"extern fn {}({}): {}{}",
|
||||
ext.name.name,
|
||||
params.join(", "),
|
||||
format_type(&ext.return_type),
|
||||
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::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) => {
|
||||
let doc = extract_doc_comment(source, e.span.start);
|
||||
let ops: Vec<String> = e.operations.iter()
|
||||
@@ -4081,6 +4123,146 @@ c")"#;
|
||||
assert_eq!(eval("let x = { a: 1, b: 2 } == { a: 1, b: 3 }").unwrap(), "false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_spread() {
|
||||
let source = r#"
|
||||
let base = { x: 1, y: 2, z: 3 }
|
||||
let updated = { ...base, y: 20 }
|
||||
let result = updated.y
|
||||
"#;
|
||||
assert_eq!(eval(source).unwrap(), "20");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deep_path_record_update() {
|
||||
// Basic deep path: { ...base, pos.x: val } desugars to { ...base, pos: { ...base.pos, x: val } }
|
||||
let source = r#"
|
||||
let npc = { name: "Goblin", pos: { x: 10, y: 20 } }
|
||||
let moved = { ...npc, pos.x: 50, pos.y: 60 }
|
||||
let result = moved.pos.x
|
||||
"#;
|
||||
assert_eq!(eval(source).unwrap(), "50");
|
||||
|
||||
// Verify other fields are preserved through spread
|
||||
let source2 = r#"
|
||||
let npc = { name: "Goblin", pos: { x: 10, y: 20 } }
|
||||
let moved = { ...npc, pos.x: 50 }
|
||||
let result = moved.pos.y
|
||||
"#;
|
||||
assert_eq!(eval(source2).unwrap(), "20");
|
||||
|
||||
// Verify top-level spread fields preserved
|
||||
let source3 = r#"
|
||||
let npc = { name: "Goblin", pos: { x: 10, y: 20 } }
|
||||
let moved = { ...npc, pos.x: 50 }
|
||||
let result = moved.name
|
||||
"#;
|
||||
assert_eq!(eval(source3).unwrap(), "\"Goblin\"");
|
||||
|
||||
// Mix of flat and deep path fields
|
||||
let source4 = r#"
|
||||
let npc = { name: "Goblin", pos: { x: 10, y: 20 }, hp: 100 }
|
||||
let updated = { ...npc, pos.x: 50, hp: 80 }
|
||||
let result = (updated.pos.x, updated.hp, updated.name)
|
||||
"#;
|
||||
assert_eq!(eval(source4).unwrap(), "(50, 80, \"Goblin\")");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deep_path_record_multilevel() {
|
||||
// Multi-level deep path: world.physics.gravity
|
||||
let source = r#"
|
||||
let world = { name: "Earth", physics: { gravity: { x: 0, y: -10 }, drag: 1 } }
|
||||
let updated = { ...world, physics.gravity.y: -20 }
|
||||
let result = (updated.physics.gravity.y, updated.physics.drag, updated.name)
|
||||
"#;
|
||||
assert_eq!(eval(source).unwrap(), "(-20, 1, \"Earth\")");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deep_path_conflict_error() {
|
||||
// Field appears as both flat and deep path — should error
|
||||
let result = eval(r#"
|
||||
let base = { pos: { x: 1, y: 2 } }
|
||||
let bad = { ...base, pos: { x: 10, y: 20 }, pos.x: 30 }
|
||||
"#);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extern_fn_parse() {
|
||||
// Extern fn should parse successfully
|
||||
let source = r#"
|
||||
extern fn getElementById(id: String): String
|
||||
let x = 42
|
||||
"#;
|
||||
assert_eq!(eval(source).unwrap(), "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extern_fn_with_js_name() {
|
||||
// Extern fn with JS name override
|
||||
let source = r#"
|
||||
extern fn getCtx(el: String, kind: String): String = "getContext"
|
||||
let x = 42
|
||||
"#;
|
||||
assert_eq!(eval(source).unwrap(), "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extern_fn_call_errors_in_interpreter() {
|
||||
// Calling an extern fn in the interpreter should produce a clear error
|
||||
let source = r#"
|
||||
extern fn alert(msg: String): Unit
|
||||
let x = alert("hello")
|
||||
"#;
|
||||
let result = eval(source);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.contains("extern") || err.contains("Extern") || err.contains("JavaScript"),
|
||||
"Error should mention extern/JavaScript: {}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pub_extern_fn() {
|
||||
// pub extern fn should parse
|
||||
let source = r#"
|
||||
pub extern fn requestAnimationFrame(callback: fn(): Unit): Int
|
||||
let x = 42
|
||||
"#;
|
||||
assert_eq!(eval(source).unwrap(), "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extern_fn_js_codegen() {
|
||||
// Verify JS backend emits extern fn calls without _lux suffix
|
||||
use crate::codegen::js_backend::JsBackend;
|
||||
use crate::parser::Parser;
|
||||
use crate::lexer::Lexer;
|
||||
|
||||
let source = r#"
|
||||
extern fn getElementById(id: String): String
|
||||
extern fn getContext(el: String, kind: String): String = "getContext"
|
||||
fn main(): Unit = {
|
||||
let el = getElementById("canvas")
|
||||
let ctx = getContext(el, "2d")
|
||||
()
|
||||
}
|
||||
"#;
|
||||
|
||||
let tokens = Lexer::new(source).tokenize().unwrap();
|
||||
let program = Parser::new(tokens).parse_program().unwrap();
|
||||
let mut backend = JsBackend::new();
|
||||
let js = backend.generate(&program).unwrap();
|
||||
|
||||
// getElementById should appear as-is (no _lux suffix)
|
||||
assert!(js.contains("getElementById("), "JS should call getElementById directly: {}", js);
|
||||
// getContext should use the JS name override
|
||||
assert!(js.contains("getContext("), "JS should call getContext directly: {}", js);
|
||||
// main should still be mangled
|
||||
assert!(js.contains("main_lux"), "main should be mangled: {}", js);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_escape_sequence() {
|
||||
let result = eval(r#"let x = "\z""#);
|
||||
@@ -5673,6 +5855,58 @@ c")"#;
|
||||
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]
|
||||
fn test_file_copy() {
|
||||
use std::io::Write;
|
||||
|
||||
@@ -52,6 +52,8 @@ impl Module {
|
||||
Declaration::Let(l) => l.visibility == Visibility::Public,
|
||||
Declaration::Type(t) => t.visibility == Visibility::Public,
|
||||
Declaration::Trait(t) => t.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
|
||||
Declaration::Effect(_) | Declaration::Handler(_) | Declaration::Impl(_) => true,
|
||||
}
|
||||
@@ -294,6 +296,12 @@ impl ModuleLoader {
|
||||
// Handlers are always exported
|
||||
exports.insert(h.name.name.clone());
|
||||
}
|
||||
Declaration::ExternFn(e) if e.visibility == Visibility::Public => {
|
||||
exports.insert(e.name.name.clone());
|
||||
}
|
||||
Declaration::ExternLet(e) if e.visibility == Visibility::Public => {
|
||||
exports.insert(e.name.name.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
258
src/parser.rs
258
src/parser.rs
@@ -238,6 +238,7 @@ impl Parser {
|
||||
|
||||
match self.peek_kind() {
|
||||
TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility, doc)?)),
|
||||
TokenKind::Extern => self.parse_extern_decl(visibility, doc),
|
||||
TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl(doc)?)),
|
||||
TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)),
|
||||
TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility, doc)?)),
|
||||
@@ -246,7 +247,7 @@ impl Parser {
|
||||
TokenKind::Impl => Ok(Declaration::Impl(self.parse_impl_decl()?)),
|
||||
TokenKind::Run => Err(self.error("Bare 'run' expressions are not allowed at top level. Use 'let _ = run ...' or 'let result = run ...'")),
|
||||
TokenKind::Handle => Err(self.error("Bare 'handle' expressions are not allowed at top level. Use 'let _ = handle ...' or 'let result = handle ...'")),
|
||||
_ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")),
|
||||
_ => Err(self.error("Expected declaration (fn, extern, effect, handler, type, trait, impl, or let)")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +324,109 @@ 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"
|
||||
fn parse_extern_fn_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<ExternFnDecl, ParseError> {
|
||||
let start = self.current_span();
|
||||
self.expect(TokenKind::Extern)?;
|
||||
self.expect(TokenKind::Fn)?;
|
||||
|
||||
let name = self.parse_ident()?;
|
||||
|
||||
// Optional type parameters
|
||||
let type_params = if self.check(TokenKind::Lt) {
|
||||
self.parse_type_params()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
self.expect(TokenKind::LParen)?;
|
||||
let params = self.parse_params()?;
|
||||
self.expect(TokenKind::RParen)?;
|
||||
|
||||
// Return type
|
||||
self.expect(TokenKind::Colon)?;
|
||||
let return_type = 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 fn")),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let span = start.merge(self.previous_span());
|
||||
Ok(ExternFnDecl {
|
||||
visibility,
|
||||
doc,
|
||||
name,
|
||||
type_params,
|
||||
params,
|
||||
return_type,
|
||||
js_name,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse effect declaration
|
||||
fn parse_effect_decl(&mut self, doc: Option<String>) -> Result<EffectDecl, ParseError> {
|
||||
let start = self.current_span();
|
||||
@@ -2296,12 +2400,34 @@ impl Parser {
|
||||
return self.parse_record_expr_rest(start);
|
||||
}
|
||||
|
||||
// Check if it's a record (ident: expr) or block
|
||||
// Check if it's a record (ident: expr or ident.path: expr) or block
|
||||
if matches!(self.peek_kind(), TokenKind::Ident(_)) {
|
||||
let lookahead = self.tokens.get(self.pos + 1).map(|t| &t.kind);
|
||||
if matches!(lookahead, Some(TokenKind::Colon)) {
|
||||
return self.parse_record_expr_rest(start);
|
||||
}
|
||||
// Check for deep path record: { ident.ident...: expr }
|
||||
if matches!(lookahead, Some(TokenKind::Dot)) {
|
||||
let mut look = self.pos + 2;
|
||||
loop {
|
||||
match self.tokens.get(look).map(|t| &t.kind) {
|
||||
Some(TokenKind::Ident(_)) => {
|
||||
look += 1;
|
||||
match self.tokens.get(look).map(|t| &t.kind) {
|
||||
Some(TokenKind::Colon) => {
|
||||
return self.parse_record_expr_rest(start);
|
||||
}
|
||||
Some(TokenKind::Dot) => {
|
||||
look += 1;
|
||||
continue;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's a block
|
||||
@@ -2309,8 +2435,9 @@ impl Parser {
|
||||
}
|
||||
|
||||
fn parse_record_expr_rest(&mut self, start: Span) -> Result<Expr, ParseError> {
|
||||
let mut fields = Vec::new();
|
||||
let mut raw_fields: Vec<(Vec<Ident>, Expr)> = Vec::new();
|
||||
let mut spread = None;
|
||||
let mut has_deep_paths = false;
|
||||
|
||||
// Check for spread: { ...expr, ... }
|
||||
if self.check(TokenKind::DotDotDot) {
|
||||
@@ -2327,9 +2454,21 @@ impl Parser {
|
||||
|
||||
while !self.check(TokenKind::RBrace) {
|
||||
let name = self.parse_ident()?;
|
||||
|
||||
// Check for dotted path: pos.x, pos.x.y, etc.
|
||||
let mut path = vec![name];
|
||||
while self.check(TokenKind::Dot) {
|
||||
self.advance(); // consume .
|
||||
let segment = self.parse_ident()?;
|
||||
path.push(segment);
|
||||
}
|
||||
if path.len() > 1 {
|
||||
has_deep_paths = true;
|
||||
}
|
||||
|
||||
self.expect(TokenKind::Colon)?;
|
||||
let value = self.parse_expr()?;
|
||||
fields.push((name, value));
|
||||
raw_fields.push((path, value));
|
||||
|
||||
self.skip_newlines();
|
||||
if self.check(TokenKind::Comma) {
|
||||
@@ -2340,10 +2479,119 @@ impl Parser {
|
||||
|
||||
self.expect(TokenKind::RBrace)?;
|
||||
let span = start.merge(self.previous_span());
|
||||
|
||||
if has_deep_paths {
|
||||
Self::desugar_deep_fields(spread, raw_fields, span)
|
||||
} else {
|
||||
// No deep paths — use flat fields directly (common case, no allocation overhead)
|
||||
let fields = raw_fields
|
||||
.into_iter()
|
||||
.map(|(mut path, value)| (path.remove(0), value))
|
||||
.collect();
|
||||
Ok(Expr::Record {
|
||||
spread,
|
||||
fields,
|
||||
span,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Desugar deep path record fields into nested record spread expressions.
|
||||
/// `{ ...base, pos.x: vx, pos.y: vy }` becomes `{ ...base, pos: { ...base.pos, x: vx, y: vy } }`
|
||||
fn desugar_deep_fields(
|
||||
spread: Option<Box<Expr>>,
|
||||
raw_fields: Vec<(Vec<Ident>, Expr)>,
|
||||
outer_span: Span,
|
||||
) -> Result<Expr, ParseError> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Group fields by first path segment, preserving order
|
||||
let mut groups: Vec<(String, Vec<(Vec<Ident>, Expr)>)> = Vec::new();
|
||||
let mut group_map: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
for (path, value) in raw_fields {
|
||||
let key = path[0].name.clone();
|
||||
if let Some(&idx) = group_map.get(&key) {
|
||||
groups[idx].1.push((path, value));
|
||||
} else {
|
||||
group_map.insert(key.clone(), groups.len());
|
||||
groups.push((key, vec![(path, value)]));
|
||||
}
|
||||
}
|
||||
|
||||
let mut fields = Vec::new();
|
||||
for (_, group) in groups {
|
||||
let first_ident = group[0].0[0].clone();
|
||||
|
||||
let has_flat = group.iter().any(|(p, _)| p.len() == 1);
|
||||
let has_deep = group.iter().any(|(p, _)| p.len() > 1);
|
||||
|
||||
if has_flat && has_deep {
|
||||
return Err(ParseError {
|
||||
message: format!(
|
||||
"Field '{}' appears as both a direct field and a deep path prefix",
|
||||
first_ident.name
|
||||
),
|
||||
span: first_ident.span,
|
||||
});
|
||||
}
|
||||
|
||||
if has_flat {
|
||||
if group.len() > 1 {
|
||||
return Err(ParseError {
|
||||
message: format!("Duplicate field '{}'", first_ident.name),
|
||||
span: group[1].0[0].span,
|
||||
});
|
||||
}
|
||||
let (_, value) = group.into_iter().next().unwrap();
|
||||
fields.push((first_ident, value));
|
||||
} else {
|
||||
// Deep paths — create nested record with spread from parent
|
||||
let sub_spread = spread.as_ref().map(|s| {
|
||||
Box::new(Expr::Field {
|
||||
object: s.clone(),
|
||||
field: first_ident.clone(),
|
||||
span: first_ident.span,
|
||||
})
|
||||
});
|
||||
|
||||
// Strip first segment from all paths
|
||||
let sub_fields: Vec<(Vec<Ident>, Expr)> = group
|
||||
.into_iter()
|
||||
.map(|(mut path, value)| {
|
||||
path.remove(0);
|
||||
(path, value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let has_nested_deep = sub_fields.iter().any(|(p, _)| p.len() > 1);
|
||||
if has_nested_deep {
|
||||
// Recursively desugar deeper paths
|
||||
let nested =
|
||||
Self::desugar_deep_fields(sub_spread, sub_fields, first_ident.span)?;
|
||||
fields.push((first_ident, nested));
|
||||
} else {
|
||||
// All sub-paths are single-segment — build Record directly
|
||||
let flat_fields: Vec<(Ident, Expr)> = sub_fields
|
||||
.into_iter()
|
||||
.map(|(mut path, value)| (path.remove(0), value))
|
||||
.collect();
|
||||
fields.push((
|
||||
first_ident.clone(),
|
||||
Expr::Record {
|
||||
spread: sub_spread,
|
||||
fields: flat_fields,
|
||||
span: first_ident.span,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Expr::Record {
|
||||
spread,
|
||||
fields,
|
||||
span,
|
||||
span: outer_span,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,48 @@ impl SymbolTable {
|
||||
Declaration::Handler(h) => self.visit_handler(h, scope_idx),
|
||||
Declaration::Trait(t) => self.visit_trait(t, scope_idx),
|
||||
Declaration::Impl(i) => self.visit_impl(i, scope_idx),
|
||||
Declaration::ExternFn(ext) => {
|
||||
let is_public = matches!(ext.visibility, Visibility::Public);
|
||||
let params: Vec<String> = ext
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, self.type_expr_to_string(&p.typ)))
|
||||
.collect();
|
||||
let sig = format!(
|
||||
"extern fn {}({}): {}",
|
||||
ext.name.name,
|
||||
params.join(", "),
|
||||
self.type_expr_to_string(&ext.return_type)
|
||||
);
|
||||
let mut symbol = self.new_symbol(
|
||||
ext.name.name.clone(),
|
||||
SymbolKind::Function,
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::ast::{
|
||||
self, BinaryOp, Declaration, EffectDecl, Expr, FunctionDecl, HandlerDecl, Ident, ImplDecl,
|
||||
ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span,
|
||||
Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
|
||||
self, BinaryOp, Declaration, EffectDecl, ExternFnDecl, Expr, FunctionDecl, HandlerDecl, Ident,
|
||||
ImplDecl, ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program,
|
||||
Span, Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
|
||||
};
|
||||
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, ErrorCode, Severity};
|
||||
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
|
||||
@@ -1227,6 +1227,22 @@ impl TypeChecker {
|
||||
let trait_impl = self.collect_impl(impl_decl);
|
||||
self.env.trait_impls.push(trait_impl);
|
||||
}
|
||||
Declaration::ExternFn(ext) => {
|
||||
// Register extern fn type signature (like a regular function but no body)
|
||||
let param_types: Vec<Type> = ext
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| self.resolve_type(&p.typ))
|
||||
.collect();
|
||||
let return_type = self.resolve_type(&ext.return_type);
|
||||
let fn_type = Type::function(param_types, return_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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3010,6 +3026,9 @@ impl TypeChecker {
|
||||
"Map" if resolved_args.len() == 2 => {
|
||||
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>),
|
||||
/// Map type (sugar for App(Map, [K, V]))
|
||||
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 {
|
||||
base: Box<Type>,
|
||||
@@ -120,7 +122,7 @@ impl Type {
|
||||
}
|
||||
Type::Tuple(elements) => elements.iter().any(|e| e.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::Versioned { base, .. } => base.contains_var(var),
|
||||
_ => false,
|
||||
@@ -161,6 +163,7 @@ impl Type {
|
||||
),
|
||||
Type::List(inner) => Type::List(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::Versioned { base, version } => Type::Versioned {
|
||||
base: Box::new(base.apply(subst)),
|
||||
@@ -211,7 +214,7 @@ impl Type {
|
||||
}
|
||||
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) => {
|
||||
let mut vars = k.free_vars();
|
||||
vars.extend(v.free_vars());
|
||||
@@ -288,6 +291,7 @@ impl fmt::Display for Type {
|
||||
}
|
||||
Type::List(inner) => write!(f, "List<{}>", 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::Versioned { base, version } => {
|
||||
write!(f, "{} {}", base, version)
|
||||
@@ -1946,6 +1950,32 @@ impl TypeEnv {
|
||||
]);
|
||||
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
|
||||
let result_type = Type::App {
|
||||
constructor: Box::new(Type::Named("Result".to_string())),
|
||||
@@ -2185,6 +2215,9 @@ impl TypeEnv {
|
||||
Type::Map(k, 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: Box::new(self.expand_type_alias(base)),
|
||||
@@ -2345,6 +2378,9 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
|
||||
// Option
|
||||
(Type::Option(a), Type::Option(b)) => unify(a, b),
|
||||
|
||||
// Ref
|
||||
(Type::Ref(a), Type::Ref(b)) => unify(a, b),
|
||||
|
||||
// Map
|
||||
(Type::Map(k1, v1), Type::Map(k2, v2)) => {
|
||||
let s1 = unify(k1, k2)?;
|
||||
|
||||
Reference in New Issue
Block a user