// 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
}

// ── 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<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
}

// ── 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<String, Any>], acc_var: String) -> String {
    let n: Int = native_list_len(children)
    let i = 0
    let out = ""
    while i < n {
        let child: Map<String, Any> = 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 + " += \"<!doctype html>\"; "
        }
        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<String, Any>], acc_var: String) -> String {
    let n: Int = native_list_len(attrs)
    let i = 0
    let out = ""
    while i < n {
        let attr: Map<String, Any> = 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<String, Any>, acc_var: String) -> String {
    let tag: String = elem["tag"]
    let attrs: [Map<String, Any>] = elem["attrs"]
    let children: [Map<String, Any>] = 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("</" + tag + ">") + "; "
    }
    out
}

fn js_cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
    let list_expr = node["list"]
    let item_name: String = node["item"]
    let body_children: [Map<String, Any>] = 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, Any>) -> 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 + " += \"<!doctype html>\"; "
    }
    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, 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" {
            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, 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 {
                            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, Any>) -> 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<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 == "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 <script>
    // tag or the module context). Emit a comment so the generated file is
    // self-documenting, but no JS function body — the implementation is external.
    if kind == "ExternFn" {
        let ename: String = stmt["name"]
        js_emit_line(indent + "// extern fn " + ename + " — provided by the JS environment")
        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 }
    if kind == "ExternFn" { return true }
    false
}

// ── Entry point ───────────────────────────────────────────────────────────────

fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
    codegen_js_inner(stmts, source, false, "")
}

fn codegen_js_bundle(stmts: [Map<String, Any>], source: String, runtime_content: String) -> String {
    codegen_js_inner(stmts, source, true, runtime_content)
}

fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool, runtime_content: String) -> String {
    // Reset per-compile state.
    state_set("__js_int_names", "")
    state_set("__js_match_counter", "")
    state_set("__js_async_fns", "")
    state_set("__js_lambda_counter", "")

    // Preamble: in bundle mode, inline the runtime and wrap in IIFE.
    // In module mode, emit a single import that side-effects globalThis.
    js_emit_line("// Generated by elc --target=js")
    if bundle_mode {
        js_emit_line("// Bundle mode: runtime inlined, no import statement needed.")
        js_emit_line("// Drop directly into a <script> tag.")
        js_emit_line(";(function() {")
        js_emit_line("\"use strict\";")
        // Inline the runtime content verbatim (already read from el_runtime.js).
        // Strip the final ES export lines -- they use `export` syntax which is
        // not valid inside an IIFE. The globalThis.__el assignment is what matters.
        js_emit_line(js_strip_es_exports(runtime_content))
        js_emit_line("")
    } else {
        js_emit_line("// Runtime: foundation/el/runtime/el_runtime.js")
        js_emit_line("import \"./el_runtime.js\";")
    }
    // In module mode: destructure all builtins off globalThis.__el so call
    // sites stay flat (println(x) not el.println(x)).
    // In bundle mode: function declarations from the inlined runtime are
    // already in scope within the IIFE -- no destructure needed.
    if !bundle_mode {
        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("  dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,")
        js_emit_line("  dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,")
        js_emit_line("  set_timeout, set_interval, clear_interval,")
        js_emit_line("  local_storage_get, local_storage_set, local_storage_remove,")
        js_emit_line("  window_location, window_redirect, window_on_load,")
        js_emit_line("  console_log,")
        js_emit_line("  window_set, window_get, native_js, native_js_call,")
        js_emit_line("  promise_then, promise_catch, promise_resolve, promise_reject,")
        js_emit_line("  object_assign, object_keys, object_values, json_deep_clone,")
        js_emit_line("  array_from, type_of, instanceof_check,")
        js_emit_line("} = globalThis.__el;")
        js_emit_blank()
    }

    // URL import pass: emit `import "url"` (module mode) or a comment
    // (bundle mode) for any import whose path starts with http(s):// or
    // doesn't end in .el (i.e., it's a JS/CSS/CDN import, not an El source
    // import which was already inlined by resolve_imports).
    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, "Import") {
            let ipath: String = stmt["path"]
            let is_url = str_starts_with(ipath, "http://")
            let is_url = is_url || str_starts_with(ipath, "https://")
            let is_js = !str_ends_with(ipath, ".el")
            if is_url || is_js {
                if bundle_mode {
                    js_emit_line("// external: " + ipath)
                } else {
                    js_emit_line("import " + js_str_lit(ipath) + ";")
                }
            }
        }
        let i = i + 1
    }
    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();")
    }

    // Close IIFE in bundle mode.
    if bundle_mode {
        js_emit_line("")
        js_emit_line("})();")
    }

    // Return empty string — output was streamed via println
    ""
}

// Strip ES module export statements from runtime content for IIFE embedding.
// The runtime ends with `export { ... }` and `export { __el as default }` lines
// that are invalid inside an IIFE. We strip everything from the first top-level
// `export {` line onward.
//
// Also strips `import` statements at the top if any (though el_runtime.js has none).
fn js_strip_es_exports(content: String) -> String {
    let lines: [String] = str_split(content, "\n")
    let n: Int = native_list_len(lines)
    let out: [String] = native_list_empty()
    let i = 0
    while i < n {
        let line: String = native_list_get(lines, i)
        let trimmed: String = str_trim(line)
        // Stop at top-level `export {` or `export default`
        if str_starts_with(trimmed, "export {") {
            let i = n
        } else {
            if str_starts_with(trimmed, "export default") {
                let i = n
            } else {
                let out = native_list_append(out, line)
            }
        }
        let i = i + 1
    }
    str_join(out, "\n")
}
