feat: add extern fn declarations for JS FFI

Adds `extern fn` syntax for declaring external JavaScript functions:
  extern fn getElementById(id: String): Element
  extern fn getContext(el: Element, kind: String): CanvasCtx = "getContext"
  pub extern fn alert(msg: String): Unit

Changes across 11 files:
- Lexer: `extern` keyword
- AST: `ExternFnDecl` struct + `Declaration::ExternFn` variant
- Parser: parse `extern fn` with optional `= "jsName"` override
- Typechecker: register extern fn type signatures
- Interpreter: ExternFn value with clear error on call
- JS backend: emit extern fn calls using JS name (no _lux suffix)
- C backend: silently skips extern fns
- Formatter, linter, modules, symbol_table: handle new variant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 18:38:42 -05:00
parent 400acc3f35
commit fbb7ddb6c3
11 changed files with 312 additions and 7 deletions

View File

@@ -71,6 +71,8 @@ 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>,
}
impl JsBackend {
@@ -93,6 +95,7 @@ impl JsBackend {
has_handlers: false,
var_substitutions: HashMap::new(),
used_effects: HashSet::new(),
extern_fns: HashMap::new(),
}
}
@@ -112,6 +115,14 @@ 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());
}
_ => {}
}
}
@@ -2723,6 +2734,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)
}