From 667a94b4dc4e2d23081e23a1df1df178aa2725e0 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 20 Feb 2026 19:29:44 -0500 Subject: [PATCH] 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 --- src/ast.rs | 15 +++++++++++ src/codegen/js_backend.rs | 13 ++++++++++ src/formatter.rs | 24 ++++++++++++++++- src/interpreter.rs | 8 ++++++ src/linter.rs | 3 +++ src/main.rs | 19 ++++++++++++++ src/modules.rs | 4 +++ src/parser.rs | 54 ++++++++++++++++++++++++++++++++++++++- src/symbol_table.rs | 18 +++++++++++++ src/typechecker.rs | 5 ++++ 10 files changed, 161 insertions(+), 2 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 110603c..c4c1dd4 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -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, + pub name: Ident, + pub typ: TypeExpr, + /// Optional JS name override: extern let foo: T = "window.foo" + pub js_name: Option, + pub span: Span, +} + /// Type expressions #[derive(Debug, Clone, PartialEq, Eq)] pub enum TypeExpr { diff --git a/src/codegen/js_backend.rs b/src/codegen/js_backend.rs index cbd119d..3762e16 100644 --- a/src/codegen/js_backend.rs +++ b/src/codegen/js_backend.rs @@ -73,6 +73,8 @@ pub struct JsBackend { used_effects: HashSet, /// Extern function names mapped to their JS names extern_fns: HashMap, + /// Extern let names mapped to their JS names + extern_lets: HashMap, } 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)) } diff --git a/src/formatter.rs b/src/formatter.rs index 01696ba..a46ca8c 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -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); diff --git a/src/interpreter.rs b/src/interpreter.rs index 7681544..eca1d94 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -1431,6 +1431,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) diff --git a/src/linter.rs b/src/linter.rs index a90b170..7364232 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -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); } diff --git a/src/main.rs b/src/main.rs index 7954841..aff62fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2311,6 +2311,25 @@ fn extract_module_doc(source: &str, path: &str) -> Result { 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 = e.operations.iter() diff --git a/src/modules.rs b/src/modules.rs index 68bef98..4970fda 100644 --- a/src/modules.rs +++ b/src/modules.rs @@ -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()); + } _ => {} } } diff --git a/src/parser.rs b/src/parser.rs index bc40179..4c21d40 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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) -> Result { + // 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) -> Result { + 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(params): ReturnType = "jsName" fn parse_extern_fn_decl(&mut self, visibility: Visibility, doc: Option) -> Result { let start = self.current_span(); diff --git a/src/symbol_table.rs b/src/symbol_table.rs index fde3c57..c93aec4 100644 --- a/src/symbol_table.rs +++ b/src/symbol_table.rs @@ -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); + } } } diff --git a/src/typechecker.rs b/src/typechecker.rs index f2182e3..5c59d22 100644 --- a/src/typechecker.rs +++ b/src/typechecker.rs @@ -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)); + } } }