// 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 } // ── 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 } // ── 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"] return fn_name + "(" + args_c + ")" } 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 + ")" } return field + "(" + obj_c + ")" } let fn_c: String = js_cg_expr(func) return fn_c + "(" + args_c + ")" } 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. let obj = expr["object"] let field: String = expr["field"] let obj_c: String = js_cg_expr(obj) return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")" } if kind == "Index" { // Map vs list dispatch on the index expression kind, same as C. 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"] 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" { 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) } "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 { // 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, "") } // ── 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 == "CgiBlock" { // CGI blocks compile to a no-op + warning comment in JS target. // The runtime cgi identity is server-side; UI code is not a CGI // principal. See spec/codegen-js.md §7. let cname: String = stmt["name"] js_emit_line(indent + "// cgi block '" + cname + "' — no-op in JS target (server-side concept)") return declared } if kind == "ServiceBlock" { let sname: String = stmt["name"] js_emit_line(indent + "// service block '" + sname + "' — no-op in JS target") return declared } declared } // Strip a single layer of surrounding parentheses from a JS expression string. fn js_strip_outer_parens(s: String) -> String { let chars: [String] = native_string_chars(s) let n: Int = native_list_len(chars) if n < 2 { return s } let first: String = native_list_get(chars, 0) let last: String = native_list_get(chars, n - 1) if first == "(" { if last == ")" { let depth = 1 let i = 1 let balanced = true while i < n - 1 { let ch: String = native_list_get(chars, i) if ch == "(" { let depth = depth + 1 } if ch == ")" { let depth = depth - 1 if depth == 0 { let balanced = false let i = n } } let i = i + 1 } if balanced { return str_slice(s, 1, n - 1) } } } s } fn js_cg_if_stmt(expr: Map, indent: String, declared: [String]) -> Void { let cond = expr["cond"] let then_stmts = expr["then"] let else_stmts = expr["else"] let has_else: Bool = expr["has_else"] let cond_c: String = js_cg_expr(cond) let cond_c = js_strip_outer_parens(cond_c) js_emit_line(indent + "if (" + cond_c + ") {") js_cg_stmts(then_stmts, indent + " ", native_list_clone(declared)) if has_else { js_emit_line(indent + "} else {") js_cg_stmts(else_stmts, indent + " ", native_list_clone(declared)) } js_emit_line(indent + "}") } fn js_cg_for_body(item: String, list_expr: Map, body: [Map], indent: String, declared: [String]) -> Void { let list_c: String = js_cg_expr(list_expr) js_emit_line(indent + "for (const " + item + " of " + list_c + ") {") let body_decl = native_list_clone(declared) let body_decl = native_list_append(body_decl, item) js_cg_stmts(body, indent + " ", body_decl) js_emit_line(indent + "}") } fn js_cg_for_stmt(expr: Map, indent: String, declared: [String]) -> Void { let item: String = expr["item"] let list_expr = expr["list"] let body = expr["body"] js_cg_for_body(item, list_expr, body, indent, declared) } fn js_cg_stmts(stmts: [Map], indent: String, declared: [String]) -> [String] { let n: Int = native_list_len(stmts) let i = 0 let decl = declared while i < n { let stmt = native_list_get(stmts, i) let decl = js_cg_stmt(stmt, indent, decl) let i = i + 1 } decl } // ── Function declaration codegen ────────────────────────────────────────────── fn js_params_str(params: [Map]) -> String { let n: Int = native_list_len(params) if n == 0 { return "" } let parts: [String] = native_list_empty() let i = 0 while i < n { let param = native_list_get(params, i) let name: String = param["name"] let parts = native_list_append(parts, name) let i = i + 1 } str_join(parts, ", ") } // Same implicit-return transform as the C backend. fn js_transform_implicit_return(body: [Map]) -> [Map] { let n: Int = native_list_len(body) if n == 0 { return body } let last: Map = native_list_get(body, n - 1) let last_kind: String = last["stmt"] if last_kind == "Expr" { let val = last["value"] let val_kind: String = val["expr"] if val_kind == "If" { return body } if val_kind == "For" { return body } let new_body: [Map] = native_list_empty() let i = 0 while i < n - 1 { let new_body = native_list_append(new_body, native_list_get(body, i)) let i = i + 1 } let return_stmt: Map = { "stmt": "Return", "value": val } let new_body = native_list_append(new_body, return_stmt) return new_body } body } fn js_cg_fn(stmt: Map) -> Void { let fn_name: String = stmt["name"] let params = stmt["params"] let body = stmt["body"] let ret_type: String = stmt["ret_type"] let params_str: String = js_params_str(params) js_build_int_names_for_params(params) // Special-case `fn main` — emit as a regular function and call it // at module bottom (after all top-level statements). This matches // the C backend's behavior where `fn main` is the entry point. if fn_name == "main" { js_emit_line("function main(" + params_str + ") {") } else { js_emit_line("function " + fn_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_cg_stmts(body_xformed, " ", decl) js_emit_line("}") js_emit_blank() } // ── Top-level codegen ───────────────────────────────────────────────────────── fn js_is_fndef(stmt: Map) -> Bool { let kind: String = stmt["stmt"] if kind == "FnDef" { return true } false } fn js_is_top_level_decl(stmt: Map) -> Bool { let kind: String = stmt["stmt"] if kind == "TypeDef" { return true } if kind == "EnumDef" { return true } if kind == "Import" { return true } if kind == "CgiBlock" { return true } if kind == "ServiceBlock" { return true } false } // ── Entry point ─────────────────────────────────────────────────────────────── fn codegen_js(stmts: [Map], source: String) -> String { // Reset per-compile state. state_set("__js_int_names", "") state_set("__js_match_counter", "") // Preamble: inline the runtime via a single import that side-effects // globalThis. The runtime path is resolved relative to the generated // output; users running `elc --target=js` are responsible for ensuring // el_runtime.js is reachable. For self-contained output, the runtime // could be inlined; that is a follow-up. js_emit_line("// Generated by elc --target=js") js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js") js_emit_line("import \"./el_runtime.js\";") js_emit_line("const {") js_emit_line(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,") js_emit_line(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,") js_emit_line(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,") js_emit_line(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,") js_emit_line(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,") js_emit_line(" list_push, list_join, list_range,") js_emit_line(" el_map_new, el_get_field, el_map_get, el_map_set,") js_emit_line(" http_get, http_post, http_post_json,") js_emit_line(" fs_read, fs_write, fs_list,") js_emit_line(" json_parse, json_stringify, json_get, json_get_string, json_get_int,") js_emit_line(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,") js_emit_line(" el_retain, el_release,") js_emit_line(" append, len, get, map_get, map_set,") js_emit_line(" native_list_get, native_list_len, native_list_append, native_list_empty,") js_emit_line(" native_list_clone, native_string_chars, native_int_to_str,") js_emit_line(" args, state_set, state_get, state_del, state_keys, env,") js_emit_line(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,") js_emit_line(" engram_node, engram_search, engram_activate,") js_emit_line(" llm_call, llm_call_system,") js_emit_line("} = globalThis.__el;") js_emit_blank() // Function definitions let n: Int = native_list_len(stmts) let i = 0 while i < n { let stmt = native_list_get(stmts, i) if js_is_fndef(stmt) { js_cg_fn(stmt) } let i = i + 1 } // Top-level statements (those that are not FnDef and not declarative) // run at module load. If the program defines `fn main`, we additionally // call main() at the end so the C-backend mental model of "fn main is // the entry point" carries over. let has_main = false let i = 0 while i < n { let stmt = native_list_get(stmts, i) let sk: String = stmt["stmt"] if str_eq(sk, "FnDef") { let fn_name: String = stmt["name"] if str_eq(fn_name, "main") { let has_main = true } } let i = i + 1 } let main_decl = native_list_empty() let i = 0 while i < n { let stmt = native_list_get(stmts, i) if js_is_fndef(stmt) { // skip } else { if js_is_top_level_decl(stmt) { // skip } else { let main_decl = js_cg_stmt(stmt, "", main_decl) } } let i = i + 1 } if has_main { js_emit_blank() js_emit_line("main();") } // Return empty string — output was streamed via println "" }