// codegen-js.el — El compiler JavaScript source code generator // // Input: list of AST statement maps (from parser.el) // Output: JavaScript source printed to stdout (streamed, one line at a time) // // Each El program compiles to a single .js file that imports el_runtime.js // (which side-effects globals so call sites stay flat — println(x), not // el.println(x)). Functions map to JS function declarations; top-level // statements run at module load. // // Entry point: fn codegen_js(stmts: [Map], source: String) -> String // Returns "" — output goes to stdout via println(). // // This file mirrors codegen.el (the C backend). Where the C backend has to // fight the int64_t-everywhere convention to dispatch arithmetic vs concat // or `==` vs `str_eq`, the JS backend can usually let JS's own operator // semantics do the right thing. We retain the dispatch logic for clarity // and so that explicit calls to `el_str_concat` or `str_eq` still work. // ── String helpers ──────────────────────────────────────────────────────────── // Escape a JS string literal (double-quotes, backslashes, newlines, etc.). fn js_escape(s: String) -> String { let chars: [String] = native_string_chars(s) let total: Int = native_list_len(chars) let parts: [String] = native_list_empty() let i = 0 while i < total { let ch: String = native_list_get(chars, i) if ch == "\"" { let parts = native_list_append(parts, "\\\"") } else { if ch == "\\" { let parts = native_list_append(parts, "\\\\") } else { if ch == "\n" { let parts = native_list_append(parts, "\\n") } else { if ch == "\r" { let parts = native_list_append(parts, "\\r") } else { if ch == "\t" { let parts = native_list_append(parts, "\\t") } else { let parts = native_list_append(parts, ch) } } } } } let i = i + 1 } str_join(parts, "") } fn js_str_lit(s: String) -> String { "\"" + js_escape(s) + "\"" } // ── Code emission ───────────────────────────────────────────────────────────── fn js_emit_line(line: String) -> Void { println(line) } fn js_emit_blank() -> Void { println("") } // ── Operator helpers ────────────────────────────────────────────────────────── fn js_binop(op: String) -> String { if op == "Plus" { return "+" } if op == "Minus" { return "-" } if op == "Star" { return "*" } if op == "Slash" { return "/" } if op == "Percent" { return "%" } if op == "EqEq" { return "===" } if op == "NotEq" { return "!==" } if op == "Lt" { return "<" } if op == "Gt" { return ">" } if op == "LtEq" { return "<=" } if op == "GtEq" { return ">=" } if op == "And" { return "&&" } if op == "Or" { return "||" } 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 // builtin (http_get, http_post, http_post_json) or to a user-declared @async // function gets an `await` prefix in generated JS. // // Known-async builtins — these return Promise in el_runtime.js. fn js_is_async_builtin(name: String) -> Bool { if str_eq(name, "http_get") { return true } if str_eq(name, "http_post") { return true } if str_eq(name, "http_post_json") { return true } if str_eq(name, "http_get_with_headers") { return true } if str_eq(name, "http_post_with_headers") { return true } false } fn js_register_async_fn(name: String) -> Bool { let csv: String = state_get("__js_async_fns") if str_eq(csv, "") { csv = "," } let key: String = "," + name + "," if str_contains(csv, key) { return true } state_set("__js_async_fns", csv + name + ",") return true } fn js_is_async_fn(name: String) -> Bool { let csv: String = state_get("__js_async_fns") if str_eq(csv, "") { return false } return str_contains(csv, "," + name + ",") } // ── Int-name tracking (mirrors codegen.el) ──────────────────────────────────── fn js_is_int_name(name: String) -> Bool { let csv: String = state_get("__js_int_names") if str_eq(csv, "") { return false } return str_contains(csv, "," + name + ",") } fn js_add_int_name(name: String) -> Bool { let csv: String = state_get("__js_int_names") if str_eq(csv, "") { csv = "," } let key: String = "," + name + "," if str_contains(csv, key) { return true } state_set("__js_int_names", csv + name + ",") return true } fn js_build_int_names_for_params(params: [Map]) -> Bool { state_set("__js_int_names", ",") let np: Int = native_list_len(params) let pi = 0 while pi < np { let param = native_list_get(params, pi) let pname: String = param["name"] let ptype: String = param["type"] if str_eq(ptype, "Int") { js_add_int_name(pname) } let pi = pi + 1 } return true } fn js_is_int_call(call_expr: Map) -> Bool { let func = call_expr["func"] let fk: String = func["expr"] if !str_eq(fk, "Ident") { return false } let name: String = func["name"] if str_eq(name, "str_len") { return true } if str_eq(name, "str_index_of") { return true } if str_eq(name, "str_to_int") { return true } if str_eq(name, "str_char_code") { return true } if str_eq(name, "native_list_len") { return true } if str_eq(name, "el_list_len") { return true } if str_eq(name, "len") { return true } if str_eq(name, "json_get_int") { return true } if str_eq(name, "time_now") { return true } if str_eq(name, "time_now_utc") { return true } if str_eq(name, "el_abs") { return true } if str_eq(name, "el_max") { return true } if str_eq(name, "el_min") { return true } return false } // ── HTML template codegen (JS) ──────────────────────────────────────────────── // // HTML template expressions compile to a JS IIFE that builds the HTML string // using string concatenation. Interpolated values go through html_escape(); // raw() bypasses escaping. {#each} blocks compile to Array.forEach or a // for-loop that pushes fragments into a parts array. // // Entry point: js_cg_html_template(expr) → JS expression string. fn js_next_html_id() -> String { let csv: String = state_get("__js_html_counter") let n = 0 if !str_eq(csv, "") { let n = str_to_int(csv) } let n = n + 1 state_set("__js_html_counter", native_int_to_str(n)) native_int_to_str(n) } fn js_cg_html_parts(children: [Map], acc_var: String) -> String { let n: Int = native_list_len(children) let i = 0 let out = "" while i < n { let child: Map = native_list_get(children, i) let html_kind: String = child["html"] if str_eq(html_kind, "Text") { let text: String = child["text"] let out = out + acc_var + " += " + js_str_lit(text) + "; " } if str_eq(html_kind, "Doctype") { let out = out + acc_var + " += \"\"; " } if str_eq(html_kind, "Interp") { let val_node = child["value"] let val_c: String = js_cg_expr(val_node) let out = out + acc_var + " += html_escape(" + val_c + "); " } if str_eq(html_kind, "Raw") { let val_node = child["value"] let val_c: String = js_cg_expr(val_node) let out = out + acc_var + " += html_raw(" + val_c + "); " } if str_eq(html_kind, "Element") { let elem_c: String = js_cg_html_element_str(child, acc_var) let out = out + elem_c } if str_eq(html_kind, "Each") { let each_c: String = js_cg_html_each(child, acc_var) let out = out + each_c } let i = i + 1 } out } fn js_cg_html_attrs_str(attrs: [Map], acc_var: String) -> String { let n: Int = native_list_len(attrs) let i = 0 let out = "" while i < n { let attr: Map = native_list_get(attrs, i) let attr_name: String = attr["name"] let kind: String = attr["kind"] // open-attr snippet: " name=\"" let open_val: String = " " + attr_name + "=\"" if str_eq(kind, "static") { let sv: String = attr["value"] let out = out + acc_var + " += " + js_str_lit(open_val) + "; " let out = out + acc_var + " += " + js_str_lit(sv) + "; " let out = out + acc_var + " += " + js_str_lit("\"") + "; " } else { if str_eq(kind, "dynamic") { let val_node = attr["value"] let val_c: String = js_cg_expr(val_node) let out = out + acc_var + " += " + js_str_lit(open_val) + "; " let out = out + acc_var + " += html_escape(" + val_c + "); " let out = out + acc_var + " += " + js_str_lit("\"") + "; " } else { // Boolean attribute let out = out + acc_var + " += " + js_str_lit(" " + attr_name) + "; " } } let i = i + 1 } out } fn js_cg_html_element_str(elem: Map, acc_var: String) -> String { let tag: String = elem["tag"] let attrs: [Map] = elem["attrs"] let children: [Map] = elem["children"] let self_closing: Bool = elem["self_closing"] let out = acc_var + " += " + js_str_lit("<" + tag) + "; " let out = out + js_cg_html_attrs_str(attrs, acc_var) if self_closing { let out = out + acc_var + " += \"/>\"" + "; " } else { let out = out + acc_var + " += \">\"; " let out = out + js_cg_html_parts(children, acc_var) let out = out + acc_var + " += " + js_str_lit("") + "; " } out } fn js_cg_html_each(node: Map, acc_var: String) -> String { let list_expr = node["list"] let item_name: String = node["item"] let body_children: [Map] = node["body"] let id: String = js_next_html_id() let list_var: String = "_html_list_" + id let len_var: String = "_html_len_" + id let idx_var: String = "_html_i_" + id let list_c: String = js_cg_expr(list_expr) let inner_c: String = js_cg_html_parts(body_children, acc_var) "{ const " + list_var + " = " + list_c + "; const " + len_var + " = el_list_len(" + list_var + "); for (let " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { const " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } " } fn js_cg_html_template(expr: Map) -> String { let root = expr["root"] let id: String = js_next_html_id() let acc: String = "_html_" + id let doctype_flag: Bool = root["doctype"] let doctype_prefix: String = "" if doctype_flag { let doctype_prefix = acc + " += \"\"; " } let body: String = js_cg_html_element_str(root, acc) "(() => { let " + acc + " = \"\"; " + doctype_prefix + body + "return " + acc + "; })()" } // ── Expression codegen ──────────────────────────────────────────────────────── // // js_cg_expr returns a JS expression string (not a statement). // // Note: the C backend's `+` dispatch is preserved here for two reasons: // 1) Generated output stays grep-equivalent across targets // 2) Explicit `el_str_concat()` lives in the runtime; codegen routes // through it for ambiguous (Ident+Ident, Call+Call) cases. JS's // own `+` would also work, but el_str_concat coerces both sides // to strings — closer to the C semantics. fn js_cg_expr(expr: Map) -> String { let kind: String = expr["expr"] if kind == "Int" { let v: String = expr["value"] return v } // DurationLit — postfix-literal time value (e.g. 30.seconds, 1.hour). // The JS backend lowers to a literal integer nanosecond count. The C // backend uses the typed wrapper el_duration_from_nanos to make intent // explicit at the runtime boundary; JS has no equivalent shim yet, so // we lower directly. A future Phase 2 JS time runtime can route through // a wrapper once added. if kind == "DurationLit" { let count: String = expr["count"] let unit: String = expr["unit"] let mult_ns = "1" if str_eq(unit, "nano") { let mult_ns = "1" } if str_eq(unit, "nanos") { let mult_ns = "1" } if str_eq(unit, "milli") { let mult_ns = "1000000" } if str_eq(unit, "millis") { let mult_ns = "1000000" } if str_eq(unit, "millisecond") { let mult_ns = "1000000" } if str_eq(unit, "milliseconds") { let mult_ns = "1000000" } if str_eq(unit, "second") { let mult_ns = "1000000000" } if str_eq(unit, "seconds") { let mult_ns = "1000000000" } if str_eq(unit, "minute") { let mult_ns = "60000000000" } if str_eq(unit, "minutes") { let mult_ns = "60000000000" } if str_eq(unit, "hour") { let mult_ns = "3600000000000" } if str_eq(unit, "hours") { let mult_ns = "3600000000000" } if str_eq(unit, "day") { let mult_ns = "86400000000000" } if str_eq(unit, "days") { let mult_ns = "86400000000000" } return "(" + count + " * " + mult_ns + ")" } if kind == "Float" { // JS numbers are already doubles — no bit-cast trick needed. let v: String = expr["value"] return v } if kind == "Str" { let v: String = expr["value"] return js_str_lit(v) } if kind == "Bool" { let v: String = expr["value"] if v == "true" { return "true" } return "false" } if kind == "Nil" { return "null" } if kind == "Ident" { let name: String = expr["name"] return name } if kind == "Not" { let inner = expr["inner"] let inner_c: String = js_cg_expr(inner) return "!" + inner_c } if kind == "Neg" { let inner = expr["inner"] let inner_c: String = js_cg_expr(inner) return "(-" + inner_c + ")" } if kind == "BinOp" { let op: String = expr["op"] let left = expr["left"] let right = expr["right"] let left_c: String = js_cg_expr(left) let right_c: String = js_cg_expr(right) let left_kind: String = left["expr"] let right_kind: String = right["expr"] // Plus dispatch — same shape as C backend, but we route through // el_str_concat for the string-concat path (its JS impl coerces // and matches C's behavior). Arithmetic uses bare JS `+`. if op == "Plus" { if left_kind == "Str" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Str" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if left_kind == "Int" { return "(" + left_c + " + " + right_c + ")" } if right_kind == "Int" { return "(" + left_c + " + " + right_c + ")" } if left_kind == "Ident" { if right_kind == "Ident" { let lname: String = left["name"] let rname: String = right["name"] if js_is_int_name(lname) { if js_is_int_name(rname) { return "(" + left_c + " + " + right_c + ")" } } } } if left_kind == "Ident" { if right_kind == "Call" { let lname: String = left["name"] if js_is_int_name(lname) { if js_is_int_call(right) { return "(" + left_c + " + " + right_c + ")" } } } } if right_kind == "Ident" { if left_kind == "Call" { let rname: String = right["name"] if js_is_int_name(rname) { if js_is_int_call(left) { return "(" + left_c + " + " + right_c + ")" } } } } if left_kind == "Call" { if right_kind == "Call" { if js_is_int_call(left) { if js_is_int_call(right) { return "(" + left_c + " + " + right_c + ")" } } } return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { return "el_str_concat(" + left_c + ", " + right_c + ")" } // Fallback: when in doubt, route through el_str_concat. JS's // own + handles strings and numbers natively, but el_str_concat // gives us a single point of control if behavior needs to diverge. if left_kind == "Ident" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Ident" { return "el_str_concat(" + left_c + ", " + right_c + ")" } } // Equality dispatch — C backend disambiguates via str_eq for // strings and == for ints. JS does both with === if we know // the types are uniform; for ambiguous identifier pairs we // route through str_eq for safety (it falls back to === in JS). if op == "EqEq" { if left_kind == "Int" { return "(" + left_c + " === " + right_c + ")" } if right_kind == "Int" { return "(" + left_c + " === " + right_c + ")" } if left_kind == "Bool" { return "(" + left_c + " === " + right_c + ")" } if right_kind == "Bool" { return "(" + left_c + " === " + right_c + ")" } if left_kind == "Nil" { return "(" + left_c + " === " + right_c + ")" } if right_kind == "Nil" { return "(" + left_c + " === " + right_c + ")" } if left_kind == "Ident" { if right_kind == "Ident" { let lname: String = left["name"] let rname: String = right["name"] if js_is_int_name(lname) { if js_is_int_name(rname) { return "(" + left_c + " === " + right_c + ")" } } } } if left_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } // Default: === (works for strings, numbers, bools in JS) return "(" + left_c + " === " + right_c + ")" } if op == "NotEq" { if left_kind == "Int" { return "(" + left_c + " !== " + right_c + ")" } if right_kind == "Int" { return "(" + left_c + " !== " + right_c + ")" } if left_kind == "Bool" { return "(" + left_c + " !== " + right_c + ")" } if right_kind == "Bool" { return "(" + left_c + " !== " + right_c + ")" } if left_kind == "Nil" { return "(" + left_c + " !== " + right_c + ")" } if right_kind == "Nil" { return "(" + left_c + " !== " + right_c + ")" } if left_kind == "Ident" { if right_kind == "Ident" { let lname: String = left["name"] let rname: String = right["name"] if js_is_int_name(lname) { if js_is_int_name(rname) { return "(" + left_c + " !== " + right_c + ")" } } } } if left_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } return "(" + left_c + " !== " + right_c + ")" } let op_c: String = js_binop(op) return "(" + left_c + " " + op_c + " " + right_c + ")" } if kind == "Call" { let func = expr["func"] let args = expr["args"] let arity: Int = native_list_len(args) let func_kind: String = func["expr"] let args_parts: [String] = native_list_empty() let i = 0 while i < arity { let arg = native_list_get(args, i) let arg_c: String = js_cg_expr(arg) let args_parts = native_list_append(args_parts, arg_c) let i = i + 1 } let args_c: String = str_join(args_parts, ", ") if func_kind == "Ident" { let fn_name: String = func["name"] let call_expr: String = fn_name + "(" + args_c + ")" if js_is_async_builtin(fn_name) { return "await " + call_expr } if js_is_async_fn(fn_name) { return "await " + call_expr } return call_expr } if func_kind == "Field" { let obj = func["object"] let field: String = func["field"] let obj_c: String = js_cg_expr(obj) // 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 + ")" } // 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) return fn_c + "(" + args_c + ")" } if kind == "Field" { // 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. // // Nil-propagation: `obj?.foo` emits `(obj)?.["foo"] ?? null`. let obj = expr["object"] let field: String = expr["field"] let obj_kind: String = obj["expr"] if str_eq(obj_kind, "Try") { let inner = obj["inner"] let inner_c: String = js_cg_expr(inner) return "(" + inner_c + ")?.[" + js_str_lit(field) + "] ?? null" } let obj_c: String = js_cg_expr(obj) return obj_c + "[" + js_str_lit(field) + "]" } if kind == "Index" { // Map vs list dispatch on the index expression kind, same as C. // If the object is a Try (nil-propagation), use JS optional indexing. let obj = expr["object"] let idx = expr["index"] let obj_c: String = js_cg_expr(obj) let idx_c: String = js_cg_expr(idx) let idx_kind: String = idx["expr"] let obj_kind: String = obj["expr"] if str_eq(obj_kind, "Try") { let inner = obj["inner"] let inner_c: String = js_cg_expr(inner) return "(" + inner_c + ")?.[" + idx_c + "] ?? null" } if str_eq(idx_kind, "Str") { return "el_get_field(" + obj_c + ", " + idx_c + ")" } return "el_list_get(" + obj_c + ", " + idx_c + ")" } if kind == "Array" { let elems = expr["elems"] let n: Int = native_list_len(elems) if n == 0 { return "[]" } let items_parts: [String] = native_list_empty() let i = 0 while i < n { let elem = native_list_get(elems, i) let elem_c: String = js_cg_expr(elem) let items_parts = native_list_append(items_parts, elem_c) let i = i + 1 } return "[" + str_join(items_parts, ", ") + "]" } if kind == "Map" { let pairs = expr["pairs"] let n: Int = native_list_len(pairs) if n == 0 { return "{}" } let items_parts: [String] = native_list_empty() let i = 0 while i < n { let pair = native_list_get(pairs, i) let key: String = pair["key"] let val = pair["value"] let val_c: String = js_cg_expr(val) let items_parts = native_list_append(items_parts, js_str_lit(key) + ": " + val_c) let i = i + 1 } return "{" + str_join(items_parts, ", ") + "}" } if kind == "Try" { // Postfix `?` — nil-propagation guard. // When used as `expr?.field` the Field handler above intercepts and // emits `(expr)?.["field"]`. Here, a bare `expr?` (not followed by // field/index access) passes through to the inner expression unchanged // (it acts as an identity but marks the value as "nil-propagating" for // its caller). This matches the C backend's current behavior. let inner = expr["inner"] return js_cg_expr(inner) } if kind == "If" { let cond = expr["cond"] let cond_c: String = js_cg_expr(cond) // If as expression: ternary. Body of the if-expression is not // currently emitted as expression-form for compound bodies; this // matches the C backend's if-expr stub. return "(" + cond_c + " ? 1 : 0)" } if kind == "Match" { return js_cg_match(expr) } // Lambda (anonymous function literal): fn(params) -> RetType { body } // Emitted as a JS arrow function expression: (params) => { body }. // Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... }) if kind == "Lambda" { return js_cg_lambda(expr) } if kind == "HtmlTemplate" { return js_cg_html_template(expr) } "null" } // ── Match codegen (basic) ───────────────────────────────────────────────────── // // Lower a match expression to an IIFE with if/else chain. Works for // LitInt / LitStr / LitBool / Wildcard / Binding patterns. Tagged-union // destructuring is not implemented — it's stubbed and falls through to // the wildcard path. fn js_next_match_id() -> String { let csv: String = state_get("__js_match_counter") let n = 0 if !str_eq(csv, "") { let n = str_to_int(csv) } let n = n + 1 state_set("__js_match_counter", native_int_to_str(n)) native_int_to_str(n) } fn js_cg_match(expr: Map) -> String { let subject = expr["subject"] let arms = expr["arms"] let subj_c: String = js_cg_expr(subject) let id: String = js_next_match_id() let subj_var: String = "_match_subj_" + id let parts: [String] = native_list_empty() let parts = native_list_append(parts, "((" + subj_var + ") => { ") let n: Int = native_list_len(arms) let i = 0 while i < n { let arm = native_list_get(arms, i) let pat = arm["pattern"] let body = arm["body"] let pkind: String = pat["pattern"] let body_c: String = js_cg_expr(body) if str_eq(pkind, "Wildcard") { let parts = native_list_append(parts, "return (" + body_c + "); ") } else { if str_eq(pkind, "Binding") { let bname: String = pat["name"] let parts = native_list_append(parts, "{ const " + bname + " = " + subj_var + "; return (" + body_c + "); } ") } else { if str_eq(pkind, "LitInt") { let v: String = pat["value"] let parts = native_list_append(parts, "if (" + subj_var + " === " + v + ") return (" + body_c + "); ") } else { if str_eq(pkind, "LitStr") { let v: String = pat["value"] let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(v) + ")) return (" + body_c + "); ") } else { if str_eq(pkind, "LitBool") { let v: String = pat["value"] let bv = "false" if str_eq(v, "true") { let bv = "true" } let parts = native_list_append(parts, "if (" + subj_var + " === " + bv + ") return (" + body_c + "); ") } else { if str_eq(pkind, "Variant") { // Enum::Variant patterns — El enums compile to plain // strings (the variant name) or ints. Match the subject // against the variant name string. let variant: String = pat["variant"] let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(variant) + ")) return (" + body_c + "); ") } else { // unknown pattern → wildcard let parts = native_list_append(parts, "return (" + body_c + "); ") } } } } } } let i = i + 1 } let parts = native_list_append(parts, "return null; })(" + subj_c + ")") str_join(parts, "") } // ── Lambda codegen ──────────────────────────────────────────────────────────── // // Anonymous function literals: fn(params) -> RetType { body } // // Strategy: emit the lambda as a hoisted JS function declaration with a // generated name (__lambda_N), then return the name as the expression value. // This works because JS function declarations are hoisted within their scope, // so the generated name is valid at any use site within the same function or // module. The emitted code looks like: // // function __lambda_1(event) { dom_hide(spinner); } // ... // dom_listen(btn, "click", __lambda_1); // // This approach is clean, debuggable, and avoids any need for a string-buffer // mode in the codegen. fn js_next_lambda_id() -> String { let csv: String = state_get("__js_lambda_counter") let n = 0 if !str_eq(csv, "") { let n = str_to_int(csv) } let n = n + 1 state_set("__js_lambda_counter", native_int_to_str(n)) native_int_to_str(n) } fn js_cg_lambda(expr: Map) -> String { let params = expr["params"] let body = expr["body"] let ret_type: String = expr["ret_type"] let id: String = js_next_lambda_id() let lambda_name: String = "__lambda_" + id let params_str: String = js_params_str(params) // Emit the function definition immediately into the output stream. // It will appear before the statement containing this expression. js_emit_line("function " + lambda_name + "(" + params_str + ") {") let decl = native_list_empty() let np: Int = native_list_len(params) let pi = 0 while pi < np { let param = native_list_get(params, pi) let pname: String = param["name"] let decl = native_list_append(decl, pname) let pi = pi + 1 } let body_xformed = body if !str_eq(ret_type, "Void") { let body_xformed = js_transform_implicit_return(body) } js_build_int_names_for_params(params) js_cg_stmts(body_xformed, " ", decl) js_emit_line("}") js_emit_blank() // Return the function name as the expression value. lambda_name } // ── Variable scope tracking ─────────────────────────────────────────────────── // // El allows `let x = ...` to redeclare in the same scope. JS would throw // with `let` (Identifier already declared). We track declared names and // emit bare `x = ...` on redeclaration, `let x = ...` first time. fn js_list_contains(lst: [String], s: String) -> Bool { let n: Int = native_list_len(lst) let i = 0 while i < n { let item: String = native_list_get(lst, i) if item == s { return true } let i = i + 1 } false } // ── Statement codegen ───────────────────────────────────────────────────────── fn js_cg_stmt(stmt: Map, indent: String, declared: [String]) -> [String] { let kind: String = stmt["stmt"] if kind == "Let" { let name: String = stmt["name"] let val = stmt["value"] let val_c: String = js_cg_expr(val) let ltype: String = stmt["type"] if str_eq(ltype, "Int") { js_add_int_name(name) } let vk: String = val["expr"] if str_eq(vk, "Int") { js_add_int_name(name) } if js_list_contains(declared, name) { js_emit_line(indent + name + " = " + val_c + ";") return declared } else { // Use `let` (not `const`) — El semantics allow rebinding. js_emit_line(indent + "let " + name + " = " + val_c + ";") return native_list_append(declared, name) } } if kind == "Return" { let val = stmt["value"] let val_kind: String = val["expr"] if val_kind == "Nil" { js_emit_line(indent + "return null;") } else { let val_c: String = js_cg_expr(val) js_emit_line(indent + "return " + val_c + ";") } return declared } // Bare reassignment: `name = expr`. Mirrors the C backend — emits a // plain JS assignment without `let` so we don't shadow an outer binding. if kind == "Assign" { let name: String = stmt["name"] let val = stmt["value"] let val_c: String = js_cg_expr(val) js_emit_line(indent + name + " = " + val_c + ";") return declared } if kind == "Expr" { let val = stmt["value"] let val_kind: String = val["expr"] if val_kind == "If" { js_cg_if_stmt(val, indent, declared) return declared } if val_kind == "For" { js_cg_for_stmt(val, indent, declared) return declared } let val_c: String = js_cg_expr(val) js_emit_line(indent + val_c + ";") return declared } if kind == "While" { let cond = stmt["cond"] let body = stmt["body"] let cond_c: String = js_cg_expr(cond) let cond_c = js_strip_outer_parens(cond_c) js_emit_line(indent + "while (" + cond_c + ") {") js_cg_stmts(body, indent + " ", native_list_clone(declared)) js_emit_line(indent + "}") return declared } if kind == "For" { let item: String = stmt["item"] let list_expr = stmt["list"] let body = stmt["body"] js_cg_for_body(item, list_expr, body, indent, declared) return declared } if kind == "FnDef" { return declared } if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } if kind == "TryCatch" { let try_body = stmt["try_body"] let catch_name: String = stmt["catch_name"] let catch_body = stmt["catch_body"] js_emit_line(indent + "try {") js_cg_stmts(try_body, indent + " ", native_list_clone(declared)) js_emit_line(indent + "} catch (" + catch_name + ") {") js_cg_stmts(catch_body, indent + " ", native_list_clone(declared)) js_emit_line(indent + "}") return declared } // ExternFn: the function exists in the JS environment (loaded via