From 01fee9396a0be2426bbe1b015547782f21f253d5 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 4 May 2026 10:58:07 -0500 Subject: [PATCH] codegen-js: native JS method dispatch and extern fn support Any-typed receiver method calls now emit obj.method(args) directly instead of requiring native_js_call. client.auth.signInWithOtp(p) compiles to client["auth"].signInWithOtp(p) -- no escape hatch needed. Field access emits obj["field"] (direct bracket notation) instead of el_get_field, so prototype-inherited JS properties resolve correctly. el_get_field's hasOwnProperty guard was silently returning null for real JS objects with inherited fields (Supabase auth, DOM APIs, etc). El runtime shortform methods (append, len, get, map_get, map_set) still use the existing method(obj, args) convention for backward compat. ExternFn statements emit a comment and are excluded from top-level statement codegen -- the extern declaration tells the compiler the function exists in the JS environment without emitting a body. --- el-compiler/src/codegen-js.el | 66 ++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/el-compiler/src/codegen-js.el b/el-compiler/src/codegen-js.el index 882f796..924329a 100644 --- a/el-compiler/src/codegen-js.el +++ b/el-compiler/src/codegen-js.el @@ -86,6 +86,26 @@ fn js_binop(op: String) -> String { op } +// ── Known El runtime method names ───────────────────────────────────────────── +// +// These are the method shortforms exported by el_runtime.js and used by the +// El C-backend convention of `obj.method(args)` -> `method(obj, args)`. +// Any method name NOT in this set is treated as a native JS method call on the +// receiver object, emitting `obj.method(args)` directly. +// +// This is the mechanism that makes `client.auth.signInWithOtp(payload)` work +// without `native_js_call`: the receiver is Any-typed, the method is unknown +// to El, so codegen emits the JS call directly. + +fn js_is_el_method(name: String) -> Bool { + if str_eq(name, "append") { return true } + if str_eq(name, "len") { return true } + if str_eq(name, "get") { return true } + if str_eq(name, "map_get") { return true } + if str_eq(name, "map_set") { return true } + false +} + // ── Async function tracking ─────────────────────────────────────────────────── // // Functions decorated with @async are recorded here. Any call to a known-async @@ -419,16 +439,27 @@ fn js_cg_expr(expr: Map) -> String { } if func_kind == "Field" { - // El's `obj.method(args)` becomes `method(obj, args)` — same - // convention as the C backend. The runtime exports method - // shortforms (append, len, get, map_get, map_set) that match. let obj = func["object"] let field: String = func["field"] let obj_c: String = js_cg_expr(obj) - if arity > 0 { - return field + "(" + obj_c + ", " + args_c + ")" + // If the method is a known El runtime shortform, keep the El + // convention: `method(obj, args)`. This preserves backward + // compatibility with list.append(x), map.map_get(k), etc. + if js_is_el_method(field) { + if arity > 0 { + return field + "(" + obj_c + ", " + args_c + ")" + } + return field + "(" + obj_c + ")" } - return field + "(" + obj_c + ")" + // Unknown method — emit as a native JS method call on the + // receiver. This handles Any-typed values (third-party library + // objects, DOM elements, Promises, etc.) without requiring + // native_js_call. Example: `client.auth.signInWithOtp(payload)` + // emits `client["auth"].signInWithOtp(args_c)`. + if arity > 0 { + return obj_c + "." + field + "(" + args_c + ")" + } + return obj_c + "." + field + "()" } let fn_c: String = js_cg_expr(func) @@ -436,13 +467,13 @@ fn js_cg_expr(expr: Map) -> String { } if kind == "Field" { - // El's `obj.foo` becomes JS `obj["foo"]` — works on plain objects - // (maps) and on JS objects with prototype. el_get_field is a - // runtime helper for callers that want EL_NULL on missing keys. + // El's `obj.foo` becomes JS `obj["foo"]` — direct bracket access. + // This works for plain El map objects AND for real JS objects with + // prototype-inherited properties (DOM elements, third-party library + // objects, Promises, etc.). el_get_field used hasOwnProperty which + // silently returned null for inherited props, breaking e.g. client.auth. // - // If the object is a Try (nil-propagation postfix `obj?`), use JS - // optional chaining: `obj?.["foo"]` — returns undefined (null) if obj - // is null/undefined, otherwise accesses the field. + // Nil-propagation: `obj?.foo` emits `(obj)?.["foo"] ?? null`. let obj = expr["object"] let field: String = expr["field"] let obj_kind: String = obj["expr"] @@ -452,7 +483,7 @@ fn js_cg_expr(expr: Map) -> String { return "(" + inner_c + ")?.[" + js_str_lit(field) + "] ?? null" } let obj_c: String = js_cg_expr(obj) - return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")" + return obj_c + "[" + js_str_lit(field) + "]" } if kind == "Index" { @@ -715,6 +746,14 @@ fn js_cg_stmt(stmt: Map, indent: String, declared: [String]) -> [St if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } + // ExternFn: the function exists in the JS environment (loaded via