Files
el/el-compiler/src/codegen-js.el
T
Will Anderson a54b2bebf9 add DOM bridge, async/await, window export, and native_js to JS target
- el_runtime.js: add 19 dom_* builtins (browser-only, throw in Node),
  window_set/window_get for exposing El functions to the browser global
  scope, and native_js/native_js_call escape hatches for third-party libs
- codegen-js.el: destructure all new builtins in generated preamble; add
  @async decorator support that emits async function + await at call sites
  for known-async HTTP builtins and user-declared @async functions; pre-
  registration pass ensures forward calls to @async functions get await
- spec/codegen-js.md: mark Phase 3 (DOM bridge) implemented, document
  @async approach and its limitations, update builtin table and status
- examples/browser-counter.el: canonical example showing dom_get_element,
  dom_set_text, dom_is_null, window_set, and state_set/get
2026-05-04 10:29:43 -05:00

1002 lines
38 KiB
EmacsLisp

// 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<String, Any>], 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
}
// 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<T> 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<String, Any>]) -> 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<String, Any>) -> 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, Any>) -> 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" {
// 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, Any>) -> 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<String, Any>, 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<String, Any>, 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<String, Any>, body: [Map<String, Any>], 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<String, Any>, 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<String, Any>], 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, Any>]) -> 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<String, Any>]) -> [Map<String, Any>] {
let n: Int = native_list_len(body)
if n == 0 { return body }
let last: Map<String, Any> = 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<String, Any>] = 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<String, Any> = { "stmt": "Return", "value": val }
let new_body = native_list_append(new_body, return_stmt)
return new_body
}
body
}
fn js_cg_fn(stmt: Map<String, Any>) -> Void {
let fn_name: String = stmt["name"]
let params = stmt["params"]
let body = stmt["body"]
let ret_type: String = stmt["ret_type"]
let decorator: String = stmt["decorator"]
let params_str: String = js_params_str(params)
js_build_int_names_for_params(params)
// Detect @async decorator emit `async function` and register the name
// so call sites for this function get `await` prefixed automatically.
// When the decorator field is absent, el_get_field returns null; str_eq
// handles null safely (returns false), so no special nil-check is needed.
if str_eq(decorator, "async") {
js_register_async_fn(fn_name)
if fn_name == "main" {
js_emit_line("async function main(" + params_str + ") {")
} else {
js_emit_line("async function " + fn_name + "(" + params_str + ") {")
}
} else {
// 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<String, Any>) -> Bool {
let kind: String = stmt["stmt"]
if kind == "FnDef" { return true }
false
}
fn js_is_top_level_decl(stmt: Map<String, Any>) -> 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<String, Any>], source: String) -> String {
// Reset per-compile state.
state_set("__js_int_names", "")
state_set("__js_match_counter", "")
state_set("__js_async_fns", "")
// 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(" dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,")
js_emit_line(" dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,")
js_emit_line(" dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,")
js_emit_line(" dom_append, dom_remove, dom_is_null,")
js_emit_line(" window_set, window_get, native_js, native_js_call,")
js_emit_line("} = globalThis.__el;")
js_emit_blank()
// Pre-registration pass: scan all FnDefs for @async decorators so that
// forward calls to @async functions get `await` even if the callee is
// defined after the caller.
let n: Int = native_list_len(stmts)
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "FnDef") {
let dec: String = stmt["decorator"]
if str_eq(dec, "async") {
let aname: String = stmt["name"]
js_register_async_fn(aname)
}
}
let i = i + 1
}
// Function definitions
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
""
}