7 Commits

Author SHA1 Message Date
694e4ec999 feat: add Ref cells for mutable state (Ref.new, Ref.get, Ref.set, Ref.update)
Implements WISH-013 mutable state primitives. Ref<T> is a mutable container
using existing module call syntax. Supported across interpreter, JS, and C backends.

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:21:26 -05:00
0f8babfd8b chore: bump version to 0.1.9 2026-02-20 18:46:51 -05:00
15 changed files with 473 additions and 9 deletions

2
Cargo.lock generated
View File

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

View File

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

View File

@@ -44,7 +44,7 @@
printf "\n"
printf " \033[1;35m \033[0m\n"
printf " \033[1;35m \033[0m\n"
printf " \033[1;35m \033[0m v0.1.8\n"
printf " \033[1;35m \033[0m v0.1.11\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.11";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
@@ -79,7 +79,7 @@
};
in muslPkgs.rustPlatform.buildRustPackage {
pname = "lux";
version = "0.1.8";
version = "0.1.11";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;

View File

@@ -223,6 +223,8 @@ pub enum Declaration {
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
@@ -445,6 +447,19 @@ pub struct ExternFnDecl {
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 {

View File

@@ -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 {

View File

@@ -73,6 +73,8 @@ pub struct JsBackend {
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 {
@@ -96,6 +98,7 @@ impl JsBackend {
var_substitutions: HashMap::new(),
used_effects: HashSet::new(),
extern_fns: HashMap::new(),
extern_lets: HashMap::new(),
}
}
@@ -123,6 +126,13 @@ impl JsBackend {
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);
}
_ => {}
}
}
@@ -1097,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))
}
@@ -1229,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);
}
}
}
@@ -1375,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);
@@ -1835,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,
@@ -2412,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,

View File

@@ -3,7 +3,7 @@
//! Formats Lux source code according to standard style guidelines.
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,
TraitDecl, TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields, Visibility,
};
@@ -104,6 +104,7 @@ impl Formatter {
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),
}
}
@@ -152,6 +153,27 @@ impl Formatter {
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);

View File

@@ -144,6 +144,12 @@ pub enum BuiltinFn {
MapFromList,
MapToList,
MapMerge,
// Ref operations
RefNew,
RefGet,
RefSet,
RefUpdate,
}
/// Runtime value
@@ -181,6 +187,8 @@ pub enum Value {
name: String,
arity: usize,
},
/// Mutable reference cell
Ref(Rc<RefCell<Value>>),
}
impl Value {
@@ -203,6 +211,7 @@ impl Value {
Value::Versioned { .. } => "Versioned",
Value::Json(_) => "Json",
Value::ExternFn { .. } => "ExternFn",
Value::Ref(_) => "Ref",
}
}
@@ -258,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,
}
@@ -414,6 +424,7 @@ impl fmt::Display for Value {
}
Value::Json(json) => write!(f, "{}", json),
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)),
]));
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
@@ -1431,6 +1451,14 @@ impl Interpreter {
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)
@@ -3432,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()))),
}
}
}
}

View File

@@ -406,6 +406,9 @@ impl Linter {
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);
}

View File

@@ -2311,6 +2311,25 @@ fn extract_module_doc(source: &str, path: &str) -> Result<ModuleDoc, String> {
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()
@@ -5836,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;

View File

@@ -53,6 +53,7 @@ impl Module {
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,
}
@@ -298,6 +299,9 @@ impl ModuleLoader {
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());
}
_ => {}
}
}

View File

@@ -238,7 +238,7 @@ impl Parser {
match self.peek_kind() {
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::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)),
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"
fn parse_extern_fn_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<ExternFnDecl, ParseError> {
let start = self.current_span();

View File

@@ -269,6 +269,24 @@ impl SymbolTable {
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);
}
}
}

View File

@@ -1238,6 +1238,11 @@ impl TypeChecker {
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));
}
}
}
@@ -3021,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()));
}
_ => {}
}
}

View File

@@ -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)?;