// codegen.el - El compiler C source code generator
//
// Input:  list of AST statement maps (from parser.el)
// Output: C source printed to stdout (streamed, one line at a time)
//
// Each El program compiles to a single .c file that #includes el_runtime.h.
// Functions map directly to C functions; top-level statements become main().
//
// Entry point: fn codegen(stmts: [Map<String, Any>], source: String) -> String
//   Returns "" - output goes to stdout via println().
//
// Streaming output avoids O(n-) string concatenation: each emitted line is
// printed immediately rather than appended to a growing string.

// -- String helpers ------------------------------------------------------------

// Escape a C string literal (double-quotes and backslashes).
// Hex-encode a single nibble (0-15) as a lowercase hex character.
fn nibble_to_hex(n: Int) -> String {
    str_char_at("0123456789abcdef", n)
}

// Encode a byte value (0-255) as a two-character hex string.
fn byte_to_hex2(b: Int) -> String {
    let hi: Int = (b / 16)
    let lo: Int = (b - hi * 16)
    nibble_to_hex(hi) + nibble_to_hex(lo)
}

// Return true if the byte value is a C hex digit (0-9, a-f, A-F).
// Used to determine whether a \xNN escape needs a string-literal split
// to prevent the C preprocessor from greedily consuming following hex chars.
fn is_hex_digit_byte(b: Int) -> Bool {
    if b >= 48 { if b <= 57 { return true } }   // 0-9
    if b >= 65 { if b <= 70 { return true } }   // A-F
    if b >= 97 { if b <= 102 { return true } }  // a-f
    false
}

fn c_escape(s: String) -> String {
    // Batch ASCII chars using str_slice instead of str_char_at per byte.
    // Track clean_start: the beginning of the current run of bytes that need
    // no escaping. On each special byte, flush the accumulated clean run via
    // str_slice, then append the escape. This reduces parts-list appends from
    // O(N) to O(K) where K = number of special bytes << N for normal strings.
    //
    // Special bytes: '"'=34, '\\'=92, '\n'=10, '\r'=13, '\t'=9, any byte>=128.
    //
    // IMPORTANT: after a \xNN hex escape, if the next byte is a hex digit
    // (0-9, a-f, A-F), we emit `""` to split the C string literal so the C
    // compiler does not greedily read extra hex digits as part of the escape.
    // E.g. "\xad" followed by "bamos" must become "\xad" "bamos" because 'b'
    // is a hex digit and C would otherwise read "\xadb" (= 0xADB, out of range).
    let total: Int = str_len(s)
    let parts: [String] = native_list_empty()
    let i: Int = 0
    let clean_start: Int = 0
    let prev_was_hex_escape: Bool = false
    while i < total {
        let bval: Int = str_char_code(s, i)
        // Handle the hex-escape split case first: if prev was \xNN and this
        // byte is a hex digit, we must flush the clean run and insert "".
        // (At this point clean_start == i since the previous special byte
        // already reset it, so flush is a no-op unless something is pending.)
        if prev_was_hex_escape {
            if is_hex_digit_byte(bval) {
                // Flush any accumulated clean bytes before the split marker.
                if clean_start < i {
                    let parts = native_list_append(parts, str_slice(s, clean_start, i))
                }
                let parts = native_list_append(parts, "\"\"")
                let clean_start = i
            }
        }
        let prev_was_hex_escape = false
        if bval == 34 {
            // 34 = '"' — flush clean run, then escape
            if clean_start < i {
                let parts = native_list_append(parts, str_slice(s, clean_start, i))
            }
            let parts = native_list_append(parts, "\\\"")
            let clean_start = i + 1
        } else {
            if bval == 92 {
                // 92 = '\\'
                if clean_start < i {
                    let parts = native_list_append(parts, str_slice(s, clean_start, i))
                }
                let parts = native_list_append(parts, "\\\\")
                let clean_start = i + 1
            } else {
                if bval == 10 {
                    // 10 = '\n'
                    if clean_start < i {
                        let parts = native_list_append(parts, str_slice(s, clean_start, i))
                    }
                    let parts = native_list_append(parts, "\\n")
                    let clean_start = i + 1
                } else {
                    if bval == 13 {
                        // 13 = '\r'
                        if clean_start < i {
                            let parts = native_list_append(parts, str_slice(s, clean_start, i))
                        }
                        let parts = native_list_append(parts, "\\r")
                        let clean_start = i + 1
                    } else {
                        if bval == 9 {
                            // 9 = '\t'
                            if clean_start < i {
                                let parts = native_list_append(parts, str_slice(s, clean_start, i))
                            }
                            let parts = native_list_append(parts, "\\t")
                            let clean_start = i + 1
                        } else {
                            if bval >= 128 {
                                // Non-ASCII: flush, then \xNN
                                if clean_start < i {
                                    let parts = native_list_append(parts, str_slice(s, clean_start, i))
                                }
                                let parts = native_list_append(parts, "\\x" + byte_to_hex2(bval))
                                let prev_was_hex_escape = true
                                let clean_start = i + 1
                            }
                            // else: plain ASCII — extends the current clean run (no append)
                        }
                    }
                }
            }
        }
        let i = i + 1
    }
    // Flush the final clean run if any
    if clean_start < total {
        let parts = native_list_append(parts, str_slice(s, clean_start, total))
    }
    let result: String = str_join(parts, "")
    // parts list fully consumed — release to free peak heap.
    el_release(parts)
    result
}

fn c_str_lit(s: String) -> String {
    "\"" + c_escape(s) + "\""
}

// sanitize_test_name — convert a test name string to a valid C identifier fragment.
// "int-to-str" -> "int_to_str", "lex empty" -> "lex_empty"
fn sanitize_test_name(name: String) -> String {
    let n: Int = str_len(name)
    let i: Int = 0
    let out: String = ""
    while i < n {
        let code: Int = str_char_code(name, i)
        // a-z: 97-122, A-Z: 65-90, 0-9: 48-57 — keep; everything else -> '_'
        if code >= 97 {
            if code <= 122 {
                let out = out + str_char_at(name, i)
            } else {
                let out = out + "_"
            }
        } else {
            if code >= 65 {
                if code <= 90 {
                    let out = out + str_char_at(name, i)
                } else {
                    if code >= 48 {
                        if code <= 57 {
                            let out = out + str_char_at(name, i)
                        } else {
                            let out = out + "_"
                        }
                    } else {
                        let out = out + "_"
                    }
                }
            } else {
                if code >= 48 {
                    if code <= 57 {
                        let out = out + str_char_at(name, i)
                    } else {
                        let out = out + "_"
                    }
                } else {
                    let out = out + "_"
                }
            }
        }
        let i = i + 1
    }
    out
}

// -- Type mapping --------------------------------------------------------------

fn el_type_to_c(type_str: String) -> String {
    if type_str == "String" { return "const char*" }
    if type_str == "Int" { return "int64_t" }
    if type_str == "Bool" { return "int" }
    if type_str == "Float" { return "double" }
    if type_str == "Void" { return "void" }
    if type_str == "void" { return "void" }
    "void*"
}

// -- Code emission -------------------------------------------------------------
//
// emit_line/emit_blank stream output directly via println.
// This avoids building a large string in memory.

fn emit_line(line: String) -> Void {
    println(line)
}

fn emit_blank() -> Void {
    println("")
}

// -- Operator helpers ----------------------------------------------------------

fn binop_to_c(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
}

// -- Expression codegen --------------------------------------------------------
//
// cg_expr returns a C expression string (not a statement).

// duration_unit_nanos - multiplier from a postfix-literal unit name to
// nanoseconds. Singular and plural forms collapse to the same multiplier;
// the parser already restricted `unit` to the set is_duration_unit accepts.
// Returns the multiplier as a decimal string suitable for splicing into
// the generated C as a literal int64 expression.
fn duration_unit_nanos(unit: String) -> String {
    if str_eq(unit, "nano") { return "1LL" }
    if str_eq(unit, "nanos") { return "1LL" }
    if str_eq(unit, "milli") { return "1000000LL" }
    if str_eq(unit, "millis") { return "1000000LL" }
    if str_eq(unit, "millisecond") { return "1000000LL" }
    if str_eq(unit, "milliseconds") { return "1000000LL" }
    if str_eq(unit, "second") { return "1000000000LL" }
    if str_eq(unit, "seconds") { return "1000000000LL" }
    if str_eq(unit, "minute") { return "60000000000LL" }
    if str_eq(unit, "minutes") { return "60000000000LL" }
    if str_eq(unit, "hour") { return "3600000000000LL" }
    if str_eq(unit, "hours") { return "3600000000000LL" }
    if str_eq(unit, "day") { return "86400000000000LL" }
    if str_eq(unit, "days") { return "86400000000000LL" }
    "1LL"
}

// ── HTML template codegen ─────────────────────────────────────────────────
//
// cg_html_template(expr) emits a C statement-expression `({ ... })` that
// builds the HTML string by chaining el_str_concat calls.
//
// Interpolated values are passed through html_escape(); the raw() form
// bypasses escaping.  {#each} blocks compile to C for-loops that index into
// the list with el_list_get / el_list_len.
//
// A per-template accumulator variable `_html_N` holds the growing string.
// A global counter stored in state keeps names unique.

fn next_html_id() -> String {
    let csv: String = state_get("__html_counter")
    let n = 0
    if !str_eq(csv, "") {
        let n = str_to_int(csv)
    }
    let n = n + 1
    state_set("__html_counter", native_int_to_str(n))
    native_int_to_str(n)
}

// Emit children nodes into a flat list of C fragment strings (parts).
// Each part is either a static string fragment (already C-literal form) or
// a dynamic expression that produces an el_val_t string.
// We build them all into parts, then the caller wraps with concat chain.

fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
    // Accumulate fragments into a list to avoid O(n²) string growth.
    // Each append is O(1); the single str_join at the end is O(total_size).
    let n: Int = native_list_len(children)
    let i = 0
    let parts: [String] = native_list_empty()
    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 parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); ")
        }
        if str_eq(html_kind, "Doctype") {
            let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<!doctype html>\")); ")
        }
        if str_eq(html_kind, "Interp") {
            let val_node = child["value"]
            let val_c: String = cg_expr(val_node)
            let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); ")
        }
        if str_eq(html_kind, "Raw") {
            let val_node = child["value"]
            let val_c: String = cg_expr(val_node)
            let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", html_raw(" + val_c + ")); ")
        }
        if str_eq(html_kind, "Element") {
            let elem_c: String = cg_html_element_str(child, acc_var)
            let parts = native_list_append(parts, elem_c)
        }
        if str_eq(html_kind, "Each") {
            let each_c: String = cg_html_each(child, acc_var)
            let parts = native_list_append(parts, each_c)
        }
        if str_eq(html_kind, "HtmlIf") {
            let if_c: String = cg_html_if(child, acc_var)
            let parts = native_list_append(parts, if_c)
        }
        let i = i + 1
    }
    str_join(parts, "")
}

// Generate open-tag attribute fragments inline.
// Parser stores attrs with "kind": "static" | "dynamic" | "bool".
// Static: "value" is the raw string value (not an expr node).
// Dynamic: "value" is an expr node.
// Bool: no "value" field.
fn cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
    // Accumulate fragments into a list to avoid O(n²) string growth.
    let n: Int = native_list_len(attrs)
    let i = 0
    let parts: [String] = native_list_empty()
    // Closing-quote snippet: EL_STR("\"") in C text.
    let close_q: String = "EL_STR(" + c_str_lit("\"") + ")"
    while i < n {
        let attr: Map<String, Any> = native_list_get(attrs, i)
        let attr_name: String = attr["name"]
        let kind: String = attr["kind"]
        // Build: EL_STR(" name=\"")
        let open_val: String = " " + attr_name + "=\""
        let open_attr: String = "EL_STR(" + c_str_lit(open_val) + ")"
        if str_eq(kind, "static") {
            // Static attribute: value is a raw string.
            let sv: String = attr["value"]
            let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); ")
            let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); ")
            let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); ")
        } else {
            if str_eq(kind, "dynamic") {
                // Dynamic attribute: value is an expr node — html_escape it.
                let val_node = attr["value"]
                let val_c: String = cg_expr(val_node)
                let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); ")
                let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); ")
                let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); ")
            } else {
                // Boolean attribute (no value): emit " name"
                let bool_attr: String = "EL_STR(" + c_str_lit(" " + attr_name) + ")"
                let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); ")
            }
        }
        let i = i + 1
    }
    str_join(parts, "")
}

// Generate code for a single element, appending into acc_var.
fn 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"]
    // Accumulate into a list to avoid O(n²) string growth for deeply nested trees.
    let parts: [String] = native_list_empty()
    // Open tag: <tagname
    let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<" + tag + "\")); ")
    let parts = native_list_append(parts, cg_html_attrs_str(attrs, acc_var))
    if self_closing {
        // Self-closing void element: />
        let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); ")
    } else {
        // Close open tag: >
        let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); ")
        let parts = native_list_append(parts, cg_html_parts(children, acc_var))
        let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"</" + tag + ">\")); ")
    }
    str_join(parts, "")
}

// Generate code for {#each list as item} ... {/each}.
fn 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 = 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 = cg_expr(list_expr)
    let inner_c: String = cg_html_parts(body_children, acc_var)
    // Emit: { el_val_t _list = expr; int _len = el_list_len(_list);
    //         for (int _i = 0; _i < _len; _i++) {
    //           el_val_t item = el_list_get(_list, _i); inner_c } }
    "{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
}

// Generate code for {#if cond} ... {/if} (with optional {#else}).
fn cg_html_if(node: Map<String, Any>, acc_var: String) -> String {
    let cond_expr = node["cond"]
    let then_children: [Map<String, Any>] = node["then"]
    let else_children: [Map<String, Any>] = node["else"]
    let cond_c: String = cg_expr(cond_expr)
    let then_c: String = cg_html_parts(then_children, acc_var)
    let else_c: String = cg_html_parts(else_children, acc_var)
    "if (" + cond_c + ") { " + then_c + " } else { " + else_c + " } "
}

// Top-level HTML template codegen — returns a C statement-expression string.
fn cg_html_template(expr: Map<String, Any>) -> String {
    let root = expr["root"]
    let id: String = next_html_id()
    let acc: String = "_html_" + id
    // If the root element has doctype:true the parser tagged it from <!doctype html>
    let doctype_flag: Bool = root["doctype"]
    let doctype_prefix: String = ""
    if doctype_flag {
        let doctype_prefix = acc + " = el_str_concat(" + acc + ", EL_STR(\"<!doctype html>\")); "
    }
    let body: String = cg_html_element_str(root, acc)
    "({ el_val_t " + acc + " = EL_STR(\"\"); " + doctype_prefix + body + acc + "; })"
}

fn 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).
    // Lowered to a literal int64 nanosecond count, wrapped in the runtime
    // entry point so the intent is explicit at the C level. The arithmetic
    // is fully constant-folded by any optimising C compiler.
    if kind == "DurationLit" {
        let count: String = expr["count"]
        let unit: String = expr["unit"]
        let mult: String = duration_unit_nanos(unit)
        return "el_duration_from_nanos((el_val_t)(" + count + "LL * " + mult + "))"
    }

    if kind == "Float" {
        // Wrap Float literals in el_from_float() so the bit pattern is
        // preserved through the el_val_t (int64) slot. Without this,
        // implicit double->int64 conversion in C truncates `0.8` to `0`
        // when passed to a builtin that expects el_val_t.
        let v: String = expr["value"]
        return "el_from_float(" + v + ")"
    }

    if kind == "Str" {
        let v: String = expr["value"]
        return "EL_STR(" + c_str_lit(v) + ")"
    }

    if kind == "Bool" {
        let v: String = expr["value"]
        if v == "true" { return "1" }
        return "0"
    }

    if kind == "Nil" {
        return "EL_NULL"
    }

    if kind == "Ident" {
        let name: String = expr["name"]
        return name
    }

    if kind == "Not" {
        let inner = expr["inner"]
        let inner_c: String = cg_expr(inner)
        return "!" + inner_c
    }

    if kind == "Neg" {
        let inner = expr["inner"]
        let inner_kind: String = inner["expr"]
        // Float literal negation: emit el_from_float(-n) so the IEEE 754 sign
        // bit is set correctly. Arithmetic negation of the int64 bit pattern
        // (the el_val_t representation) produces garbage, not -f.
        if str_eq(inner_kind, "Float") {
            let fval: String = inner["value"]
            return "el_from_float(-" + fval + ")"
        }
        let inner_c: String = 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 = cg_expr(left)
        let right_c: String = cg_expr(right)
        let left_kind: String = left["expr"]
        let right_kind: String = right["expr"]

        // -- String/equality fast-path: skip O(N-) temporal traversals --------
        // The 10 temporal predicates below each recurse into the left subtree:
        // O(depth) state_get calls per predicate, O(N-) total for a chain of N
        // string-concat BinOps (e.g. the 70-100-part HTML chains in soul.el).
        // When either operand is a bare Str literal the result is always concat
        // or str_eq - no temporal dispatch is possible. Exit immediately.
        if str_eq(op, "Plus") {
            if str_eq(left_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" }
            if str_eq(right_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" }
        }
        if str_eq(op, "EqEq") {
            if str_eq(left_kind, "Str") { return "str_eq(" + left_c + ", " + right_c + ")" }
            if str_eq(right_kind, "Str") { return "str_eq(" + left_c + ", " + right_c + ")" }
        }
        if str_eq(op, "NotEq") {
            if str_eq(left_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" }
            if str_eq(right_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" }
        }

        // -- Temporal-type dispatch (Instant + Duration first-class) --------
        // Run BEFORE the int / string / generic paths so typed temporal
        // operands route through the runtime wrappers and invalid combos
        // become #error directives rather than silently falling through to
        // raw int arithmetic. The wrappers are no-op casts at the C level
        // but make the intent explicit and centralise future changes (e.g.
        // saturating arithmetic, overflow guards).
        let left_is_inst: Bool = is_instant_expr(left)
        let right_is_inst: Bool = is_instant_expr(right)
        let left_is_dur:  Bool = is_duration_expr(left)
        let right_is_dur: Bool = is_duration_expr(right)

        // Phase 1.5 LocalDate / LocalTime / CalendarTime dispatch. These
        // route through their typed runtime wrappers (el_local_date_add_dur,
        // el_local_time_add_dur, el_local_date_lt, el_local_date_eq) and
        // forbid mismatched ops at codegen time. Cross-calendar arithmetic
        // (CalendarTime + CalendarTime, CalendarTime - CalendarTime under
        // mismatched calendars) is structurally meaningless: a CalendarTime
        // already projects an Instant under a Calendar, so subtraction
        // between two of them only makes sense in instant-space (use
        // cal_to_instant first).
        let left_is_ld: Bool = is_localdate_expr(left)
        let right_is_ld: Bool = is_localdate_expr(right)
        let left_is_lt: Bool = is_localtime_expr(left)
        let right_is_lt: Bool = is_localtime_expr(right)
        let left_is_ct: Bool = is_caltime_expr(left)
        let right_is_ct: Bool = is_caltime_expr(right)
        if left_is_ld {
            if op == "Plus" {
                if right_is_dur {
                    return "el_local_date_add_dur(" + left_c + ", " + right_c + ")"
                }
            }
            if op == "Lt" {
                if right_is_ld { return "el_local_date_lt(" + left_c + ", " + right_c + ")" }
            }
            if op == "EqEq" {
                if right_is_ld { return "el_local_date_eq(" + left_c + ", " + right_c + ")" }
            }
        }
        if left_is_lt {
            if op == "Plus" {
                if right_is_dur {
                    return "el_local_time_add_dur(" + left_c + ", " + right_c + ")"
                }
            }
        }
        if left_is_ct {
            if op == "Plus" {
                if right_is_ct {
                    record_call(state_get("__cg_current_fn"), "temporal:caltime_plus_caltime")
                    return "0 /* TIME_TYPE_ERROR: CalendarTime + CalendarTime */"
                }
            }
        }

        let any_temporal: Bool = false
        if left_is_inst { let any_temporal = true }
        if right_is_inst { let any_temporal = true }
        if left_is_dur { let any_temporal = true }
        if right_is_dur { let any_temporal = true }
        if any_temporal {
            if op == "Plus" {
                if left_is_inst {
                    if right_is_dur {
                        return "el_instant_add_dur(" + left_c + ", " + right_c + ")"
                    }
                    if right_is_inst {
                        record_call(state_get("__cg_current_fn"), "temporal:instant_plus_instant")
                        return "0 /* TIME_TYPE_ERROR: Instant + Instant */"
                    }
                    // A bare literal is a MAGNITUDE WITH NO AXIS. Duration + Int
                    // was already refused because an Int carries no unit; adding
                    // one to a POINT is worse, because it silently moves the
                    // instant by an unspecified amount. The asymmetry had no
                    // justification -- it was simply never written.
                    if is_int_expr(right) {
                        record_call(state_get("__cg_current_fn"), "temporal:instant_plus_int")
                        return "0 /* TIME_TYPE_ERROR: Instant + Int */"
                    }
                }
                if left_is_dur {
                    if right_is_inst {
                        return "el_instant_add_dur(" + right_c + ", " + left_c + ")"
                    }
                    if right_is_dur {
                        return "el_duration_add(" + left_c + ", " + right_c + ")"
                    }
                    if is_int_expr(right) {
                        record_call(state_get("__cg_current_fn"), "temporal:duration_plus_int")
                        return "0 /* TIME_TYPE_ERROR: Duration + Int */"
                    }
                }
                if right_is_dur {
                    if is_int_expr(left) {
                        record_call(state_get("__cg_current_fn"), "temporal:duration_plus_int")
                        return "0 /* TIME_TYPE_ERROR: Int + Duration */"
                    }
                }
            }
            if op == "Minus" {
                if left_is_inst {
                    if right_is_dur {
                        return "el_instant_sub_dur(" + left_c + ", " + right_c + ")"
                    }
                    if right_is_inst {
                        return "el_instant_diff(" + left_c + ", " + right_c + ")"
                    }
                }
                if left_is_dur {
                    if right_is_dur {
                        return "el_duration_sub(" + left_c + ", " + right_c + ")"
                    }
                    if is_int_expr(right) {
                        record_call(state_get("__cg_current_fn"), "temporal:duration_minus_int")
                        return "0 /* TIME_TYPE_ERROR: Duration - Int */"
                    }
                }
            }
            if op == "Star" {
                if left_is_dur {
                    if is_int_expr(right) {
                        return "el_duration_scale(" + left_c + ", " + right_c + ")"
                    }
                }
                if right_is_dur {
                    if is_int_expr(left) {
                        return "el_duration_scale(" + right_c + ", " + left_c + ")"
                    }
                }
            }
            if op == "Slash" {
                if left_is_dur {
                    if is_int_expr(right) {
                        return "el_duration_div(" + left_c + ", " + right_c + ")"
                    }
                }
            }
            // Comparisons. Cross-type comparisons are forbidden.
            if op == "Lt" {
                if left_is_inst {
                    if right_is_inst { return "el_instant_lt(" + left_c + ", " + right_c + ")" }
                    if right_is_dur {
                        record_call(state_get("__cg_current_fn"), "temporal:instant_cmp_duration")
                        return "0 /* TIME_TYPE_ERROR: Instant < Duration */"
                    }
                }
                if left_is_dur {
                    if right_is_dur { return "el_duration_lt(" + left_c + ", " + right_c + ")" }
                    if right_is_inst {
                        record_call(state_get("__cg_current_fn"), "temporal:duration_cmp_instant")
                        return "0 /* TIME_TYPE_ERROR: Duration < Instant */"
                    }
                }
            }
            if op == "LtEq" {
                if left_is_inst {
                    if right_is_inst { return "el_instant_le(" + left_c + ", " + right_c + ")" }
                }
                if left_is_dur {
                    if right_is_dur { return "el_duration_le(" + left_c + ", " + right_c + ")" }
                }
            }
            if op == "Gt" {
                if left_is_inst {
                    if right_is_inst { return "el_instant_gt(" + left_c + ", " + right_c + ")" }
                }
                if left_is_dur {
                    if right_is_dur { return "el_duration_gt(" + left_c + ", " + right_c + ")" }
                }
            }
            if op == "GtEq" {
                if left_is_inst {
                    if right_is_inst { return "el_instant_ge(" + left_c + ", " + right_c + ")" }
                }
                if left_is_dur {
                    if right_is_dur { return "el_duration_ge(" + left_c + ", " + right_c + ")" }
                }
            }
            if op == "EqEq" {
                if left_is_inst {
                    if right_is_inst { return "el_instant_eq(" + left_c + ", " + right_c + ")" }
                }
                if left_is_dur {
                    if right_is_dur { return "el_duration_eq(" + left_c + ", " + right_c + ")" }
                }
            }
            if op == "NotEq" {
                if left_is_inst {
                    if right_is_inst { return "el_instant_ne(" + left_c + ", " + right_c + ")" }
                }
                if left_is_dur {
                    if right_is_dur { return "el_duration_ne(" + left_c + ", " + right_c + ")" }
                }
            }
            // Fall through - let the existing path handle anything we
            // didn't explicitly cover (typically string-concat with a
            // typed temporal value, e.g. for debug prints, which works
            // because both share the int64 slot).
        }

        // -- Float arithmetic / comparison dispatch -------------------------
        // When either operand is provably Float, promote BOTH operands to a C
        // double and emit real double arithmetic. Arithmetic results are
        // re-wrapped into the el_val_t float slot via el_from_float(); the
        // integer % becomes fmod(); comparisons yield a bare 0/1. Without this
        // path, float `+` fell to el_str_concat (segfault) and `- * / %` fell
        // to integer ops on the raw IEEE-754 bit pattern (garbage). Mixed
        // Int/Float promotes the Int side with a (double) cast. This runs
        // AFTER temporal dispatch (a Float is never temporal) and BEFORE the
        // Int/String paths, and is skipped when either side is a bare Str so
        // string concat/equality is never disturbed.
        if !str_eq(left_kind, "Str") {
            if !str_eq(right_kind, "Str") {
                let any_float: Bool = false
                if is_float_expr(left)  { let any_float = true }
                if is_float_expr(right) { let any_float = true }
                if any_float {
                    let l_d: String = float_operand_c(left, left_c)
                    let r_d: String = float_operand_c(right, right_c)
                    if str_eq(op, "Plus")    { return "el_from_float(" + l_d + " + " + r_d + ")" }
                    if str_eq(op, "Minus")   { return "el_from_float(" + l_d + " - " + r_d + ")" }
                    if str_eq(op, "Star")    { return "el_from_float(" + l_d + " * " + r_d + ")" }
                    if str_eq(op, "Slash")   { return "el_from_float(" + l_d + " / " + r_d + ")" }
                    if str_eq(op, "Percent") { return "el_from_float(fmod(" + l_d + ", " + r_d + "))" }
                    if str_eq(op, "Lt")    { return "(" + l_d + " < " + r_d + ")" }
                    if str_eq(op, "Gt")    { return "(" + l_d + " > " + r_d + ")" }
                    if str_eq(op, "LtEq")  { return "(" + l_d + " <= " + r_d + ")" }
                    if str_eq(op, "GtEq")  { return "(" + l_d + " >= " + r_d + ")" }
                    if str_eq(op, "EqEq")  { return "(" + l_d + " == " + r_d + ")" }
                    if str_eq(op, "NotEq") { return "(" + l_d + " != " + r_d + ")" }
                    // And/Or with a float operand is ill-typed — fall through.
                }
            }
        }

        if op == "Plus" {
            // If either side is a string literal, always concat
            if left_kind == "Str" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Str" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            // Type-driven dispatch via recursive is_int_expr: any expression
            // whose value is provably Int (literal, typed Ident, known-Int
            // builtin, or BinOp arithmetic over Ints) participates in
            // arithmetic, not string concat. Recursion into BinOp lets
            // `a + b + c` (chained Int adds) and `acc * 16 + d` route to
            // arithmetic instead of falling to el_str_concat - both sides
            // are Int so the outer `+` is too.
            if is_int_expr(left) {
                if is_int_expr(right) {
                    let op_c: String = binop_to_c(op)
                    return "(" + left_c + " " + op_c + " " + right_c + ")"
                }
            }
            // Mixed cases: at least one side is provably Int but the other
            // is not provably anything. Historical heuristic biases to
            // arithmetic when a literal Int is present (preserves prior
            // behaviour for `pos + 1` where `pos` is an untyped param).
            if left_kind == "Int" {
                let op_c: String = binop_to_c(op)
                return "(" + left_c + " " + op_c + " " + right_c + ")"
            }
            if right_kind == "Int" {
                let op_c: String = binop_to_c(op)
                return "(" + left_c + " " + op_c + " " + right_c + ")"
            }
            // Otherwise: BinOp(+) with a Call/Ident side without int-typed
            // evidence - fall back to string concat (the historical default).
            if left_kind == "Call" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Call" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            if left_kind == "BinOp" {
                let left_op: String = left["op"]
                if left_op == "Plus" {
                    return "el_str_concat(" + left_c + ", " + right_c + ")"
                }
            }
            if right_kind == "BinOp" {
                let right_op: String = right["op"]
                if right_op == "Plus" {
                    return "el_str_concat(" + left_c + ", " + right_c + ")"
                }
            }
            if left_kind == "Ident" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Ident" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
        }

        // String equality: use str_eq() when either side is a string literal or ident.
        // Use plain == when comparing integer literals OR when both sides are
        // identifiers tracked in __int_names (typed Int via `let x: Int = ...`).
        // Without the int-name check, `seen == idx` between two Int locals
        // miscompiles to str_eq(seen, idx), strcmp'ing what are integer values
        // dressed as char* - segfault on the first non-printable byte.
        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 == "Ident" {
                if right_kind == "Ident" {
                    let lname: String = left["name"]
                    let rname: String = right["name"]
                    if is_int_name(lname) {
                        if is_int_name(rname) {
                            return "(" + left_c + " == " + right_c + ")"
                        }
                    }
                }
            }
            // Extend int-equality to mixed Ident/BinOp cases: `i == n - 1`
            // where the left is an int-name Ident and the right is an
            // arithmetic BinOp (or vice-versa). Without this check the
            // fallthrough to str_eq produces str_eq(int_value, int_value)
            // which reads the integer as a char* and segfaults.
            // EITHER side provably Int is enough. Requiring BOTH meant a call
            // whose return type codegen cannot infer poisoned the operator:
            //     getint(5) == a      ->  str_eq(getint(5), a)
            // even with `a` declared Int. str_eq then reads an integer as a
            // char* and segfaults. Only an integer LITERAL on one side forced
            // the numeric form, so the bug was invisible in the common case.
            //
            // Loosening to OR is strictly safer: when one side is a known Int,
            // str_eq is always wrong (it dereferences that int), while numeric
            // comparison is at worst a wrong answer on an already ill-typed
            // program. When neither side is Int nothing changes, so string
            // comparison is untouched.
            if is_int_expr(left) {
                return "(" + left_c + " == " + right_c + ")"
            }
            if is_int_expr(right) {
                return "(" + left_c + " == " + right_c + ")"
            }
            // Float literal or negative float literal: use plain == (bit-equal
            // el_val_t comparison). This handles `r0 == 3.0`, `neg == -3.0`, etc.
            if is_float_expr(left) {
                return "(" + left_c + " == " + right_c + ")"
            }
            if is_float_expr(right) {
                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 + ")"
            }
            if left_kind == "Ident" {
                return "str_eq(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Ident" {
                return "str_eq(" + left_c + ", " + right_c + ")"
            }
            if left_kind == "Call" {
                return "str_eq(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Call" {
                return "str_eq(" + 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 == "Ident" {
                if right_kind == "Ident" {
                    let lname: String = left["name"]
                    let rname: String = right["name"]
                    if is_int_name(lname) {
                        if is_int_name(rname) {
                            return "(" + left_c + " != " + right_c + ")"
                        }
                    }
                }
            }
            // Same mixed Ident/BinOp fix as EqEq: use is_int_expr to detect
            // integer-typed operands before falling through to !str_eq.
            // Either side Int is enough — see the EqEq note above.
            if is_int_expr(left) {
                return "(" + left_c + " != " + right_c + ")"
            }
            if is_int_expr(right) {
                return "(" + left_c + " != " + right_c + ")"
            }
            // Float-typed operands use plain != (bit-equal comparison).
            if is_float_expr(left) {
                return "(" + left_c + " != " + right_c + ")"
            }
            if is_float_expr(right) {
                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 + ")"
            }
            if left_kind == "Ident" {
                return "!str_eq(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Ident" {
                return "!str_eq(" + left_c + ", " + right_c + ")"
            }
            if left_kind == "Call" {
                return "!str_eq(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Call" {
                return "!str_eq(" + left_c + ", " + right_c + ")"
            }
        }

        let op_c: String = binop_to_c(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 = 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, ", ")
        // args_parts list fully consumed — release to free peak heap.
        el_release(args_parts)

        if func_kind == "Ident" {
            let fn_name: String = func["name"]
            // Every call is recorded here, from cg_expr, because this runs for
            // EVERY expression in every context -- including main's body, which
            // cg_fn returns early on since C provides its own main. Hooking the
            // recording to cg_fn instead left every call in main invisible to
            // the relation graph, a blind spot exactly where a program does its
            // work. Caught by capability_query.sh, not by reading the code.
            record_call("program", "is_kind:" + state_get("__program_kind"))
            record_call(state_get("__cg_current_fn"), fn_name)
            // Record the ARITY at the call site. The compiler knows how many
            // arguments were written; whether that is correct is a question
            // about the runtime's surface, and the runtime already declares its
            // surface in el_runtime.h. A table inside the emitter is a second
            // copy of that header, maintained by hand.
            record_call(fn_name, "arity:" + native_int_to_str(native_list_len(args)))
            // Arity check against the builtin table - refuse, with a clear
            // El-source message, when a known builtin gets the wrong arg
            // count (e.g. `http_serve(port)` instead of `http_serve(port,
            // handler)`). User-defined fns and variadic builtins pass
            // through (builtin_arity returns -1).
            // sleep(Duration) - Phase 1 of the typed-time work. When the
            // single arg is provably a Duration we lower to el_sleep_duration
            // so the runtime sees nanos directly. Existing sleep() callers
            // that pass an Int still emit `sleep(<int>)`, which falls through
            // to the no-such-symbol path - those call sites must migrate to
            // a typed Duration. Acceptable: the spec marks them out for an
            // audit pass during Phase 1.
            if str_eq(fn_name, "sleep") {
                if arity == 1 {
                    let only_arg = native_list_get(args, 0)
                    if is_duration_expr(only_arg) {
                        return "el_sleep_duration(" + args_c + ")"
                    }
                }
            }
            // el_from_float takes a raw C double - do not wrap the float
            // argument in el_from_float() again. Without this, the float
            // literal codegen (which wraps every Float in el_from_float())
            // produces el_from_float(el_from_float(0.7)) - double-encoded.
            if str_eq(fn_name, "el_from_float") {
                if arity == 1 {
                    let only_arg = native_list_get(args, 0)
                    let arg_kind: String = only_arg["expr"]
                    if str_eq(arg_kind, "Float") {
                        let v: String = only_arg["value"]
                        return "el_from_float(" + v + ")"
                    }
                }
            }
            return fn_name + "(" + args_c + ")"
        }

        if func_kind == "Field" {
            let obj = func["object"]
            let field: String = func["field"]
            let obj_c: String = cg_expr(obj)
            if arity > 0 {
                return field + "(" + obj_c + ", " + args_c + ")"
            }
            return field + "(" + obj_c + ")"
        }

        let fn_c: String = cg_expr(func)
        return fn_c + "(" + args_c + ")"
    }

    if kind == "Field" {
        let obj = expr["object"]
        let field: String = expr["field"]
        let obj_c: String = cg_expr(obj)
        // el_get_field takes el_val_t for both args, so the field name
        // string literal must be wrapped in EL_STR(). Without the wrap
        // the C compiler treats the bare const char* as an int64 (warns
        // -Wint-conversion) and the runtime reads gibberish at the address
        // when looking up the key.
        return "el_get_field(" + obj_c + ", EL_STR(" + c_str_lit(field) + "))"
    }

    if kind == "Index" {
        // El programs use `t["field"]` for map access and `arr[i]` for
        // list access. The parser emits the same Index node for both.
        // Dispatch at codegen time on the index expression kind: string-
        // literal index -> map field access (`el_get_field`); anything
        // else -> list element access (`el_list_get`).
        let obj = expr["object"]
        let idx = expr["index"]
        let obj_c: String = cg_expr(obj)
        let idx_c: String = 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)
        // Empty literal: el_list_new(0, ) generates malformed C (trailing
        // comma in a varargs call). Emit el_list_empty() directly.
        if n == 0 { return "el_list_empty()" }
        let items_parts: [String] = native_list_empty()
        let i = 0
        while i < n {
            let elem = native_list_get(elems, i)
            let elem_c: String = cg_expr(elem)
            let items_parts = native_list_append(items_parts, elem_c)
            let i = i + 1
        }
        let items_joined: String = str_join(items_parts, ", ")
        // items_parts fully consumed — release to free peak heap.
        el_release(items_parts)
        return "el_list_new(" + native_int_to_str(n) + ", " + items_joined + ")"
    }

    if kind == "Map" {
        let pairs = expr["pairs"]
        let n: Int = native_list_len(pairs)
        // Empty literal: `el_map_new(0, )` is malformed C (trailing comma in
        // a varargs call). Emit `el_map_new(0)` directly so empty-map
        // shadowing inside for/while/if bodies - `let acc: Map = {}` -
        // doesn't fail downstream cc with parse errors.
        if n == 0 { return "el_map_new(0)" }
        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 = cg_expr(val)
            let items_parts = native_list_append(items_parts, c_str_lit(key) + ", " + val_c)
            let i = i + 1
        }
        let items_joined: String = str_join(items_parts, ", ")
        // items_parts fully consumed — release to free peak heap.
        el_release(items_parts)
        return "el_map_new(" + native_int_to_str(n) + ", " + items_joined + ")"
    }

    if kind == "Try" {
        let inner = expr["inner"]
        return cg_expr(inner)
    }

    if kind == "If" {
        return cg_if_expr(expr)
    }

    if kind == "Match" {
        return cg_match(expr)
    }

    if kind == "HtmlTemplate" {
        return cg_html_template(expr)
    }

    if kind == "Lambda" {
        // Lambda expressions are JS-target only. In the C target, emit EL_NULL.
        "EL_NULL"
    } else {
        "EL_NULL"
    }
}

// -- Match codegen -------------------------------------------------------------
//
// Lower a match expression to a GCC/Clang statement-expression.
// A unique label suffix is allocated per match via state_set("__match_counter").

fn next_match_id() -> String {
    let csv: String = state_get("__match_counter")
    let n = 0
    if !str_eq(csv, "") {
        let n = str_to_int(csv)
    }
    let n = n + 1
    state_set("__match_counter", native_int_to_str(n))
    native_int_to_str(n)
}

fn cg_match(expr: Map<String, Any>) -> String {
    let subject = expr["subject"]
    let arms = expr["arms"]
    let subj_c: String = cg_expr(subject)
    let id: String = next_match_id()
    let subj_var: String = "_match_subj_" + id
    let result_var: String = "_match_result_" + id
    let done_label: String = "_match_done_" + id
    // Accumulate arm fragments into a list to avoid O(n-) string growth.
    let parts: [String] = native_list_empty()
    let parts = native_list_append(parts, "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; ")
    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 = cg_expr(body)
        if str_eq(pkind, "Wildcard") {
            let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
        } else {
            if str_eq(pkind, "Binding") {
                let bname: String = pat["name"]
                let parts = native_list_append(parts, "{ el_val_t " + bname + " = " + subj_var + "; " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
            } else {
                if str_eq(pkind, "LitInt") {
                    let v: String = pat["value"]
                    let parts = native_list_append(parts, "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
                } else {
                    if str_eq(pkind, "LitStr") {
                        let v: String = pat["value"]
                        let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
                    } else {
                        if str_eq(pkind, "LitBool") {
                            let v: String = pat["value"]
                            let bv = "0"
                            if str_eq(v, "true") {
                                let bv = "1"
                            }
                            let parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
                        } else {
                            if str_eq(pkind, "Variant") {
                                // Enum::Variant pattern — match against the variant name
                                // string (El enums compile to plain strings).
                                let variant: String = pat["variant"]
                                let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(variant) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
                            } else {
                                // unknown pattern -> wildcard
                                let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
                            }
                        }
                    }
                }
            }
        }
        let i = i + 1
    }
    let parts = native_list_append(parts, done_label + ":; " + result_var + "; })")
    let result: String = str_join(parts, "")
    // parts list fully consumed — release to free peak heap.
    el_release(parts)
    result
}

// Lower a match statement (used for side effects, not as an expression) to a
// chain of C if/else if/else blocks. The subject is evaluated once into a
// scoped temporary; each arm generates a condition and a braced body; the
// wildcard/binding arm becomes the final `else` branch.
//
// Pattern dispatch:
//   LitStr  -> str_eq(subj, EL_STR("..."))
//   LitInt  -> subj == N
//   LitBool -> subj == 1 / subj == 0
//   Binding -> else { el_val_t name = subj; <body> }
//   Wildcard -> else { <body> }
fn cg_match_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
    let subject = expr["subject"]
    let arms = expr["arms"]
    let subj_c: String = cg_expr(subject)
    let id: String = next_match_id()
    let subj_var: String = "_match_subj_" + id
    let inner: String = indent + "  "
    emit_line(indent + "{")
    emit_line(inner + "el_val_t " + subj_var + " = " + subj_c + ";")
    let n: Int = native_list_len(arms)
    let i = 0
    let first_cond: Bool = true
    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 = cg_expr(body)
        if str_eq(pkind, "LitStr") {
            let v: String = pat["value"]
            let cond_str = "str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))"
            if first_cond {
                emit_line(inner + "if (" + cond_str + ") {")
                let first_cond = false
            } else {
                emit_line(inner + "} else if (" + cond_str + ") {")
            }
            emit_line(inner + "  " + body_c + ";")
        } else {
            if str_eq(pkind, "LitInt") {
                let v: String = pat["value"]
                let cond_str = subj_var + " == " + v
                if first_cond {
                    emit_line(inner + "if (" + cond_str + ") {")
                    let first_cond = false
                } else {
                    emit_line(inner + "} else if (" + cond_str + ") {")
                }
                emit_line(inner + "  " + body_c + ";")
            } else {
                if str_eq(pkind, "LitBool") {
                    let v: String = pat["value"]
                    let bv = "0"
                    if str_eq(v, "true") {
                        let bv = "1"
                    }
                    let cond_str = subj_var + " == " + bv
                    if first_cond {
                        emit_line(inner + "if (" + cond_str + ") {")
                        let first_cond = false
                    } else {
                        emit_line(inner + "} else if (" + cond_str + ") {")
                    }
                    emit_line(inner + "  " + body_c + ";")
                } else {
                    // Wildcard or Binding - becomes the else branch
                    if first_cond {
                        emit_line(inner + "{")
                    } else {
                        emit_line(inner + "} else {")
                    }
                    if str_eq(pkind, "Binding") {
                        let bname: String = pat["name"]
                        emit_line(inner + "  el_val_t " + bname + " = " + subj_var + ";")
                    }
                    emit_line(inner + "  " + body_c + ";")
                    emit_line(inner + "}")
                    let first_cond = true
                }
            }
        }
        let i = i + 1
    }
    // Close any open if/else-if chain (only reached when last arm was a literal pattern)
    if !first_cond {
        emit_line(inner + "}")
    }
    emit_line(indent + "}")
}

// -- If-as-expression codegen -------------------------------------------------
//
// Lower `if cond { thenBody } else { elseBody }` used in expression position
// (e.g. `let x = if a { b } else { c }`) to a GCC/Clang statement-expression
// so the actual arm bodies are evaluated, not just `(cond ? 1 : 0)`.
//
// Each arm body is a list of statements; the result of the arm is the value
// of its final Expr statement (mirroring transform_implicit_return at function
// scope). Statements before the final Expr are emitted as expression-statements
// for their side effects.

fn next_if_id() -> String {
    let csv: String = state_get("__if_expr_counter")
    let n = 0
    if !str_eq(csv, "") {
        let n = str_to_int(csv)
    }
    let n = n + 1
    state_set("__if_expr_counter", native_int_to_str(n))
    native_int_to_str(n)
}

// is_void_builtin — true for runtime builtins declared `void` in el_runtime.h.
// User `-> Void` functions are emitted as el_val_t (return 0) so they are safe
// to assign; only these C-level void builtins are not.
fn is_void_builtin(name: String) -> Bool {
    if str_eq(name, "println") { return true }
    if str_eq(name, "print") { return true }
    if str_eq(name, "engram_strengthen") { return true }
    if str_eq(name, "engram_forget") { return true }
    if str_eq(name, "engram_connect") { return true }
    if str_eq(name, "dharma_emit") { return true }
    if str_eq(name, "dharma_strengthen") { return true }
    if str_eq(name, "llm_register_tool") { return true }
    if str_eq(name, "exit_program") { return true }
    if str_eq(name, "http_serve") { return true }
    if str_eq(name, "http_set_handler") { return true }
    if str_eq(name, "http_serve_async") { return true }
    if str_eq(name, "el_cgi_init") { return true }
    if str_eq(name, "el_retain") { return true }
    if str_eq(name, "el_release") { return true }
    false
}

// cg_expr_is_void — true if `val` is a direct call to a void builtin, so the
// if-expression arm must emit it as a bare statement rather than assigning its
// (nonexistent) value to the result var.
fn cg_expr_is_void(val: Map<String, Any>) -> Bool {
    let vk: String = val["expr"]
    if str_eq(vk, "Call") {
        let f = val["func"]
        let fk: String = f["expr"]
        if str_eq(fk, "Ident") {
            return is_void_builtin(f["name"])
        }
    }
    false
}

// Render a single arm of the if-as-expression: emit each statement-before-last
// as a side-effecting expression, then assign the final Expr's value to the
// result var. If the arm body is empty or its last stmt isn't an Expr, the
// result var stays at its initial 0.
fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
    let n: Int = native_list_len(stmts)
    // Collect statement fragments into a list to avoid O(n-) string growth.
    let parts: [String] = native_list_empty()
    // Track names already declared in this arm's C block. El permits `let x`
    // to redeclare/rebind x in the same scope, but C forbids redeclaring the
    // same name in one block: emit `el_val_t x = ...` first, `x = ...` after.
    let declared: [String] = native_list_empty()
    let i = 0
    while i < n {
        let s = native_list_get(stmts, i)
        let sk: String = s["stmt"]
        let is_last: Bool = false
        if i == n - 1 { let is_last = true }
        if str_eq(sk, "Let") {
            let name: String = s["name"]
            let val = s["value"]
            let val_c: String = cg_expr(val)
            if list_contains(declared, name) {
                let parts = native_list_append(parts, name + " = " + val_c + "; ")
            } else {
                let declared = native_list_append(declared, name)
                let parts = native_list_append(parts, "el_val_t " + name + " = " + val_c + "; ")
            }
        } else {
            if str_eq(sk, "Return") {
                let val = s["value"]
                let val_c: String = cg_expr(val)
                if cg_expr_is_void(val) {
                    let parts = native_list_append(parts, val_c + "; ")
                } else {
                    let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
                }
            } else {
                if str_eq(sk, "Expr") {
                    let val = s["value"]
                    let val_c: String = cg_expr(val)
                    if is_last {
                        if cg_expr_is_void(val) {
                            let parts = native_list_append(parts, val_c + "; ")
                        } else {
                            let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
                        }
                    } else {
                        let parts = native_list_append(parts, "(void)(" + val_c + "); ")
                    }
                } else {
                    if str_eq(sk, "Assign") {
                        // Real reassignment in an expression-position arm -
                        // emit the store; the arm's "value" stays whatever
                        // result_var was last set to, which is the El
                        // semantics (assignment is a statement, not a value).
                        let aname: String = s["name"]
                        let aval = s["value"]
                        let aval_c: String = cg_expr(aval)
                        let parts = native_list_append(parts, aname + " = " + aval_c + "; ")
                    } else {
                        // Non-trivial stmt kinds (While/For) shouldn't appear in
                        // expression-position arm bodies; emit nothing rather
                        // than malformed C.
                    }
                }
            }
        }
        let i = i + 1
    }
    let result: String = str_join(parts, "")
    // parts list fully consumed — release to free peak heap.
    el_release(parts)
    result
}

fn cg_if_expr(expr: Map<String, Any>) -> String {
    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 = cg_expr(cond)
    let id: String = next_if_id()
    let result_var: String = "_if_result_" + id
    let then_c: String = cg_if_expr_arm(then_stmts, result_var)
    let else_c: String = ""
    if has_else {
        let else_c = cg_if_expr_arm(else_stmts, result_var)
    }
    let out: String = "({ el_val_t " + result_var + " = 0; if (" + cond_c + ") { " + then_c + "} else { " + else_c + "} " + result_var + "; })"
    out
}

// -- Variable scope tracking ---------------------------------------------------
//
// El allows `let x = expr` to both declare and reassign x in the same scope.
// C doesn't allow redeclaring the same name in the same block.
// We track declared names in a list and emit `x = expr` (no type prefix)
// when x is already declared. The declared list is passed through all
// statement emitters.

fn 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 ---------------------------------------------------------
//
// cg_stmt emits C lines via println. declared is a list of already-declared
// variable names in the current C scope; returns updated declared list.

fn 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 = cg_expr(val)
        // If the binding is annotated `: Int` and val is an Int literal,
        // register `name` in the per-function int-name set so that later
        // `name + ...` dispatches to arithmetic, not concat.
        let ltype: String = stmt["type"]
        if str_eq(ltype, "Int") {
            add_int_name(name)
        }
        // ANNOTATION CHECKING. El has type annotations and, until now, no type
        // checking: the annotation fed dispatch and was never itself verified.
        // That is the root cause of every silent miscompilation found on
        // 2026-08-17, and the failure is not merely a wrong answer:
        //
        //   let x: Int = "hello"   ; x + 1  -> printed 4343631981, a string
        //                                     POINTER interpreted as an integer
        //   let s: String = 42     ; print  -> dereferenced address 42
        //
        // The second is an arbitrary-read primitive if the integer is ever
        // attacker-influenced. Literals are checked here because they are
        // unambiguous; the emitter only RECORDS the mismatch, the query decides.
        let vkind: String = val["expr"]
        if str_eq(ltype, "Int") {
            if str_eq(vkind, "Str") {
                record_call(state_get("__cg_current_fn"), "typemismatch:Int:String:" + name)
            }
        }
        if str_eq(ltype, "String") {
            if str_eq(vkind, "Int") {
                record_call(state_get("__cg_current_fn"), "typemismatch:String:Int:" + name)
            }
            if str_eq(vkind, "Float") {
                record_call(state_get("__cg_current_fn"), "typemismatch:String:Float:" + name)
            }
        }
        if str_eq(ltype, "Float") {
            if str_eq(vkind, "Str") {
                record_call(state_get("__cg_current_fn"), "typemismatch:Float:String:" + name)
            }
        }
        // UNANNOTATED let: take the type from what the initialiser RETURNS.
        // Without this, `let a = str_len(s)` loses the Int and a later `a + b`
        // lowers to el_str_concat on two integers -- silently, with no error,
        // producing a program that prints nothing where it should print 7.
        // Verified present in the pre-change compiler too, so this is a repair
        // rather than a regression. The return types were already needed for
        // dispatch; they were just never consulted here.
        if str_eq(ltype, "") {
            if is_int_expr(val) { add_int_name(name) }
        }
        // Same as params: Bool is an int in the value model. Without this a
        // `let ok: Bool = ...` compared to another Bool lowered to str_eq.
        if str_eq(ltype, "Bool") {
            add_int_name(name)
        }
        if str_eq(ltype, "Float") {
            add_float_name(name)
        }
        // Temporal type annotations register the name with the matching
        // typed-set so BinOp / comparison codegen routes through the
        // typed wrappers and forbids cross-type ops.
        if str_eq(ltype, "Instant") {
            add_instant_name(name)
        }
        if str_eq(ltype, "Duration") {
            add_duration_name(name)
        }
        if str_eq(ltype, "Calendar") {
            add_calendar_name(name)
        }
        if str_eq(ltype, "CalendarTime") {
            add_caltime_name(name)
        }
        if str_eq(ltype, "Rhythm") {
            add_rhythm_name(name)
        }
        if str_eq(ltype, "LocalDate") {
            add_localdate_name(name)
        }
        if str_eq(ltype, "LocalTime") {
            add_localtime_name(name)
        }
        if str_eq(ltype, "LocalDateTime") {
            add_localdt_name(name)
        }
        if str_eq(ltype, "Zone") {
            add_zone_name(name)
        }
        // Inference from RHS - duration literals and known-typed calls
        // propagate even when the let is unannotated.
        if is_instant_expr(val) {
            add_instant_name(name)
        }
        if is_duration_expr(val) {
            add_duration_name(name)
        }
        if is_calendar_expr(val) {
            add_calendar_name(name)
        }
        if is_caltime_expr(val) {
            add_caltime_name(name)
        }
        if is_rhythm_expr(val) {
            add_rhythm_name(name)
        }
        if is_localdate_expr(val) {
            add_localdate_name(name)
        }
        if is_localtime_expr(val) {
            add_localtime_name(name)
        }
        if is_localdt_expr(val) {
            add_localdt_name(name)
        }
        if is_zone_expr(val) {
            add_zone_name(name)
        }
        // Float inference: an unannotated `let` whose RHS is provably Float
        // (literal, math builtin, or float arithmetic) registers the name so
        // later `name <op> ...` routes through the double path.
        if is_float_expr(val) {
            add_float_name(name)
        }
        let vk: String = val["expr"]
        if str_eq(vk, "Int") {
            add_int_name(name)
        }
        if list_contains(declared, name) {
            emit_line(indent + name + " = " + val_c + ";")
            return declared
        } else {
            emit_line(indent + "el_val_t " + 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" {
            emit_line(indent + "return 0;")
        } else {
            let val_c: String = cg_expr(val)
            emit_line(indent + "return " + val_c + ";")
        }
        return declared
    }

    if kind == "Break" {
        emit_line(indent + "break;")
        return declared
    }

    if kind == "Continue" {
        emit_line(indent + "continue;")
        return declared
    }

    // Bare reassignment: `name = expr`. Always emits a plain C assignment
    // (no `el_val_t` prefix) - by construction the parser only produces
    // Assign for an existing identifier. If the name happens NOT to be in
    // `declared` for the current C scope (it was let-bound by an enclosing
    // block) the emit still resolves at C level because the variable lives
    // in the surrounding scope.
    if kind == "Assign" {
        let name: String = stmt["name"]
        let val = stmt["value"]
        let val_c: String = cg_expr(val)
        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" {
            cg_if_stmt(val, indent, declared)
            return declared
        }
        if val_kind == "For" {
            cg_for_stmt(val, indent, declared)
            return declared
        }
        if val_kind == "Match" {
            cg_match_stmt(val, indent, declared)
            return declared
        }
        let val_c: String = cg_expr(val)
        emit_line(indent + val_c + ";")
        return declared
    }

    if kind == "While" {
        let cond = stmt["cond"]
        let body = stmt["body"]
        let cond_c: String = cg_expr(cond)
        let cond_c = strip_outer_parens(cond_c)
        emit_line(indent + "while (" + cond_c + ") {")
        // Body lives in its own C block - clone so let-bindings inside the
        // loop don't leak into the parent's `declared` list (which would make
        // a sibling scope's `let x` emit assignment on an undeclared name).
        cg_stmts(body, indent + "  ", native_list_clone(declared))
        emit_line(indent + "}")
        return declared
    }

    if kind == "For" {
        let item: String = stmt["item"]
        let list_expr = stmt["list"]
        let body = stmt["body"]
        cg_for_body(item, list_expr, body, indent, declared)
        return declared
    }

    if kind == "ForRange" {
        let var_name: String = stmt["var"]
        let start_expr = stmt["start"]
        let end_expr = stmt["end"]
        let inclusive: Bool = stmt["inclusive"]
        let body = stmt["body"]
        let start_c: String = cg_expr(start_expr)
        let end_c: String = cg_expr(end_expr)
        // Loop variable introduced as a C local scoped to the for statement.
        // Body gets its own declared clone so let-bindings don't leak out.
        let body_decl = native_list_clone(declared)
        let body_decl = native_list_append(body_decl, var_name)
        if inclusive {
            emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " <= " + end_c + "; " + var_name + "++) {")
        } else {
            emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " < " + end_c + "; " + var_name + "++) {")
        }
        cg_stmts(body, indent + "  ", body_decl)
        emit_line(indent + "}")
        return declared
    }

    if kind == "FnDef" { return declared }
    if kind == "TypeDef" { return declared }
    if kind == "EnumDef" { return declared }
    if kind == "Import"  { return declared }
    if kind == "ExternFn" { return declared }
    if kind == "CgiBlock" { return declared }
    // TryCatch: browser-only control flow. In the C target, emit a comment
    // noting that the try body runs unconditionally; error handling is a no-op.
    // Programs that rely on catching JS exceptions should compile with --target=js.
    if kind == "TryCatch" {
        let try_body = stmt["try_body"]
        emit_line(indent + "/* try (C target: exception handling not supported) */")
        cg_stmts(try_body, indent, native_list_clone(declared))
        return declared
    }

    // assert <cond> , <msg> — test harness assertion
    if kind == "Assert" {
        let cond_node = stmt["cond"]
        let msg_node = stmt["msg"]
        let c_cond: String = cg_expr(cond_node)
        let c_msg: String = ""
        let msg_kind: String = msg_node["expr"]
        if str_eq(msg_kind, "Str") {
            let raw_msg: String = msg_node["value"]
            let c_msg = "\"" + c_escape(raw_msg) + "\""
        } else {
            let c_msg = "EL_STR_PTR(" + cg_expr(msg_node) + ")"
        }
        // Assertions record into PER-TEST state, not global counters. The test
        // is the unit of result; a global pass/fail tally cannot say which test
        // failed or whether a test ran at all. Reporting is the runner's job —
        // nothing is printed here.
        emit_line(indent + "if (!(" + c_cond + ")) {")
        emit_line(indent + "  __el_test_fail(" + c_msg + ");")
        emit_line(indent + "} else { __el_cur_asserts++; }")
        return declared
    }

    declared
}

// Strip a single layer of surrounding parentheses from a C expression string.
fn 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 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 = cg_expr(cond)
    let cond_c = strip_outer_parens(cond_c)
    emit_line(indent + "if (" + cond_c + ") {")
    // Each branch gets its own clone of `declared` - variables let-bound
    // inside the then/else block live only in that C scope, and must not
    // leak back to the parent (or to the sibling branch) through shared
    // list mutation. Cheap shallow copy; the entries (variable name strings)
    // are shared.
    cg_stmts(then_stmts, indent + "  ", native_list_clone(declared))
    if has_else {
        emit_line(indent + "} else {")
        cg_stmts(else_stmts, indent + "  ", native_list_clone(declared))
    }
    emit_line(indent + "}")
}

fn cg_for_body(item: String, list_expr: Map<String, Any>, body: [Map<String, Any>], indent: String, declared: [String]) -> Void {
    let list_c: String = cg_expr(list_expr)
    let idx = "_el_i"
    let list_tmp = "_el_lst"
    let len_tmp = "_el_len"
    emit_line(indent + "{")
    emit_line(indent + "  el_val_t " + list_tmp + " = " + list_c + ";")
    emit_line(indent + "  el_val_t " + len_tmp + " = el_list_len(" + list_tmp + ");")
    emit_line(indent + "  for (el_val_t " + idx + " = 0; " + idx + " < " + len_tmp + "; " + idx + "++) {")
    emit_line(indent + "    el_val_t " + item + " = el_list_get(" + list_tmp + ", " + idx + ");")
    // Body lives inside its own C block; the loop variable and any locally
    // let-bound names go out of scope at the closing brace, so we mustn't
    // pollute the parent's `declared` with them.
    let body_decl = native_list_clone(declared)
    let body_decl = native_list_append(body_decl, item)
    cg_stmts(body, indent + "    ", body_decl)
    emit_line(indent + "  }")
    emit_line(indent + "}")
}

fn 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"]
    cg_for_body(item, list_expr, body, indent, declared)
}

fn 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)
        // Per-statement arena scope: free intermediate strings (str_concat
        // fragments, cg_expr results) after each statement is emitted.
        let s_mark: Any = el_arena_push()
        let decl = cg_stmt(stmt, indent, decl)
        el_arena_pop(s_mark)
        let i = i + 1
    }
    decl
}

// -- Function declaration codegen -----------------------------------------------

fn param_decl(param: Map<String, Any>, idx: Int) -> String {
    let name: String = param["name"]
    "el_val_t " + name
}

fn params_to_c(params: [Map<String, Any>]) -> String {
    let n: Int = native_list_len(params)
    if n == 0 { return "void" }
    let parts: [String] = native_list_empty()
    let i = 0
    while i < n {
        let param = native_list_get(params, i)
        let decl: String = param_decl(param, i)
        let parts = native_list_append(parts, decl)
        let i = i + 1
    }
    let result: String = str_join(parts, ", ")
    // parts list fully consumed — release to free peak heap.
    el_release(parts)
    result
}

// Transform a function body so that an implicit-return final expression
// becomes an explicit Return. El allows the last expression in a function
// body to be the return value (e.g. `fn lex(s) { ... tokens }` returns
// `tokens`). Without this transform, the codegen emits the bare expression
// and falls through to the trailing `return 0;`, losing the value.
//
// Rules: a body ending in a bare Expr whose inner expr is NOT a control-
// flow construct (If/For) is rewritten so that final Expr becomes a
// Return statement carrying the same value. Bodies whose final statement
// is already a Return, While, For, or a non-value-producing form pass
// through unchanged.
fn 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"]
        // Skip control-flow expressions used as statements
        if val_kind == "If" { return body }
        if val_kind == "For" { return body }
        // Replace the last bare Expr with a Return carrying the same value
        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
}

// Test whether `name` is currently registered as an Int-typed identifier
// for the function being codegened. The set is maintained as a comma-
// bounded CSV in process state; cg_fn seeds it from typed parameters,
// cg_stmt extends it from typed `let` bindings.
fn is_int_name(name: String) -> Bool {
    let csv: String = state_get("__int_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

// Float-typed binding set — parallel to is_int_name. Populated from `let x:
// Float = ...` annotations, `: Float` parameters, and inference from a Float
// RHS. Consulted by is_float_expr to route arithmetic through the double path.
fn is_float_name(name: String) -> Bool {
    let csv: String = state_get("__float_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

// Same shape as is_int_name, for Instant- and Duration-typed bindings.
// Used by the BinOp/comparison codegen to dispatch arithmetic through the
// typed runtime wrappers (el_instant_add_dur, el_duration_lt, -) and to
// surface mismatches (Instant + Instant, Duration + Int) as #error
// directives at the top of the generated C.
fn is_instant_name(name: String) -> Bool {
    let csv: String = state_get("__instant_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

fn is_duration_name(name: String) -> Bool {
    let csv: String = state_get("__duration_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

// Known runtime builtins that return Int. Used to dispatch arithmetic vs
// string-concat on `+` when one side is a Call. New builtins must be added
// here when they return Int and may participate in arithmetic.
fn is_int_call(call_expr: Map<String, Any>) -> Bool {
    call_returns(call_expr, "Int")
}


// Known runtime builtins that return Float. Parallel to is_int_call — lets a
// Call participate in float arithmetic (and get inferred into __float_names on
// an unannotated `let`). New Float-returning builtins must be added here.
fn is_float_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, "int_to_float") { return true }
    if str_eq(name, "str_to_float") { return true }
    if str_eq(name, "json_get_float") { return true }
    if str_eq(name, "decimal_round") { return true }
    if str_eq(name, "math_pi") { return true }
    if str_eq(name, "math_sin") { return true }
    if str_eq(name, "math_cos") { return true }
    if str_eq(name, "math_sqrt") { return true }
    if str_eq(name, "math_log") { return true }
    if str_eq(name, "math_ln") { return true }
    if str_eq(name, "el_from_float") { return true }
    return false
}

// Builtins that return an Instant. Used by is_instant_expr and the BinOp
// dispatch - `now() + 5.seconds` types as Instant only because we can see
// that now() is an Instant-returning Call.
// load_signatures — El-level return types, read once from
// tools/check/signatures.rel. Previously 19 hardcoded names across
// is_instant_call and is_duration_call. The header cannot carry these: it
// declares everything as el_val_t, because El has one type.
fn load_signatures() -> Void {
    if !str_eq(state_get("__sig_loaded"), "") { return }
    state_set("__sig_loaded", "1")
    let path: String = env("EL_SIGNATURES")
    if str_eq(path, "") { let path = "tools/check/signatures.rel" }
    if !fs_exists(path) { return }
    let lines = str_split_lines(fs_read(path))
    let n: Int = native_list_len(lines)
    let i = 0
    while i < n {
        let line: String = str_trim(native_list_get(lines, i))
        if !str_eq(line, "") {
            if !str_starts_with(line, "#") {
                let parts = str_split(line, " ")
                let np: Int = native_list_len(parts)
                let name: String = ""
                let ty: String = ""
                let seen = 0
                let j = 0
                while j < np {
                    let tok: String = str_trim(native_list_get(parts, j))
                    if !str_eq(tok, "") {
                        if seen == 0 { let name = tok }
                        if seen == 2 { let ty = tok }
                        let seen = seen + 1
                    }
                    let j = j + 1
                }
                if seen >= 3 { state_set("__sig_" + name, ty) }
            }
        }
        let i = i + 1
    }
}

fn call_returns(expr: Map<String, Any>, want: String) -> Bool {
    load_signatures()
    let func = expr["func"]
    let fk: String = func["expr"]
    if !str_eq(fk, "Ident") { return false }
    let name: String = func["name"]
    return str_eq(state_get("__sig_" + name), want)
}

fn is_instant_call(expr: Map<String, Any>) -> Bool {
    call_returns(expr, "Instant")
}


// Builtins that return a Duration. Same role as is_instant_call.
fn is_duration_call(expr: Map<String, Any>) -> Bool {
    call_returns(expr, "Duration")
}


// Phase 1.5 - Calendar / CalendarTime / Rhythm / LocalDate / LocalTime /
// LocalDateTime / Zone are first-class boxed types. Each has its own name
// set in process state, populated from typed `let` bindings and parameter
// annotations. The BinOp dispatcher consults these to forbid mismatched
// arithmetic (e.g. CalendarTime + CalendarTime, LocalDate < CalendarTime).
fn is_calendar_name(name: String) -> Bool {
    let csv: String = state_get("__calendar_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

fn is_caltime_name(name: String) -> Bool {
    let csv: String = state_get("__caltime_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

fn is_rhythm_name(name: String) -> Bool {
    let csv: String = state_get("__rhythm_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

fn is_localdate_name(name: String) -> Bool {
    let csv: String = state_get("__localdate_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

fn is_localtime_name(name: String) -> Bool {
    let csv: String = state_get("__localtime_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

fn is_localdt_name(name: String) -> Bool {
    let csv: String = state_get("__localdt_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

fn is_zone_name(name: String) -> Bool {
    let csv: String = state_get("__zone_names")
    if str_eq(csv, "") { return false }
    return str_contains(csv, "," + name + ",")
}

// Calendar-returning builtins. earth_calendar / mars_calendar / cycle_calendar
// / no_cycle_calendar / relative_calendar all box a calendar struct.
fn is_calendar_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, "earth_calendar") { return true }
    if str_eq(name, "earth_calendar_default") { return true }
    if str_eq(name, "mars_calendar") { return true }
    if str_eq(name, "cycle_calendar") { return true }
    if str_eq(name, "no_cycle_calendar") { return true }
    if str_eq(name, "relative_calendar") { return true }
    return false
}

// CalendarTime-returning builtins.
fn is_caltime_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, "now_in") { return true }
    if str_eq(name, "in_calendar") { return true }
    if str_eq(name, "cal_in") { return true }
    if str_eq(name, "zoned") { return true }
    return false
}

// Rhythm-returning builtins.
fn is_rhythm_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, "rhythm_cycle_start") { return true }
    if str_eq(name, "rhythm_cycle_phase") { return true }
    if str_eq(name, "rhythm_duration") { return true }
    if str_eq(name, "rhythm_session_start") { return true }
    if str_eq(name, "rhythm_event") { return true }
    if str_eq(name, "rhythm_and") { return true }
    if str_eq(name, "rhythm_or") { return true }
    if str_eq(name, "rhythm_weekday") { return true }
    if str_eq(name, "rhythm_weekly_at") { return true }
    return false
}

// LocalDate-returning builtins.
fn is_localdate_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, "local_date") { return true }
    if str_eq(name, "el_local_date_add_dur") { return true }
    return false
}

fn is_localtime_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, "local_time") { return true }
    if str_eq(name, "el_local_time_add_dur") { return true }
    return false
}

fn is_localdt_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, "local_datetime") { return true }
    return false
}

fn is_zone_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, "zone") { return true }
    if str_eq(name, "zone_utc") { return true }
    if str_eq(name, "zone_local") { return true }
    if str_eq(name, "zone_offset") { return true }
    return false
}

fn is_calendar_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Ident") { return is_calendar_name(expr["name"]) }
    if str_eq(k, "Call") { return is_calendar_call(expr) }
    return false
}

fn is_caltime_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Ident") { return is_caltime_name(expr["name"]) }
    if str_eq(k, "Call") { return is_caltime_call(expr) }
    return false
}

fn is_rhythm_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Ident") { return is_rhythm_name(expr["name"]) }
    if str_eq(k, "Call") { return is_rhythm_call(expr) }
    return false
}

fn is_localdate_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Ident") { return is_localdate_name(expr["name"]) }
    if str_eq(k, "Call") { return is_localdate_call(expr) }
    return false
}

fn is_localtime_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Ident") { return is_localtime_name(expr["name"]) }
    if str_eq(k, "Call") { return is_localtime_call(expr) }
    return false
}

fn is_localdt_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Ident") { return is_localdt_name(expr["name"]) }
    if str_eq(k, "Call") { return is_localdt_call(expr) }
    return false
}

fn is_zone_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Ident") { return is_zone_name(expr["name"]) }
    if str_eq(k, "Call") { return is_zone_call(expr) }
    return false
}

// Recursive type predicates for Instant / Duration. Mirror is_int_expr.
// is_instant_expr / is_duration_expr return true only when the expression
// is provably of that type at codegen time. Anything ambiguous returns
// false - the BinOp dispatcher then leaves the expression on the
// untyped-int path, which is the safest fallback because at the runtime
// level all three types share the int64 slot.
fn is_instant_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Ident") {
        let name: String = expr["name"]
        return is_instant_name(name)
    }
    if str_eq(k, "Call") {
        return is_instant_call(expr)
    }
    if str_eq(k, "BinOp") {
        let op: String = expr["op"]
        if str_eq(op, "Plus") {
            // Instant + Duration  -> Instant
            // Duration + Instant  -> Instant
            if is_instant_expr(expr["left"]) {
                if is_duration_expr(expr["right"]) { return true }
            }
            if is_duration_expr(expr["left"]) {
                if is_instant_expr(expr["right"]) { return true }
            }
            return false
        }
        if str_eq(op, "Minus") {
            // Instant - Duration  -> Instant
            if is_instant_expr(expr["left"]) {
                if is_duration_expr(expr["right"]) { return true }
            }
            return false
        }
        return false
    }
    return false
}

fn is_duration_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "DurationLit") { return true }
    if str_eq(k, "Ident") {
        let name: String = expr["name"]
        return is_duration_name(name)
    }
    if str_eq(k, "Call") {
        return is_duration_call(expr)
    }
    if str_eq(k, "Neg") {
        return is_duration_expr(expr["inner"])
    }
    if str_eq(k, "BinOp") {
        let op: String = expr["op"]
        if str_eq(op, "Plus") {
            // Duration + Duration -> Duration
            if is_duration_expr(expr["left"]) {
                if is_duration_expr(expr["right"]) { return true }
            }
            return false
        }
        if str_eq(op, "Minus") {
            // Duration - Duration -> Duration
            // Instant - Instant   -> Duration (caught here, not in is_instant_expr)
            if is_duration_expr(expr["left"]) {
                if is_duration_expr(expr["right"]) { return true }
            }
            if is_instant_expr(expr["left"]) {
                if is_instant_expr(expr["right"]) { return true }
            }
            return false
        }
        if str_eq(op, "Star") {
            // Duration * Int  -> Duration
            // Int * Duration  -> Duration
            if is_duration_expr(expr["left"]) {
                if is_int_expr(expr["right"]) { return true }
            }
            if is_int_expr(expr["left"]) {
                if is_duration_expr(expr["right"]) { return true }
            }
            return false
        }
        if str_eq(op, "Slash") {
            // Duration / Int  -> Duration
            if is_duration_expr(expr["left"]) {
                if is_int_expr(expr["right"]) { return true }
            }
            return false
        }
        return false
    }
    return false
}

// Record a temporal-type violation. Surfaced as `#error` directives at the
// top of the generated C, identical machinery to cap_record_violation.
// kinds: "instant_plus_instant", "duration_plus_int", etc.

// Recursive type-propagation: is `expr` known-Int at codegen time?
// This unifies the BinOp(+) dispatch so chained arithmetic over Int
// operands stays arithmetic. Without recursion, a wrapping `+` between
// `BinOp(+) of two Ints` and another Int falls to el_str_concat because
// the outer dispatch only checks the immediate kind, not the inner.
//
// Rules:
//   Int literal                      -> Int
//   Ident in __int_names             -> Int
//   Call to known-Int builtin        -> Int
//   Neg of Int                       -> Int
//   BinOp arithmetic of two Ints     -> Int  (Plus, Minus, Star, Slash, Percent)
//   BinOp comparison/logical         -> Int  (yields 0/1; safe to treat as Int)
//   anything else                    -> not provably Int
fn is_int_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Int") { return true }
    if str_eq(k, "Ident") {
        let name: String = expr["name"]
        return is_int_name(name)
    }
    if str_eq(k, "Call") {
        return is_int_call(expr)
    }
    if str_eq(k, "Neg") {
        return is_int_expr(expr["inner"])
    }
    if str_eq(k, "Not") {
        return true
    }
    if str_eq(k, "BinOp") {
        let op: String = expr["op"]
        // Comparisons and logicals always yield 0/1 - safe Int.
        if str_eq(op, "EqEq")  { return true }
        if str_eq(op, "NotEq") { return true }
        if str_eq(op, "Lt")    { return true }
        if str_eq(op, "Gt")    { return true }
        if str_eq(op, "LtEq")  { return true }
        if str_eq(op, "GtEq")  { return true }
        if str_eq(op, "And")   { return true }
        if str_eq(op, "Or")    { return true }
        // Arithmetic propagates: Int op Int -> Int.
        if str_eq(op, "Plus")  {
            if is_int_expr(expr["left"]) {
                if is_int_expr(expr["right"]) { return true }
            }
            return false
        }
        if str_eq(op, "Minus") {
            if is_int_expr(expr["left"]) {
                if is_int_expr(expr["right"]) { return true }
            }
            return false
        }
        if str_eq(op, "Star")  {
            if is_int_expr(expr["left"]) {
                if is_int_expr(expr["right"]) { return true }
            }
            return false
        }
        if str_eq(op, "Slash") {
            if is_int_expr(expr["left"]) {
                if is_int_expr(expr["right"]) { return true }
            }
            return false
        }
        if str_eq(op, "Percent") {
            if is_int_expr(expr["left"]) {
                if is_int_expr(expr["right"]) { return true }
            }
            return false
        }
        return false
    }
    return false
}

// is_float_expr — true when expr is (or provably evaluates to) a Float value.
// Mirrors is_int_expr: Float literal, Neg of a Float, a Float-typed Ident
// (registered in __float_names), a Float-returning builtin Call, or a Float
// arithmetic BinOp (float propagates: if either operand is Float the result
// is Float). Drives the float-arithmetic dispatch in cg_expr and the EqEq/
// NotEq float comparison. Kept conservative (only provably-Float) so it never
// intercepts a value the Int paths must own — the two predicates are mutually
// exclusive on well-typed source.
fn is_float_expr(expr: Map<String, Any>) -> Bool {
    let k: String = expr["expr"]
    if str_eq(k, "Float") { return true }
    if str_eq(k, "Neg") {
        return is_float_expr(expr["inner"])
    }
    if str_eq(k, "Ident") {
        let name: String = expr["name"]
        return is_float_name(name)
    }
    if str_eq(k, "Call") {
        return is_float_call(expr)
    }
    if str_eq(k, "BinOp") {
        let op: String = expr["op"]
        if str_eq(op, "Plus") {
            if is_float_expr(expr["left"])  { return true }
            if is_float_expr(expr["right"]) { return true }
            return false
        }
        if str_eq(op, "Minus") {
            if is_float_expr(expr["left"])  { return true }
            if is_float_expr(expr["right"]) { return true }
            return false
        }
        if str_eq(op, "Star") {
            if is_float_expr(expr["left"])  { return true }
            if is_float_expr(expr["right"]) { return true }
            return false
        }
        if str_eq(op, "Slash") {
            if is_float_expr(expr["left"])  { return true }
            if is_float_expr(expr["right"]) { return true }
            return false
        }
        if str_eq(op, "Percent") {
            if is_float_expr(expr["left"])  { return true }
            if is_float_expr(expr["right"]) { return true }
            return false
        }
        return false
    }
    false
}

// float_operand_c — render a BinOp operand as a C double for float arithmetic.
// A provably-Float operand round-trips through el_to_float() (recovering the
// double from the int64 bit-slot); anything else is treated as Int-like and
// numerically converted with (double). This is why Float locals/params must
// be annotated `: Float` — an unannotated var holding float bits would be
// mis-converted by the (double) cast, exactly as the Int system requires
// annotations to dispatch arithmetic vs concat.
fn float_operand_c(expr: Map<String, Any>, expr_c: String) -> String {
    if is_float_expr(expr) { return "el_to_float(" + expr_c + ")" }
    return "(double)(" + expr_c + ")"
}

// -- Capability-kind enforcement ----------------------------------------------
//
// A program's top-level block (cgi / service / none) determines which
// runtime primitives it may call. The compiler records violations in
// process state during cg_expr's Call emission; codegen's entry point
// then emits #error directives at the top of the generated C so the
// downstream cc step fails with a clear message.
//
// Capability tiers:
//   "cgi"     - full self-formation. All primitives.
//   "service" - bounded. Cannot call self-formation primitives:
//               llm_call_agentic, llm_register_tool, dharma_emit,
//               dharma_field. Single-turn LLM calls are allowed.
//   "utility" - default. No DHARMA, no LLM. Pure compute + I/O.
//
// The compiler-level rule is structural: the binary either CAN or CANNOT
// emit the call. There is no runtime check, no opt-in, no override.


// Self-formation primitives - the cut between CGI and service. A program
// that emits these calls IS structurally a CGI; we forbid them everywhere
// else.

// Any DHARMA primitive - utilities have zero network presence.

// Any LLM primitive - utilities have no LLM access at all.


// Emit collected capability violations as #error directives. Called
// from codegen()'s entry point right after the cgi/service-block scan,
// so they appear at the very top of the generated C.

// Surface temporal-type violations as #error directives. The cg_expr BinOp
// dispatcher records each violation (Instant + Instant, Duration + Int, -)
// as a CSV entry "kind:detail" via time_record_violation. Each entry maps
// to a single #error so downstream cc fails the build with a clear El-
// source-level message before the bogus C even links.

// -- Builtin arity table -------------------------------------------------------
//
// El programs sometimes call runtime builtins with the wrong number of
// arguments (e.g. `http_serve(port)` instead of `http_serve(port, handler)`).
// Without this check the generated C compiles to a call with too few /
// too many args and fails downstream cc with a generic "too few arguments"
// message that doesn't point to the El source line.
//
// Strategy: a small static table mirrors el_runtime.h. Variadic builtins
// (el_list_new, el_map_new, args) and unknown identifiers (user fns,
// dynamic dispatch) return -1 -> no check. A mismatch records a violation
// in process state, which emit_arity_violations() turns into #error
// directives at the top of the generated C.


// Validate the call's arity against the builtin table. Returns true (always)
// because cg_expr ignores the result; -1 from builtin_arity signals
// "no check possible" (variadic or user-defined). A mismatch is recorded
// and surfaced as an #error at the bottom of the generated C, so cc fails
// before it ever attempts to type-check the wrong call.

// Emit recorded arity violations as #error directives.

fn add_int_name(name: String) -> Bool {
    let csv: String = state_get("__int_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__int_names", csv + name + ",")
    return true
}

fn add_float_name(name: String) -> Bool {
    let csv: String = state_get("__float_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__float_names", csv + name + ",")
    return true
}

fn add_instant_name(name: String) -> Bool {
    let csv: String = state_get("__instant_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__instant_names", csv + name + ",")
    return true
}

fn add_duration_name(name: String) -> Bool {
    let csv: String = state_get("__duration_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__duration_names", csv + name + ",")
    return true
}

fn add_calendar_name(name: String) -> Bool {
    let csv: String = state_get("__calendar_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__calendar_names", csv + name + ",")
    return true
}

fn add_caltime_name(name: String) -> Bool {
    let csv: String = state_get("__caltime_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__caltime_names", csv + name + ",")
    return true
}

fn add_rhythm_name(name: String) -> Bool {
    let csv: String = state_get("__rhythm_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__rhythm_names", csv + name + ",")
    return true
}

fn add_localdate_name(name: String) -> Bool {
    let csv: String = state_get("__localdate_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__localdate_names", csv + name + ",")
    return true
}

fn add_localtime_name(name: String) -> Bool {
    let csv: String = state_get("__localtime_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__localtime_names", csv + name + ",")
    return true
}

fn add_localdt_name(name: String) -> Bool {
    let csv: String = state_get("__localdt_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__localdt_names", csv + name + ",")
    return true
}

fn add_zone_name(name: String) -> Bool {
    let csv: String = state_get("__zone_names")
    if str_eq(csv, "") { csv = "," }
    let key: String = "," + name + ","
    if str_contains(csv, key) { return true }
    state_set("__zone_names", csv + name + ",")
    return true
}

fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
    state_set("__int_names", ",")
    state_set("__float_names", ",")
    state_set("__instant_names", ",")
    state_set("__duration_names", ",")
    state_set("__calendar_names", ",")
    state_set("__caltime_names", ",")
    state_set("__rhythm_names", ",")
    state_set("__localdate_names", ",")
    state_set("__localtime_names", ",")
    state_set("__localdt_names", ",")
    state_set("__zone_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") {
            add_int_name(pname)
        }
        // Bool is an integer in the value model (type_to_c maps Bool -> "int";
        // el_runtime.h: "Bool -> el_val_t (0 = false, nonzero = true)"), but
        // Bool names were registered nowhere. So `cond == want` between two
        // Bool params fell through to str_eq and dereferenced 0 or 1 as a
        // char* — an immediate segfault. Track them as int-like, which is what
        // they are.
        if str_eq(ptype, "Bool") {
            add_int_name(pname)
        }
        if str_eq(ptype, "Float") {
            add_float_name(pname)
        }
        if str_eq(ptype, "Instant") {
            add_instant_name(pname)
        }
        if str_eq(ptype, "Duration") {
            add_duration_name(pname)
        }
        if str_eq(ptype, "Calendar") {
            add_calendar_name(pname)
        }
        if str_eq(ptype, "CalendarTime") {
            add_caltime_name(pname)
        }
        if str_eq(ptype, "Rhythm") {
            add_rhythm_name(pname)
        }
        if str_eq(ptype, "LocalDate") {
            add_localdate_name(pname)
        }
        if str_eq(ptype, "LocalTime") {
            add_localtime_name(pname)
        }
        if str_eq(ptype, "LocalDateTime") {
            add_localdt_name(pname)
        }
        if str_eq(ptype, "Zone") {
            add_zone_name(pname)
        }
        let pi = pi + 1
    }
    return true
}

// fn_has_decorator — does this FnDef carry a decorator named `name`?
// Reads the `decorators` list [{name, args}] attached by the parser. Absent
// key -> native_list_len returns 0 -> false. This is the multi-decorator-aware
// replacement for the old single `decorator` string check, so a fn may stack
// roles with other decorators (e.g. `@route(...) @manager fn ...`).
fn fn_has_decorator(stmt: Map<String, Any>, name: String) -> Bool {
    let dl = stmt["decorators"]
    let n: Int = native_list_len(dl)
    let i = 0
    while i < n {
        let d = native_list_get(dl, i)
        let dn: String = d["name"]
        if str_eq(dn, name) { return true }
        let i = i + 1
    }
    false
}

// cg_entry_seam — emit the guards and the entry injection for a decorated fn.
// Extracted so it can be emitted into the WRAPPER when a construct injects at
// exit (the body then lives in a static helper) and inline otherwise.
//
// Guards run FIRST and may refuse — a non-zero return short-circuits the fn and
// becomes its result. A refused call must not report a crossing, so guards
// precede injection. EVERY guard runs (stacking @authenticate @authorize
// applies both); injection is topmost-wins, matching the VBD role convention,
// because a role is singular and a refusal is not.
fn cg_entry_seam(stmt: Map<String, Any>, fn_name: String) -> Void {
    // The crossing is resolved at EXECUTION. One refusable indirection replaces
    // the compile-time guard loop and injection loop: which constructs apply,
    // and whether any of them refuses, is read from a table written after the
    // binary exists.
    emit_line("  { el_val_t __s = el_seam_run(EL_STR(" + c_str_lit(fn_name) + "), 0, 0); if (__s) return __s; }")
}

// cg_exit_target / cg_exit_construct — the first construct on this fn that
// injects at exit, or "" if none.



// params_to_env_fields / params_to_env_init / params_to_env_args — the captured
// environment. This IS the closure: a struct of captured values, a function
// pointer that takes it, and the pair handed to the wrap target.
fn params_to_env_fields(params: [Any]) -> String {
    // A zero-param fn would emit `struct __env_f {  };` -- an empty struct is a
    // GNU extension, not C99, and an empty initialiser is C23.
    if native_list_len(params) == 0 { return " char __e0;" }
    let out: String = ""
    let n: Int = native_list_len(params)
    let i = 0
    while i < n {
        let p = native_list_get(params, i)
        let pn: String = p["name"]
        let out = out + " el_val_t " + pn + ";"
        let i = i + 1
    }
    out
}

fn params_to_env_init(params: [Any]) -> String {
    if native_list_len(params) == 0 { return "0" }
    let out: String = ""
    let n: Int = native_list_len(params)
    let i = 0
    while i < n {
        let p = native_list_get(params, i)
        let pn: String = p["name"]
        if i > 0 { let out = out + ", " }
        let out = out + pn
        let i = i + 1
    }
    out
}

fn params_to_env_args(params: [Any]) -> String {
    let out: String = ""
    let n: Int = native_list_len(params)
    let i = 0
    while i < n {
        let p = native_list_get(params, i)
        let pn: String = p["name"]
        if i > 0 { let out = out + ", " }
        let out = out + "__e->" + pn
        let i = i + 1
    }
    out
}


// params_to_call_args — "a, b, c" from the param list, for the wrapper's call
// into the body helper.

fn cg_fn(stmt: Map<String, Any>) -> Void {
    let fn_name: String = stmt["name"]
    state_set("__cg_current_fn", fn_name)
    // Record every top-level definition. El has NO namespacing -- imports are
    // textual inlining, so two modules defining the same name emit two C
    // functions into one translation unit. cc catches it, but names the
    // generated helpers first and the user's fn fourth. Recording it lets the
    // collision be reported at El level, in El terms.
    record_call(fn_name, "defines_at:" + stmt["line"])
    // Emit which constructs this fn carries, so the prohibition check can be a
    // query over relations instead of a walk inside the emitter.
    let cdl = stmt["decorators"]
    let n_cdl: Int = native_list_len(cdl)
    let cdi = 0
    while cdi < n_cdl {
        let cd = native_list_get(cdl, cdi)
        let cdn: String = cd["name"]
        record_call(fn_name, "@" + cdn)
        let cdi = cdi + 1
    }
    // Skip El's `fn main()` - C provides its own main() for top-level stmts
    // and a duplicate `el_val_t main(void)` would collide with it.
    if fn_name == "main" { return }
    let params = stmt["params"]
    let body = stmt["body"]
    let ret_type: String = stmt["ret_type"]
    let params_c: String = params_to_c(params)
    // VBD role enforcement: dharma_emit / dharma_field may only be called
    // from @manager-decorated functions. Surface violations to the C compiler
    // via #error directives emitted before the function definition. Read the
    // decorator LIST so the role may be stacked with other decorators.
    // Check each DECLARED prohibition in turn. The owning construct is known by
    // construction, so the diagnostic names it rather than hardcoding one rule.
    // The walk records; it no longer decides. Whether a call is legal where it
    // appears is a query over the emitted relations — see
    // tools/check/prohibitions.sh. An emitter that also adjudicates has to
    // contain every rule anyone will ever want.
    vbd_has_restricted_call(body)
    // Seed the per-function int-name set so the `+` codegen can dispatch
    // arithmetic vs concat on type-annotated identifiers.
    build_int_names_for_params(params)
    // ── API-reshape decorator-seam: auto-emit at the decorated-fn boundary ──
    // A decorated op self-reports with ZERO hand-written instrumentation in its
    // body. What it reports, and whether it may refuse, is DECLARED by the
    // construct — codegen reads it (see scan_declared_decorators).
    //
    // When a construct injects at exit, the body moves into a static helper and
    // the visible fn becomes a wrapper, so EARLY RETURNS still pass through the
    // exit injection. Emitting it only before the fall-through return would
    // silently miss every early return — which is precisely the class of
    // failure this seam exists to remove. Fns with no exit construct emit
    // exactly as before, byte for byte.
    if true {
        emit_line("static el_val_t __el_body_" + fn_name + "(" + params_c + ") {")
    } else {
        emit_line("el_val_t " + fn_name + "(" + params_c + ") {")
        cg_entry_seam(stmt, fn_name)
    }
    // Seed declared with parameter names so reassignment works
    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
    }
    // Lift the final bare expression into an explicit return so implicit
    // returns ("fn lex(s) { ... tokens }") actually return their value.
    // Void-returning functions skip this - wrapping `println(x)` in
    // `return -` is a C type error.
    let body_xformed = body
    if !str_eq(ret_type, "Void") {
        let body_xformed = transform_implicit_return(body)
    }
    let final_decl = cg_stmts(body_xformed, "  ", decl)
    el_release(final_decl)
    emit_line("  return 0;")
    emit_line("}")
    // The wrapper: guards, entry injection, the body call, then the exit
    // injection, which receives the result so it can observe what the fn
    // actually returned.
    if true {
        // EVERY fn gets an env struct and a thunk. Codegen cannot know which fns
        // a wrap construct will be bound to after the binary exists, and the
        // wrapper unconditionally calls through el_seam_wrap.
        emit_line("struct __env_" + fn_name + " { " + params_to_env_fields(params) + " };")
        emit_line("static el_val_t __thunk_" + fn_name + "(void* __v) {")
        emit_line("  struct __env_" + fn_name + "* __e = (struct __env_" + fn_name + "*)__v; (void)__e;")
        emit_line("  return __el_body_" + fn_name + "(" + params_to_env_args(params) + ");")
        emit_line("}")
    }
    if true {
        emit_line("el_val_t " + fn_name + "(" + params_c + ") {")
        cg_entry_seam(stmt, fn_name)
        emit_line("  struct __env_" + fn_name + " __env = { " + params_to_env_init(params) + " };")
        emit_line("  el_val_t __r = el_seam_wrap(EL_STR(" + c_str_lit(fn_name) + "), __thunk_" + fn_name + ", &__env);")
        emit_line("  __r = el_seam_run(EL_STR(" + c_str_lit(fn_name) + "), 1, __r);")
        emit_line("  return __r;")
        emit_line("}")
    }
    emit_blank()
}

// -- Top-level codegen ---------------------------------------------------------

fn is_fndef(stmt: Map<String, Any>) -> Bool {
    let kind: String = stmt["stmt"]
    if kind == "FnDef" { return true }
    false
}

fn 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 == "ProgramBlock" { return true }
    if kind == "ExternFn" { return true }
    false
}

// Format a string-or-EL_NULL argument for el_cgi_init.
fn cgi_arg(value: String, has_value: Bool) -> String {
    if has_value {
        return "EL_STR(" + c_str_lit(value) + ")"
    }
    return "EL_NULL"
}

// -- Program block: cross-cutting concerns injected at the process boundary ----
//
// emit_program_init — emit the `static void __el_program_init(void)` that
// carries a program's declared cross-cutting concerns. Called from main()
// BEFORE any user statement runs, so the guarantees hold for the whole process
// rather than depending on each call site remembering to ask for them.
//
// This is emitted at the point the `program` block is encountered, not buffered
// until main(). The streaming backend emits in source order and cannot hold a
// declaration's entry list alive until main(); emitting a named function here
// and calling it from main() means only a single bool has to survive.
//
// Order matters and is deliberate:
//   1. singleton FIRST — if another instance already holds the lock, refuse and
//      exit before touching configuration, ports, or any data directory.
//   2. config declarations — resolve env-or-default, one declaration per entry.
//   3. validate LAST — report EVERY missing/ill-typed entry at once, then exit.
//
// `singleton:` carries its `guards:` expression as its SECOND argument — the
// state the lock protects, evaluated here at the process boundary. A singleton
// without one does not compile (see below): a lock keyed on the program's name
// rather than on its state refuses unrelated instances and permits concurrent
// ones, which is not a weaker guard but a wrong one.
fn el_bool_arg(b: Bool) -> String {
    if b { return "EL_INT(1)" }
    return "EL_INT(0)"
}

fn emit_program_init(stmt: Map<String, Any>) -> Void {
    let pname: String = stmt["name"]
    emit_line("static void __el_program_init(void) {")
    let has_singleton: Bool = stmt["has_singleton"]
    if has_singleton {
        let sid: String = stmt["singleton"]
        let has_guards: Bool = stmt["has_guards"]
        if has_guards {
            let guards_c: String = cg_expr(stmt["guards"])
            emit_line("  el_singleton_acquire(EL_STR(" + c_str_lit(sid) + "), " + guards_c + ");")
        } else {
            // Refuse at COMPILE time. The alternative — emitting a name-keyed
            // lock — is the defect itself, and it fails silently in the direction
            // that loses data.
            emit_line("#error \"singleton '" + sid + "' declares no `guards:` — a singleton must name the state it protects, e.g. `guards: engram_resolve_data_dir()` (spec 18.2)\"")
        }
    }
    let entries = stmt["entries"]
    let n: Int = native_list_len(entries)
    let i = 0
    while i < n {
        let e = native_list_get(entries, i)
        let ename: String = e["name"]
        let etype: String = e["etype"]
        let edefault: String = e["default"]
        let has_default: Bool = e["has_default"]
        let erequired: Bool = e["required"]
        let arg_def: String = cgi_arg(edefault, has_default)
        emit_line("  el_config_declare(EL_STR(" + c_str_lit(ename) + "), EL_STR(" + c_str_lit(etype) + "), " + arg_def + ", " + el_bool_arg(has_default) + ", " + el_bool_arg(erequired) + ");")
        let i = i + 1
    }
    emit_line("  el_config_validate(EL_STR(" + c_str_lit(pname) + "));")
    emit_line("}")
    emit_blank()
}

// -- VBD role enforcement ------------------------------------------------------
//
// Scan a function body for direct calls to DHARMA-restricted builtins
// (dharma_emit, dharma_field). These may only appear inside @manager fns.

// record_call — emit one <caller> calls <callee> relation. The traversal is
// irreducible: you must walk the AST to find calls. What is NOT irreducible is
// the rule (which names are restricted) or the decision (#error). Those move
// out; the walk stays and changes purpose from deciding to recording.
fn record_call(caller: String, callee: String) -> Void {
    let path: String = env("EL_RELATIONS_OUT")
    if str_eq(path, "") { return }
    // cg_fn returns early for `main` -- C provides its own -- so __cg_current_fn
    // is unset while main's body is walked. Without this, every call in main is
    // invisible to the relation graph and both checkers have a blind spot
    // exactly where a program does its work. Caught by capability_query.sh.
    let caller2: String = caller
    if str_eq(caller2, "") { let caller2 = "main" }
    let prev: String = ""
    if fs_exists(path) { let prev = fs_read(path) }
    fs_write(path, prev + caller2 + " calls " + callee + "\n")
}


fn vbd_expr_has_restricted_call(expr: Map<String, Any>) -> Bool {
    let kind: String = expr["expr"]
    if str_eq(kind, "Call") {
        let func = expr["func"]
        let fk: String = func["expr"]
        if str_eq(fk, "Ident") {
            let fname: String = func["name"]
            record_call(state_get("__cg_current_fn"), fname)
        }
        if vbd_expr_has_restricted_call(func) { return true }
        let args = expr["args"]
        let an: Int = native_list_len(args)
        let ai = 0
        while ai < an {
            let a = native_list_get(args, ai)
            if vbd_expr_has_restricted_call(a) { return true }
            let ai = ai + 1
        }
        return false
    }
    if str_eq(kind, "BinOp") {
        let l = expr["left"]
        let r = expr["right"]
        if vbd_expr_has_restricted_call(l) { return true }
        if vbd_expr_has_restricted_call(r) { return true }
        return false
    }
    if str_eq(kind, "Not") {
        return vbd_expr_has_restricted_call(expr["inner"])
    }
    if str_eq(kind, "Neg") {
        return vbd_expr_has_restricted_call(expr["inner"])
    }
    if str_eq(kind, "Field") {
        return vbd_expr_has_restricted_call(expr["object"])
    }
    if str_eq(kind, "Index") {
        if vbd_expr_has_restricted_call(expr["object"]) { return true }
        if vbd_expr_has_restricted_call(expr["index"]) { return true }
        return false
    }
    if str_eq(kind, "Try") {
        return vbd_expr_has_restricted_call(expr["inner"])
    }
    if str_eq(kind, "Array") {
        let elems = expr["elems"]
        let n: Int = native_list_len(elems)
        let i = 0
        while i < n {
            let e = native_list_get(elems, i)
            if vbd_expr_has_restricted_call(e) { return true }
            let i = i + 1
        }
        return false
    }
    if str_eq(kind, "Map") {
        let pairs = expr["pairs"]
        let n: Int = native_list_len(pairs)
        let i = 0
        while i < n {
            let pair = native_list_get(pairs, i)
            let v = pair["value"]
            if vbd_expr_has_restricted_call(v) { return true }
            let i = i + 1
        }
        return false
    }
    if str_eq(kind, "If") {
        if vbd_expr_has_restricted_call(expr["cond"]) { return true }
        if vbd_has_restricted_call(expr["then"]) { return true }
        if vbd_has_restricted_call(expr["else"]) { return true }
        return false
    }
    if str_eq(kind, "For") {
        if vbd_expr_has_restricted_call(expr["list"]) { return true }
        if vbd_has_restricted_call(expr["body"]) { return true }
        return false
    }
    if str_eq(kind, "Match") {
        if vbd_expr_has_restricted_call(expr["subject"]) { return true }
        let arms = expr["arms"]
        let n: Int = native_list_len(arms)
        let i = 0
        while i < n {
            let arm = native_list_get(arms, i)
            let body = arm["body"]
            if vbd_expr_has_restricted_call(body) { return true }
            let i = i + 1
        }
        return false
    }
    false
}

fn vbd_has_restricted_call(stmts: [Map<String, Any>]) -> Bool {
    let n: Int = native_list_len(stmts)
    let i = 0
    while i < n {
        let s = native_list_get(stmts, i)
        let sk: String = s["stmt"]
        if str_eq(sk, "Let") {
            if vbd_expr_has_restricted_call(s["value"]) { return true }
        }
        if str_eq(sk, "Return") {
            if vbd_expr_has_restricted_call(s["value"]) { return true }
        }
        if str_eq(sk, "Expr") {
            if vbd_expr_has_restricted_call(s["value"]) { return true }
        }
        if str_eq(sk, "While") {
            if vbd_expr_has_restricted_call(s["cond"]) { return true }
            if vbd_has_restricted_call(s["body"]) { return true }
        }
        if str_eq(sk, "For") {
            if vbd_expr_has_restricted_call(s["list"]) { return true }
            if vbd_has_restricted_call(s["body"]) { return true }
        }
        let i = i + 1
    }
    false
}

// -- Entry point ----------------------------------------------------------------

fn codegen(stmts: [Map<String, Any>], source: String) -> String {
    // Detect cgi/service blocks: at most one declarative top-level block.
    // The block determines the program's CAPABILITY KIND:
    //   "cgi"     - full self-formation. Calls all primitives.
    //   "service" - bounded. Cannot call self-formation primitives
    //               (llm_call_agentic, llm_register_tool, dharma_emit,
    //               dharma_field, mindlink-creation).
    //   "utility" - default; no DHARMA membership, no LLM, no agentic.
    // Codegen enforces this with #error directives at every restricted
    // call site. The capability boundary is structural: a binary either
    // CAN or CANNOT do a thing, and the compiler decides at emission time.
    let n_top: Int = native_list_len(stmts)
    let cgi_count = 0
    let cgi_block: Map<String, Any> = { "stmt": "None" }
    let svc_count = 0
    let svc_block: Map<String, Any> = { "stmt": "None" }
    let ti = 0
    while ti < n_top {
        let s = native_list_get(stmts, ti)
        let sk: String = s["stmt"]
        if str_eq(sk, "CgiBlock") {
            let cgi_count = cgi_count + 1
            if cgi_count == 1 {
                let cgi_block = s
            }
        }
        if str_eq(sk, "ServiceBlock") {
            let svc_count = svc_count + 1
            if svc_count == 1 {
                let svc_block = s
            }
        }
        let ti = ti + 1
    }
    if cgi_count > 1 {
        emit_line("#error \"El: multiple cgi blocks in program (only one allowed)\"")
    }
    if svc_count > 1 {
        emit_line("#error \"El: multiple service blocks in program (only one allowed)\"")
    }
    if cgi_count >= 1 {
        if svc_count >= 1 {
            emit_line("#error \"El: program declares both cgi and service blocks (mutually exclusive - pick one)\"")
        }
    }
    // Stash the program kind so cg_expr's Call branch can enforce
    // per-kind capability restrictions on every emitted call.
    let kind: String = "utility"
    if cgi_count >= 1 { let kind = "cgi" }
    if svc_count >= 1 { let kind = "service" }
    state_set("__program_kind", kind)
    // Clear capability-violation accumulator from any prior compile.
    // Clear arity-violation accumulator from any prior compile.
    // Clear temporal-type-violation accumulator from any prior compile.

    // Preamble
    emit_line("#include <stdint.h>")
    emit_line("#include <stdlib.h>")
    emit_line("#include \"el_runtime.h\"")

    // Cross-module forward declarations: for each imported module, emit
    // #include "module.elh" so Clang sees the function signatures from
    // that module without needing the full source inlined. The .elh files
    // are generated by `elc --emit-header` and live in the same dist/
    // directory as the generated .c files. We use basename only (strip
    // the directory prefix and .el extension) so the include resolves
    // correctly regardless of the source tree layout.
    let imp_n: Int = native_list_len(stmts)
    let imp_i = 0
    while imp_i < imp_n {
        let imp_stmt = native_list_get(stmts, imp_i)
        let imp_kind: String = imp_stmt["stmt"]
        if str_eq(imp_kind, "Import") {
            let imp_path: String = imp_stmt["path"]
            // Extract basename: find last '/' and strip from there.
            let imp_path_len: Int = str_len(imp_path)
            let imp_last_slash: Int = -1
            let imp_j: Int = 0
            while imp_j < imp_path_len {
                let imp_c: String = str_slice(imp_path, imp_j, imp_j + 1)
                if str_eq(imp_c, "/") { let imp_last_slash = imp_j }
                let imp_j = imp_j + 1
            }
            let imp_base: String = str_slice(imp_path, imp_last_slash + 1, imp_path_len)
            // Strip .el extension if present.
            let imp_base_len: Int = str_len(imp_base)
            let imp_bname: String = imp_base
            if str_ends_with(imp_base, ".el") {
                let imp_bname = str_slice(imp_base, 0, imp_base_len - 3)
            }
            emit_line("#include \"" + imp_bname + ".elh\"")
        }
        let imp_i = imp_i + 1
    }
    emit_blank()

    // Forward declarations (skip `main` - C provides its own)
    let n: Int = native_list_len(stmts)
    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        let kind: String = stmt["stmt"]
        if kind == "FnDef" {
            let fn_name: String = stmt["name"]
            if !str_eq(fn_name, "main") {
                let params = stmt["params"]
                let params_c: String = params_to_c(params)
                emit_line("el_val_t " + fn_name + "(" + params_c + ");")
            }
        }
        if kind == "ExternFn" {
            let fn_name: String = stmt["name"]
            let params = stmt["params"]
            let params_c: String = params_to_c(params)
            emit_line("el_val_t " + fn_name + "(" + params_c + ");")
        }
        let i = i + 1
    }
    emit_blank()

    // Top-level `let` bindings -> file-scope storage. El programs use
    // top-level `let GREETING = "..."` as module constants that any
    // function below should be able to read. Without this pass, a top-
    // level Let only declares the name inside main()'s scope and any
    // function referencing it compiles to an undefined-symbol use of
    // the bare name (or, with non-static linkage, fails to link).
    //
    // We emit each top-level Let as `el_val_t NAME = VALUE;` at file
    // scope and seed the int-name set when the binding is `: Int` so
    // arithmetic/concat dispatch on the name works inside functions.
    // Runtime-call initializers (e.g. `let m = el_map_new(...)`) cannot
    // appear in C static initializers, so we emit a non-const slot and
    // initialize it at the top of main() before any user statements run.
    let has_toplevel_lets = false
    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        let kind: String = stmt["stmt"]
        if str_eq(kind, "Let") {
            let name: String = stmt["name"]
            let ltype: String = stmt["type"]
            if str_eq(ltype, "Int") { add_int_name(name) }
            let val = stmt["value"]
            let vk: String = val["expr"]
            if str_eq(vk, "Int") { add_int_name(name) }
            emit_line("el_val_t " + name + ";")
            let has_toplevel_lets = true
        }
        let i = i + 1
    }
    if has_toplevel_lets {
        emit_blank()
    }

    // Detect whether this compilation unit has an entry point.
    // A unit is a library (no C main emitted) when there is no fn main()
    // and no top-level executable statements. This supports separate
    // compilation: library .c files contain only function definitions.
    let has_el_main = false
    let has_toplevel_stmts = 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_chk: String = stmt["name"]
            if str_eq(fn_name_chk, "main") { let has_el_main = true }
        }
        if !is_fndef(stmt) {
            if !is_top_level_decl(stmt) {
                if !str_eq(sk, "Let") {
                    let has_toplevel_stmts = true
                }
            }
        }
        let i = i + 1
    }
    let is_library = false
    if !has_el_main {
        if !has_toplevel_stmts {
            let is_library = true
        }
    }

    // Program block: emit the cross-cutting init function before the user's
    // functions so main() can call it (see emit_program_init).
    let prog_have: Bool = false
    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        let sk4: String = stmt["stmt"]
        if str_eq(sk4, "ProgramBlock") {
            emit_program_init(stmt)
            let prog_have = true
        }
        let i = i + 1
    }

    // Function definitions
    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        if is_fndef(stmt) {
            cg_fn(stmt)
        }
        let i = i + 1
    }

    // Skip C main() for library units (no fn main, no top-level stmts)
    if is_library { return "" }

    // main(). Use _argc/_argv so El programs are free to declare their own
    // local `argv` / `argc` (compiler.el itself does this) without colliding
    // with the C-side parameters when fn main()'s body is folded in below.
    // From here the emitter is walking main's body. Without resetting this the
    // caller attribution is stale from the last cg_fn, so a violation inside
    // main is reported against whichever function happened to be emitted last
    // -- a diagnostic naming the wrong fn is worse than none.
    state_set("__cg_current_fn", "main")
    emit_line("int main(int _argc, char** _argv) {")
    emit_line("  el_runtime_init_args(_argc, _argv);")
    if prog_have {
        emit_line("  __el_program_init();")
    }
    if cgi_count >= 1 {
        let cname: String = cgi_block["name"]
        let cdid: String = cgi_block["dharma_id"]
        let cprin: String = cgi_block["principal"]
        let cnet: String = cgi_block["network"]
        let ceng: String = cgi_block["engram"]
        let has_did: Bool = cgi_block["has_dharma_id"]
        let has_prin: Bool = cgi_block["has_principal"]
        let has_net: Bool = cgi_block["has_network"]
        let has_eng: Bool = cgi_block["has_engram"]
        let arg_name: String = "EL_STR(" + c_str_lit(cname) + ")"
        let arg_did: String = cgi_arg(cdid, has_did)
        let arg_prin: String = cgi_arg(cprin, has_prin)
        let arg_net: String = cgi_arg(cnet, has_net)
        let arg_eng: String = cgi_arg(ceng, has_eng)
        emit_line("  el_cgi_init(" + arg_name + ", " + arg_did + ", " + arg_prin + ", " + arg_net + ", " + arg_eng + ");")
    }
    // Seed `declared` with the names of every top-level Let so that
    // cg_stmt emits plain assignment (`X = ...;`) instead of a redundant
    // `el_val_t X = ...;` shadowing the file-scope slot.
    let main_decl = native_list_empty()
    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        let kind: String = stmt["stmt"]
        if str_eq(kind, "Let") {
            let name: String = stmt["name"]
            let main_decl = native_list_append(main_decl, name)
        }
        let i = i + 1
    }
    // First pass: capture the body of `fn main()` if the source declared
    // one. We've already skipped emitting it as a regular el_val_t
    // function (see cg_fn early return); fold its body into C's main
    // alongside top-level statements so the program actually runs.
    let el_main_body = native_list_empty()
    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        if is_fndef(stmt) {
            let fn_name: String = stmt["name"]
            if str_eq(fn_name, "main") {
                let body = stmt["body"]
                let bn: Int = native_list_len(body)
                let bi: Int = 0
                while bi < bn {
                    let el_main_body = native_list_append(el_main_body, native_list_get(body, bi))
                    let bi = bi + 1
                }
            }
        }
        let i = i + 1
    }

    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        if is_fndef(stmt) {
            // skip - fn defs already emitted above; fn main body folded later
        } else {
            if is_top_level_decl(stmt) {
                // skip
            } else {
                let main_decl = cg_stmt(stmt, "  ", main_decl)
            }
        }
        // Release AST node after final use - each stmt is fully processed
        // by this point (forward decls, fn defs, top-level lets, and now
        // the main-body pass are all done). Releasing here prevents the
        // accumulated AST from exhausting memory on large source files.
        el_release(stmt)
        let i = i + 1
    }

    // Fold fn main()'s body in here, after top-level statements.
    let mn: Int = native_list_len(el_main_body)
    let mi: Int = 0
    while mi < mn {
        let mstmt = native_list_get(el_main_body, mi)
        let main_decl = cg_stmt(mstmt, "  ", main_decl)
        let mi = mi + 1
    }

    emit_line("  return 0;")
    emit_line("}")
    emit_blank()

    // Emit any accumulated capability-violation #error directives. cc
    // will fail on the first one and surface the message; placement at
    // the bottom is fine - preprocessor errors halt the build wherever
    // they appear.
    // Same for builtin-arity violations: cc halts on the first #error,
    // so a misuse of a known builtin (wrong arg count) fails the build
    // with a clear message naming the builtin and its expected arity.
    // Temporal-type violations (Instant + Instant, Duration + Int, -).

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

// ── Streaming codegen (JIT function-at-a-time) ─────────────────────────────
//
// codegen_streaming is a memory-efficient alternative to codegen().
// Instead of receiving the full parsed AST, it receives the raw token list
// and a pre-scanned signature list (from scan_fn_sigs in parser.el).
//
// Pipeline:
//   1. Scan phase (already done by caller): scan_fn_sigs(tokens) -> sigs
//   2. Emit preamble using sigs (no full AST needed)
//   3. For each top-level statement:
//        parse_one(tokens, pos) -> { node, pos }
//        cg_decl_streaming(node)    <- emit C for this one decl
//        el_release(node)           <- discard AST immediately
//
// Peak memory: O(one function's AST) instead of O(whole program AST).
//
// Entry point: codegen_streaming(tokens, sigs, source) -> String

// cg_decl_streaming — emit C for a single top-level declaration.
// Handles FnDef, ExternFn, TypeDef, EnumDef, Import, CgiBlock, ServiceBlock.
// Top-level Let statements go into the main() body, not here.
// Top-level executable statements (non-fn, non-let, non-decl) are
// accumulated into state and emitted later in main().
fn cg_decl_streaming(stmt: Map<String, Any>) -> Void {
    let sk: String = stmt["stmt"]
    if str_eq(sk, "FnDef") {
        cg_fn(stmt)
        return
    }
    // All other top-level decl kinds are either no-ops (Import, TypeDef,
    // EnumDef, ExternFn forward decl already emitted) or capability markers
    // (CgiBlock, ServiceBlock already handled in preamble).
    // Top-level Lets are also no-ops here (file-scope slots already emitted).
    // Executable top-level stmts (Expr, Return, etc.) are accumulated in state.
    if !str_eq(sk, "FnDef") {
        if !is_top_level_decl(stmt) {
            if !str_eq(sk, "Let") {
                // This is an executable top-level statement.
                // We can't emit it into main() yet because we haven't started
                // emitting main(). Accumulate in state as a list index.
                // We'll collect these into a list and emit after all fns.
                state_set("__streaming_has_toplevel_stmts", "1")
            }
        }
    }
}

// ── @route dispatcher generation ──────────────────────────────────────────────
//
// Scan the token stream for @route-decorated fns and synthesize a generic HTTP
// dispatcher `el_route_dispatch(method, clean, path, body)`. A decorated handler
// must have the uniform signature (method, path, body) -> String. The dispatcher
// matches `clean` (the query-stripped path, supplied by the caller) against each
// route and calls the handler with the ORIGINAL `path` so query strings survive.
// Returns the sentinel "__EL_NO_ROUTE__" when nothing matches, so the caller may
// fall through to any remaining hand-written branches (mixed mode).
//
// Decorator grammar: @route(path, method, kind, suffix)
//   path   — the match string (or the prefix, for compound)
//   method — "GET" | "POST" | ... ; a '|'-list like "GET|POST"; "ANY"/"" = no guard
//   kind   — "exact" (default) | "prefix" | "suffix" | "compound"
//   suffix — for "compound": the required str_ends_with suffix
//
// The dispatch table is emitted SPECIFICITY-SORTED (most-specific first), NOT in
// source order, so overlapping prefixes (e.g. /api/x/search vs /api/x) never
// shadow each other regardless of how the handlers are written.

// split_pipe — split "GET|POST" on '|' into ["GET","POST"]. Self-contained
// (no dependency on str_split runtime semantics).
fn split_pipe(s: String) -> [String] {
    let out: [String] = native_list_empty()
    let cur: String = ""
    let n: Int = str_len(s)
    let i: Int = 0
    while i < n {
        let ch: String = str_slice(s, i, i + 1)
        if str_eq(ch, "|") {
            let out = native_list_append(out, cur)
            let cur = ""
        } else {
            let cur = cur + ch
        }
        let i = i + 1
    }
    let out = native_list_append(out, cur)
    out
}

// route_make_record — build a route record map from the @route decorator args.
fn route_make_record(fn_name: String, args: [String]) -> Map<String, Any> {
    let na: Int = native_list_len(args)
    let rpath: String = ""
    if na >= 1 { let rpath = native_list_get(args, 0) }
    let rmethod: String = "GET"
    if na >= 2 { let rmethod = native_list_get(args, 1) }
    let rkind: String = "exact"
    if na >= 3 { let rkind = native_list_get(args, 2) }
    let rsuffix: String = ""
    if na >= 4 { let rsuffix = native_list_get(args, 3) }
    { "name": fn_name, "path": rpath, "method": rmethod, "kind": rkind, "suffix": rsuffix }
}

// route_spec_score — higher = more specific = emitted earlier. Ordering:
// exact > compound > suffix > prefix; within a class, a longer path/suffix
// wins (so /api/x/search sorts before /api/x). Guarantees correct dispatch
// independent of source order.
fn route_spec_score(rec: Map<String, Any>) -> Int {
    let kind: String = rec["kind"]
    let path: String = rec["path"]
    let suffix: String = rec["suffix"]
    let plen: Int = str_len(path)
    let slen: Int = str_len(suffix)
    if str_eq(kind, "exact") { return 4000000 + plen }
    if str_eq(kind, "compound") { return 3000000 + plen * 100 + slen }
    if str_eq(kind, "suffix") { return 2000000 + slen }
    return 1000000 + plen
}

// route_sort_desc — selection sort of route records by descending specificity.
// N is small (routes per module), so O(n^2) is fine and keeps codegen simple.
fn route_sort_desc(recs: [Map<String, Any>]) -> [Map<String, Any>] {
    let n: Int = native_list_len(recs)
    let out: [Map<String, Any>] = native_list_empty()
    let used: [Bool] = native_list_empty()
    let u: Int = 0
    while u < n {
        let used = native_list_append(used, false)
        let u = u + 1
    }
    let picked: Int = 0
    while picked < n {
        let best_i: Int = 0 - 1
        let best_score: Int = 0 - 1
        let i: Int = 0
        while i < n {
            let is_used: Bool = native_list_get(used, i)
            if !is_used {
                let sc: Int = route_spec_score(native_list_get(recs, i))
                if sc > best_score {
                    let best_score = sc
                    let best_i = i
                }
            }
            let i = i + 1
        }
        let out = native_list_append(out, native_list_get(recs, best_i))
        // Rebuild `used` with best_i marked (runtime has no native_list_set).
        let new_used: [Bool] = native_list_empty()
        let j: Int = 0
        while j < n {
            if j == best_i {
                let new_used = native_list_append(new_used, true)
            } else {
                let new_used = native_list_append(new_used, native_list_get(used, j))
            }
            let j = j + 1
        }
        let used = new_used
        let picked = picked + 1
    }
    out
}

// scan_routes — token-level scan collecting every @route-decorated fn as a
// route record. Runs once per module (like scan_fn_sigs) so the dispatcher can
// be synthesized in the streaming backend, which discards per-fn ASTs. Handles
// decorator STACKING: `@route(...) @manager fn` still records the route.
fn scan_routes(tokens: [Any]) -> [Map<String, Any>] {
    let total: Int = native_list_len(tokens) / 3
    let recs: [Map<String, Any>] = native_list_empty()
    let has_pending: Bool = false
    let pending_args: [String] = native_list_empty()
    let pos: Int = 0
    let going: Bool = true
    while going {
        if pos >= total {
            let going = false
        } else {
            let k: String = tok_kind(tokens, pos)
            if str_eq(k, "Eof") {
                let going = false
            } else {
                if str_eq(k, "At") {
                    let dname: String = tok_value(tokens, pos + 1)
                    let p: Int = pos + 2
                    let args: [String] = native_list_empty()
                    let ka: String = tok_kind(tokens, p)
                    if str_eq(ka, "LParen") {
                        let p = p + 1
                        let running: Bool = true
                        while running {
                            let kd: String = tok_kind(tokens, p)
                            if str_eq(kd, "RParen") {
                                let running = false
                            } else {
                                if str_eq(kd, "Eof") {
                                    let running = false
                                } else {
                                    if str_eq(kd, "Str") {
                                        let args = native_list_append(args, tok_value(tokens, p))
                                    }
                                    let p = p + 1
                                }
                            }
                        }
                        if str_eq(tok_kind(tokens, p), "RParen") { let p = p + 1 }
                    }
                    if str_eq(dname, "route") {
                        let has_pending = true
                        let pending_args = args
                    }
                    let pos = p
                } else {
                    if str_eq(k, "Fn") {
                        let fname: String = tok_value(tokens, pos + 1)
                        if has_pending {
                            let recs = native_list_append(recs, route_make_record(fname, pending_args))
                            let has_pending = false
                        }
                        let pos = pos + 2
                    } else {
                        let pos = pos + 1
                    }
                }
            }
        }
    }
    recs
}

// program_has_routes — did scan_routes find any @route fn?
fn program_has_routes(recs: [Map<String, Any>]) -> Bool {
    native_list_len(recs) > 0
}

// ── Declared constructs ──────────────────────────────────────────────────────
//
// A construct declares its own meaning. Codegen READS the declaration instead
// of knowing the construct:
//
//     @decorator("injects_at_entry", "engram_boundary_beat")
//     fn manager() {}
//
// Any fn decorated @manager then gets
//     engram_boundary_beat(EL_STR(<fn>), EL_STR("manager"));
// injected at entry. Adding a construct is a declaration IN THE PROGRAM. It
// does not touch the compiler.
//
// WHY: codegen called fn_has_decorator for exactly three names — manager,
// accessor, route. Twelve others parsed, attached as {name,args}, and compiled
// to nothing, including four that look like protection: @authenticate (6 uses),
// @authorize (3), @rate_limit (3), @validate (2). The meaning of a construct
// had nowhere to live except the emitter, so every construct was a compiler
// edit and an undeclared one was silently inert. A lookup table of
// name -> injection would have moved the enumeration twenty lines up, not
// removed it; the class only goes away when the construct itself carries its
// meaning.
//
// manager and accessor are seeded below as the compiled-in core — a seed the
// periphery is declared against, the same shape as fixedSelf in substrate.go:
// a complete fallback exists, declaration is enrichment. A program may declare
// its own constructs and may redeclare these.
//
// This is the injection half of the seam. The prohibition half (@manager's
// #error on dharma_emit) is still hardcoded, because "which calls may appear
// inside this boundary" is a query over program structure and there is nothing
// yet to ask.



// A GUARD is an injection that may refuse. The declared target is called at
// entry with the same (fn, construct) pair; a non-zero return short-circuits
// the decorated fn and becomes its result.
//
//     @decorator("guards_at_entry", "my_auth_check")
//     fn authenticate() {}
//
//     @authenticate
//     fn handler() -> String { ... }   // my_auth_check runs first and may refuse
//
// This is what @authenticate (6 uses), @authorize (3), @rate_limit (3) and
// @validate (2) needed and never had. They parsed, attached, and compiled to
// nothing — fourteen applications that read as protection and emitted no
// instruction. The compiler still knows nothing about authentication: the
// program points the construct at its own function.


// An EXIT injection runs after the fn returns and receives the result:
//     target(<fn>, <construct>, <result>)
//
//     @decorator("injects_at_exit", "persist_now")
//     fn durable() {}
//
// This is `hold`'s after-crossing face. §6 of the design records 62
// persist-after-mutate sites and 9-of-9 failed index-after-append sites — every
// one an obligation that decayed into "remember to do this after you mutate."
// An obligation a human must remember is not an obligation.


// A WRAP construct receives the body as a CLOSURE and decides how, whether, and
// how many times to invoke it.
//
//     target(<fn>, <construct>, el_val_t (*body)(void*), void* env) -> el_val_t
//
// C has no closure SYNTAX. That is not the same as C being unable to express a
// closure: every C++ lambda, every Go closure, every Rust closure compiles to a
// struct of captured values plus a function pointer, which is exactly what is
// emitted here. Taking "C has no closures" as a fact about what is possible,
// rather than about one grammar, produces a strictly weaker construct that can
// only decide whether to repeat — no timeout, no rollback-and-retry, no
// parallel, no memoize-on-arguments. It would also have crippled the JS backend,
// which has closures natively, for a limit in the C one.


// A PROHIBITION is the other half of a boundary: not what runs when something
// crosses, but what may not cross at all.
//
//     @decorator("prohibits_outside", "dharma_emit,dharma_field")
//     fn manager() {}
//
// Calls to those names may then appear ONLY inside a fn carrying @manager.
// Before this the rule lived as two string literals in vbd_is_restricted_name
// and one #error in cg_fn -- one prohibition, uneditable without a compiler
// release, and unrankable because a #error is a string handed to cpp, which
// knows nothing about El.
//
// The construct list is kept in one state key so the recursive walker can be
// parameterised without threading an argument through every branch of it --
// the same mechanism codegen already uses for __match_counter and
// __if_expr_counter.
fn declare_prohibition(construct: String, names_csv: String) -> Void {
    record_call(construct, "prohibits:" + names_csv)
}



// scan_declared_decorators — token-level pre-pass registering every construct
// the program declares. Runs once per module alongside scan_routes, because
// the streaming backend discards per-fn ASTs and there is no whole-program AST
// to walk.
fn scan_declared_decorators(tokens: [Any]) -> Void {
    declare_prohibition("manager", "dharma_emit,dharma_field")
    let total: Int = native_list_len(tokens) / 3
    let has_pending_p: Bool = false
    let pending_prohibit: String = ""
    let pos: Int = 0
    let going: Bool = true
    while going {
        if pos >= total {
            let going = false
        } else {
            let k: String = tok_kind(tokens, pos)
            if str_eq(k, "Eof") {
                let going = false
            } else {
                if str_eq(k, "At") {
                    let dname: String = tok_value(tokens, pos + 1)
                    let p: Int = pos + 2
                    let args: [String] = native_list_empty()
                    let ka: String = tok_kind(tokens, p)
                    if str_eq(ka, "LParen") {
                        let p = p + 1
                        let running: Bool = true
                        while running {
                            let kd: String = tok_kind(tokens, p)
                            if str_eq(kd, "RParen") {
                                let running = false
                            } else {
                                if str_eq(kd, "Eof") {
                                    let running = false
                                } else {
                                    if str_eq(kd, "Str") {
                                        let args = native_list_append(args, tok_value(tokens, p))
                                    }
                                    let p = p + 1
                                }
                            }
                        }
                        if str_eq(tok_kind(tokens, p), "RParen") { let p = p + 1 }
                    }
                    if str_eq(dname, "decorator") {
                        if native_list_len(args) >= 2 {
                            let dkind: String = native_list_get(args, 0)
                            if str_eq(dkind, "prohibits_outside") {
                                let has_pending_p = true
                                let pending_prohibit = native_list_get(args, 1)
                            }
                        }
                    }
                    let pos = p
                } else {
                    if str_eq(k, "Fn") {
                        let fname: String = tok_value(tokens, pos + 1)
                        if has_pending_p {
                            declare_prohibition(fname, pending_prohibit)
                            let has_pending_p = false
                        }
                        let pos = pos + 2
                    } else {
                        let pos = pos + 1
                    }
                }
            }
        }
    }
}

// route_method_guard — C boolean prefix guarding on HTTP method, or "" for none.
fn route_method_guard(method: String) -> String {
    if str_eq(method, "") { return "" }
    if str_eq(method, "ANY") { return "" }
    if str_contains(method, "|") {
        let parts: [String] = split_pipe(method)
        let np: Int = native_list_len(parts)
        let expr: String = ""
        let i: Int = 0
        while i < np {
            let m: String = native_list_get(parts, i)
            if str_eq(m, "") {
                let i = i + 1
            } else {
                let piece: String = "str_eq(method, EL_STR(" + c_str_lit(m) + "))"
                if str_eq(expr, "") {
                    let expr = piece
                } else {
                    let expr = expr + " || " + piece
                }
                let i = i + 1
            }
        }
        if str_eq(expr, "") { return "" }
        return "(" + expr + ") && "
    }
    "str_eq(method, EL_STR(" + c_str_lit(method) + ")) && "
}

// route_match_expr — C boolean matching `clean` against the route path/kind.
fn route_match_expr(kind: String, path: String, suffix: String) -> String {
    if str_eq(kind, "prefix") {
        return "str_starts_with(clean, EL_STR(" + c_str_lit(path) + "))"
    }
    if str_eq(kind, "suffix") {
        return "str_ends_with(clean, EL_STR(" + c_str_lit(path) + "))"
    }
    if str_eq(kind, "compound") {
        return "str_starts_with(clean, EL_STR(" + c_str_lit(path) + ")) && str_ends_with(clean, EL_STR(" + c_str_lit(suffix) + "))"
    }
    "str_eq(clean, EL_STR(" + c_str_lit(path) + "))"
}

// emit_route_dispatch — emit the generated el_route_dispatch definition from the
// specificity-sorted route records. No-op if there are no routes.
fn emit_route_dispatch(recs: [Map<String, Any>]) -> Void {
    if !program_has_routes(recs) { return }
    let sorted: [Map<String, Any>] = route_sort_desc(recs)
    emit_line("// ── generated @route dispatcher (specificity-sorted) ──")
    emit_line("el_val_t el_route_dispatch(el_val_t method, el_val_t clean, el_val_t path, el_val_t body) {")
    let n: Int = native_list_len(sorted)
    let i: Int = 0
    while i < n {
        let rec = native_list_get(sorted, i)
        let guard: String = route_method_guard(rec["method"])
        let match_e: String = route_match_expr(rec["kind"], rec["path"], rec["suffix"])
        let fn_name: String = rec["name"]
        emit_line("  if (" + guard + match_e + ") { return " + fn_name + "(method, path, body); }")
        let i = i + 1
    }
    emit_line("  return EL_STR(\"__EL_NO_ROUTE__\");")
    emit_line("}")
    emit_blank()
}

// emit_streaming_preamble — emit #includes, forward decls, and file-scope lets
// using the pre-scanned signature data (no full AST).
fn emit_streaming_preamble(sigs: [Map<String, Any>], source: String) -> Void {
    let n: Int = native_list_len(sigs)

    // Detect program kind from sigs
    let cgi_count: Int = 0
    let svc_count: Int = 0
    let i: Int = 0
    while i < n {
        let sig = native_list_get(sigs, i)
        let sk: String = sig["kind"]
        if str_eq(sk, "cgi_block") { let cgi_count = cgi_count + 1 }
        if str_eq(sk, "service_block") { let svc_count = svc_count + 1 }
        let i = i + 1
    }
    if cgi_count > 1 {
        emit_line("#error \"El: multiple cgi blocks in program (only one allowed)\"")
    }
    if svc_count > 1 {
        emit_line("#error \"El: multiple service blocks in program (only one allowed)\"")
    }
    if cgi_count >= 1 {
        if svc_count >= 1 {
            emit_line("#error \"El: program declares both cgi and service blocks (mutually exclusive - pick one)\"")
        }
    }
    let kind: String = "utility"
    if cgi_count >= 1 { let kind = "cgi" }
    if svc_count >= 1 { let kind = "service" }
    state_set("__program_kind", kind)

    emit_line("#include <stdint.h>")
    emit_line("#include <stdlib.h>")
    emit_line("#include \"el_runtime.h\"")
    emit_blank()

    // Forward declarations — use pre-computed params_c strings from scan.
    let i = 0
    while i < n {
        let sig = native_list_get(sigs, i)
        let sk: String = sig["kind"]
        if str_eq(sk, "fn") {
            let fn_name: String = sig["name"]
            if !str_eq(fn_name, "main") {
                let params_c: String = sig["params_c"]
                emit_line("el_val_t " + fn_name + "(" + params_c + ");")
            }
        }
        if str_eq(sk, "extern_fn") {
            let fn_name: String = sig["name"]
            let params_c: String = sig["params_c"]
            emit_line("el_val_t " + fn_name + "(" + params_c + ");")
        }
        let i = i + 1
    }
    emit_blank()

    // File-scope let slots
    let has_toplevel_lets: Bool = false
    let i = 0
    while i < n {
        let sig = native_list_get(sigs, i)
        let sk: String = sig["kind"]
        if str_eq(sk, "toplevel_let") {
            let name: String = sig["name"]
            let ltype: String = sig["ltype"]
            if str_eq(ltype, "Int") { add_int_name(name) }
            emit_line("el_val_t " + name + ";")
            let has_toplevel_lets = true
        }
        let i = i + 1
    }
    if has_toplevel_lets { emit_blank() }
}

// codegen_streaming — JIT function-at-a-time compiler backend.
// tokens: flat token list from lex()
// sigs:   pre-scanned signature list from scan_fn_sigs(tokens)
// source: original source string (for string literal lookup)
fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) -> String {
    let total_tokens: Int = native_list_len(tokens) / 3

    // Emit preamble (forward decls, file-scope lets, #includes)
    // Arena scope: free intermediate strings built during preamble emission.
    let preamble_mark: Any = el_arena_push()
    emit_streaming_preamble(sigs, source)
    el_arena_pop(preamble_mark)

    // @route: scan the token stream once for @route-decorated fns. Kept in
    // codegen_streaming scope (survives the per-fn arena pops and el_release of
    // tokens below via refcount, like `sigs`). If any exist, forward-declare the
    // generated dispatcher NOW so hand-written fns (e.g. handle_request) may call
    // it before its definition is emitted after the fn-emit loop.
    // Declared constructs: register what the program says its decorators mean,
    // before any fn is emitted. Must precede the fn-emit loop — cg_fn reads the
    // registry to decide what, if anything, a decorator injects.
    scan_declared_decorators(tokens)

    let route_records: [Map<String, Any>] = scan_routes(tokens)
    if program_has_routes(route_records) {
        emit_line("el_val_t el_route_dispatch(el_val_t method, el_val_t clean, el_val_t path, el_val_t body);")
        emit_blank()
    }

    // Detect whether there is a fn main() and whether there are top-level
    // executable stmts (for library detection) from sigs.
    let has_el_main: Bool = false
    let ns: Int = native_list_len(sigs)
    let si: Int = 0
    while si < ns {
        let sig = native_list_get(sigs, si)
        let sk2: String = sig["kind"]
        if str_eq(sk2, "fn") {
            let fn_name_chk: String = sig["name"]
            if str_eq(fn_name_chk, "main") { let has_el_main = true }
        }
        let si = si + 1
    }

    // Collect top-level let names for seeding main()'s declared set.
    let toplevel_let_names: [String] = native_list_empty()
    let si = 0
    while si < ns {
        let sig = native_list_get(sigs, si)
        let sk2: String = sig["kind"]
        if str_eq(sk2, "toplevel_let") {
            let tname: String = sig["name"]
            let toplevel_let_names = native_list_append(toplevel_let_names, tname)
        }
        let si = si + 1
    }

    // In test mode: collect test function names for harness main().
    let test_is_mode: Bool = false
    let tmode_str: String = state_get("__test_mode")
    if str_eq(tmode_str, "1") { let test_is_mode = true }
    let test_names: [String] = native_list_empty()
    let test_c_names: [String] = native_list_empty()

    // Emit test harness preamble (counters, fail printer) when in test mode.
    if test_is_mode {
        emit_line("#include <stdio.h>")
        emit_line("#include <string.h>")
        emit_line("#include <time.h>")
        emit_blank()
        // Per-test result state. Reset by __el_reg_invoke before each test, so
        // every test gets its own record rather than contributing to a global
        // tally. The first failure message is retained; later ones only bump
        // the count, which keeps the common case allocation-free.
        emit_line("static int __el_cur_fails = 0;")
        emit_line("static int __el_cur_asserts = 0;")
        emit_line("static char __el_cur_msg[512] = \"\";")
        emit_line("static const char *__el_cur_test = \"(none)\";")
        emit_line("static void __el_test_fail(const char *msg) {")
        emit_line("  if (__el_cur_fails == 0 && msg) {")
        emit_line("    snprintf(__el_cur_msg, sizeof __el_cur_msg, \"%s\", msg);")
        emit_line("  }")
        emit_line("  __el_cur_fails++; __el_cur_asserts++;")
        emit_line("}")
        emit_blank()
        // Forward declarations for the registry accessors. The definitions are
        // emitted at the END of the unit (they reference the test functions,
        // which do not exist yet at this point), but the El-side runner is
        // compiled in between and calls them — so it needs the prototypes here.
        emit_line("el_val_t __el_reg_count(void);")
        emit_line("el_val_t __el_reg_name(el_val_t i);")
        emit_line("el_val_t __el_reg_invoke(el_val_t i);")
        emit_line("el_val_t __el_reg_last_ns(void);")
        emit_line("el_val_t __el_reg_msg(void);")
        emit_line("el_val_t __el_reg_asserts(void);")
        emit_line("el_val_t __el_opt_json(void);")
        emit_blank()
    }

    // Streaming parse-emit loop.
    // For each parsed stmt:
    //   - FnDef (not main): emit immediately via cg_fn, release AST
    //   - Others: accumulate only fn-main body and top-level executable stmts
    //     (these are small in count relative to fn bodies)
    let pos: Int = 0
    let el_main_body: [Map<String, Any>] = native_list_empty()
    let toplevel_exec_stmts: [Map<String, Any>] = native_list_empty()
    // CGI IDENTITY CAPTURE (2026-08-09). A cgi block is a top-level DECLARATION, so
    // the classifier below correctly excludes it from toplevel_exec_stmts and calls
    // el_release on it. The identity emission further down then searched
    // toplevel_exec_stmts for it — a list that structurally can never contain it —
    // found nothing, and emitted nothing, silently. Measured: that search sees only
    // [Let, Expr] for a program whose first statement is a cgi block.
    // Fix: copy the values out BEFORE the release (strings, so no dangling reference)
    // and emit from these. No search, so the failure mode is removed rather than moved.
    let cgi_have: Bool = false
    let prog_have: Bool = false
    let cgi_name_v: String = ""
    let cgi_did_v: String = ""
    let cgi_prin_v: String = ""
    let cgi_net_v: String = ""
    let cgi_eng_v: String = ""
    let cgi_has_did: Bool = false
    let cgi_has_prin: Bool = false
    let cgi_has_net: Bool = false
    let cgi_has_eng: Bool = false
    let has_toplevel_exec: Bool = false

    let stream_running: Bool = true
    while stream_running {
        if pos >= total_tokens {
            let stream_running = false
        } else {
            let k: String = tok_kind(tokens, pos)
            if str_eq(k, "Eof") {
                let stream_running = false
            } else {
                if str_eq(k, "Test") {
                    if test_is_mode {
                        // Compile test "name" { ... } block into a static void __el_test_NAME() function.
                        let p: Int = pos + 1
                        let test_name: String = "unnamed"
                        if str_eq(tok_kind(tokens, p), "Str") {
                            let test_name = tok_value(tokens, p)
                            let p = p + 1
                        }
                        let fn_c_name: String = "__el_test_" + sanitize_test_name(test_name)
                        let test_names = native_list_append(test_names, test_name)
                        let test_c_names = native_list_append(test_c_names, fn_c_name)
                        // Emit the test function header.
                        emit_line("static void " + fn_c_name + "(void) {")
                        emit_line("  __el_cur_test = \"" + c_escape(test_name) + "\";")
                        // Skip the opening LBrace and parse body statements.
                        if str_eq(tok_kind(tokens, p), "LBrace") { let p = p + 1 }
                        let body_decl: [String] = native_list_empty()
                        let body_done: Bool = false
                        while !body_done {
                            let bk: String = tok_kind(tokens, p)
                            if str_eq(bk, "RBrace") {
                                let body_done = true
                            } else {
                                if str_eq(bk, "Eof") {
                                    let body_done = true
                                } else {
                                    let br = parse_one(tokens, p)
                                    let bstmt = br["node"]
                                    let np: Int = br["pos"]
                                    el_release(br)
                                    if np > p {
                                        let body_arena: Any = el_arena_push()
                                        let body_decl = cg_stmt(bstmt, "  ", body_decl)
                                        el_arena_pop(body_arena)
                                        el_release(bstmt)
                                        let p = np
                                    } else {
                                        let p = p + 1
                                    }
                                }
                            }
                        }
                        // Skip past closing RBrace.
                        if str_eq(tok_kind(tokens, p), "RBrace") { let p = p + 1 }
                        el_release(body_decl)
                        emit_line("}")
                        emit_blank()
                        let pos = p
                    } else {
                        // Non-test mode: skip test blocks entirely to avoid OOM.
                        // Without this skip, the body `{ ... }` would be parsed as a Map
                        // literal, building a huge AST with O(n²) string allocation.
                        let p: Int = pos + 1
                        let k_name: String = tok_kind(tokens, p)
                        if str_eq(k_name, "Str") { let p = p + 1 }
                        let k_body: String = tok_kind(tokens, p)
                        if str_eq(k_body, "LBrace") { let p = skip_to_rbrace(tokens, p) }
                        let pos = p
                    }
                } else {
                let r = parse_one(tokens, pos)
                let stmt = r["node"]
                let new_pos: Int = r["pos"]
                el_release(r)

                // Guard against infinite loops
                if new_pos <= pos {
                    el_release(stmt)
                    let pos = pos + 1
                } else {
                    let sk: String = stmt["stmt"]

                    if str_eq(sk, "FnDef") {
                        let fn_name2: String = stmt["name"]
                        if str_eq(fn_name2, "main") {
                            // Capture main() body for later
                            let body = stmt["body"]
                            let bn: Int = native_list_len(body)
                            let bi: Int = 0
                            while bi < bn {
                                let el_main_body = native_list_append(el_main_body, native_list_get(body, bi))
                                let bi = bi + 1
                            }
                            el_release(stmt)
                        } else {
                            // Emit immediately — this is the JIT core
                            // Arena scope: free all intermediate strings (str_concat,
                            // int_to_str, cg_expr fragments) after each function.
                            let fn_arena_mark: Any = el_arena_push()
                            cg_fn(stmt)
                            el_release(stmt)
                            el_arena_pop(fn_arena_mark)
                            el_mem_check()
                        }
                    } else {
                        if is_top_level_decl(stmt) {
                            // Import, TypeDef, EnumDef, CgiBlock, ServiceBlock, ExternFn
                            // These are no-ops in codegen (forward decls already emitted)
                            // — except a CgiBlock, whose declared identity must survive
                            // this release to be emitted as a compiled constant.
                            // A ProgramBlock's cross-cutting declarations are
                            // emitted HERE, as a named init function, because the
                            // streaming backend cannot hold the entry list alive
                            // until main(). Only the bool survives.
                            if str_eq(sk, "ProgramBlock") {
                                emit_program_init(stmt)
                                let prog_have = true
                            }
                            if str_eq(sk, "CgiBlock") {
                                let cgi_have = true
                                let cgi_name_v = stmt["name"]
                                let cgi_did_v = stmt["dharma_id"]
                                let cgi_prin_v = stmt["principal"]
                                let cgi_net_v = stmt["network"]
                                let cgi_eng_v = stmt["engram"]
                                let cgi_has_did = stmt["has_dharma_id"]
                                let cgi_has_prin = stmt["has_principal"]
                                let cgi_has_net = stmt["has_network"]
                                let cgi_has_eng = stmt["has_engram"]
                            }
                            el_release(stmt)
                        } else {
                            if str_eq(sk, "Let") {
                                // Top-level let: file-scope slot already declared.
                                // Keep for main() init — these are few and small.
                                let toplevel_exec_stmts = native_list_append(toplevel_exec_stmts, stmt)
                                let has_toplevel_exec = true
                            } else {
                                // Executable top-level stmt (rare)
                                let toplevel_exec_stmts = native_list_append(toplevel_exec_stmts, stmt)
                                let has_toplevel_exec = true
                            }
                        }
                    }

                    let pos = new_pos
                }
                }
            }
        }
    }

    // @route: emit the generated dispatcher definition now — after every handler
    // fn has been emitted, but before `tokens` is released (route_records holds
    // its own refs to the extracted strings). No-op unless the module declared
    // at least one @route fn. Emitted before the test/library early-returns so it
    // is present in library modules (e.g. neuron's routes.el) too.
    let route_arena_mark: Any = el_arena_push()
    emit_route_dispatch(route_records)
    el_arena_pop(route_arena_mark)

    // Tokens fully consumed by the streaming loop — release now to free peak heap.
    el_release(tokens)

    if test_is_mode {
        // Test mode: emit test harness main() that calls each collected test function.
        // Discard El's main body and top-level exec stmts (not needed in test harness).
        el_release(el_main_body)
        el_release(toplevel_exec_stmts)
        el_release(toplevel_let_names)
        el_release(sigs)

        let test_arena_mark: Any = el_arena_push()
        let tn: Int = native_list_len(test_c_names)

        // ── Generated test registry ──────────────────────────────────────────
        // Discovery happens HERE, at compile time. The runner never searches
        // for tests; it walks this table. That ordering — discovery strictly
        // before execution — is what makes --list, filtering, sharding and
        // per-test reporting possible later, and it is why the old harness
        // (which inlined direct calls into main) could not have any of them.
        emit_line("typedef void (*__el_test_fp)(void);")
        emit_line("typedef struct { const char *name; __el_test_fp fn; } __el_test_entry;")
        emit_line("static const __el_test_entry __el_registry[] = {")
        let ri: Int = 0
        while ri < tn {
            let r_name: String = native_list_get(test_names, ri)
            let r_cfn: String = native_list_get(test_c_names, ri)
            emit_line("  { \"" + c_escape(r_name) + "\", " + r_cfn + " },")
            let ri = ri + 1
        }
        // Trailing sentinel keeps the array non-empty when a file declares no
        // tests (a zero-length array is not valid C).
        emit_line("  { 0, 0 }")
        emit_line("};")
        emit_line("static const int __el_registry_n = " + int_to_str(tn) + ";")
        emit_blank()
        emit_line("static long long __el_last_ns = 0;")
        emit_line("static int __el_opt_json_v = 0;")
        emit_blank()

        // ── Index-based accessors ────────────────────────────────────────────
        // El has no function pointers, so the runner works purely in indices.
        // This is the whole seam between generated C and the El-side runner.
        emit_line("el_val_t __el_reg_count(void) { return (el_val_t)(int64_t)__el_registry_n; }")
        emit_line("el_val_t __el_reg_name(el_val_t i) {")
        emit_line("  int64_t k = (int64_t)i;")
        emit_line("  if (k < 0 || k >= __el_registry_n) return EL_STR(\"\");")
        emit_line("  return EL_STR(__el_registry[k].name);")
        emit_line("}")
        // Timing is taken immediately around the call, in C, on the MONOTONIC
        // clock — never the wall clock, which can step backwards under NTP.
        emit_line("el_val_t __el_reg_invoke(el_val_t i) {")
        emit_line("  int64_t k = (int64_t)i;")
        emit_line("  if (k < 0 || k >= __el_registry_n) return 0;")
        emit_line("  __el_cur_fails = 0; __el_cur_asserts = 0; __el_cur_msg[0] = '\\0';")
        emit_line("  __el_cur_test = __el_registry[k].name;")
        emit_line("  struct timespec _t0, _t1;")
        emit_line("  clock_gettime(CLOCK_MONOTONIC, &_t0);")
        emit_line("  __el_registry[k].fn();")
        emit_line("  clock_gettime(CLOCK_MONOTONIC, &_t1);")
        emit_line("  __el_last_ns = (long long)(_t1.tv_sec - _t0.tv_sec) * 1000000000LL")
        emit_line("              + (long long)(_t1.tv_nsec - _t0.tv_nsec);")
        emit_line("  return (el_val_t)(int64_t)__el_cur_fails;")
        emit_line("}")
        emit_line("el_val_t __el_reg_last_ns(void) { return (el_val_t)(int64_t)__el_last_ns; }")
        emit_line("el_val_t __el_reg_msg(void) { return EL_STR(__el_cur_msg); }")
        emit_line("el_val_t __el_reg_asserts(void) { return (el_val_t)(int64_t)__el_cur_asserts; }")
        emit_line("el_val_t __el_opt_json(void) { return (el_val_t)(int64_t)__el_opt_json_v; }")
        emit_blank()

        // main() delegates to the El-side runner. Everything above this line is
        // generated glue; all reporting logic lives in runtime/eltest.el.
        state_set("__cg_current_fn", "main")
        emit_line("int main(int _argc, char **_argv) {")
        emit_line("  el_runtime_init_args(_argc, _argv);")
        emit_line("  for (int _i = 1; _i < _argc; _i++) {")
        emit_line("    if (strcmp(_argv[_i], \"--json\") == 0) __el_opt_json_v = 1;")
        emit_line("  }")
        emit_line("  return (int)(int64_t)el_test_main();")
        emit_line("}")
        el_arena_pop(test_arena_mark)
        el_release(test_names)
        el_release(test_c_names)
        return ""
    }

    // Release test tracking lists (empty in non-test mode).
    el_release(test_names)
    el_release(test_c_names)

    // Library detection: no fn main and no top-level executable stmts
    let is_library: Bool = false
    if !has_el_main {
        if !has_toplevel_exec {
            let is_library = true
        }
    }
    if is_library { return "" }

    // Emit main() — wrap in arena scope to free intermediate strings.
    let main_arena_mark: Any = el_arena_push()
    let kind2: String = state_get("__program_kind")
    state_set("__cg_current_fn", "main")
    emit_line("int main(int _argc, char** _argv) {")
    emit_line("  el_runtime_init_args(_argc, _argv);")
    // Cross-cutting concerns declared by a `program` block run BEFORE anything
    // else — a singleton violation must refuse the start before this process
    // touches a port or a data directory, and configuration must be validated
    // before the first read of it rather than at each read site.
    if prog_have {
        emit_line("  __el_program_init();")
    }

    // cgi init if needed
    let ns2: Int = native_list_len(sigs)
    let si2: Int = 0
    while si2 < ns2 {
        let sig2 = native_list_get(sigs, si2)
        let sk3: String = sig2["kind"]
        if str_eq(sk3, "cgi_block") {
            // Emit from the values captured before the declaration was released.
            // The previous implementation searched toplevel_exec_stmts, which by
            // construction never contains a declaration — so it emitted nothing and
            // said nothing. See the capture block near toplevel_exec_stmts init.
            if cgi_have {
                let arg_name2: String = "EL_STR(" + c_str_lit(cgi_name_v) + ")"
                let arg_did2: String = cgi_arg(cgi_did_v, cgi_has_did)
                let arg_prin2: String = cgi_arg(cgi_prin_v, cgi_has_prin)
                let arg_net2: String = cgi_arg(cgi_net_v, cgi_has_net)
                let arg_eng2: String = cgi_arg(cgi_eng_v, cgi_has_eng)
                emit_line("  el_cgi_init(" + arg_name2 + ", " + arg_did2 + ", " + arg_prin2 + ", " + arg_net2 + ", " + arg_eng2 + ");")
            }
        }
        let si2 = si2 + 1
    }

    // sigs fully consumed — release to free peak heap.
    el_release(sigs)

    // Seed declared set with top-level let names
    let main_decl2: [String] = native_list_empty()
    let tln: Int = native_list_len(toplevel_let_names)
    let tli: Int = 0
    while tli < tln {
        let main_decl2 = native_list_append(main_decl2, native_list_get(toplevel_let_names, tli))
        let tli = tli + 1
    }
    // toplevel_let_names fully consumed — release to free peak heap.
    el_release(toplevel_let_names)

    // Emit top-level executable stmts (lets and others) into main()
    // Per-statement arena scope mirrors el_main_body: frees intermediate strings
    // (str_concat fragments from cg_expr) after each statement, preventing O(n²)
    // accumulation when many stmts are present (e.g. from unrecognized constructs).
    let tes_n2: Int = native_list_len(toplevel_exec_stmts)
    let tes_i2: Int = 0
    while tes_i2 < tes_n2 {
        let tes2 = native_list_get(toplevel_exec_stmts, tes_i2)
        let tes_k2: String = tes2["stmt"]
        if !str_eq(tes_k2, "CgiBlock") {
            if !str_eq(tes_k2, "ServiceBlock") {
                let tes_mark: Any = el_arena_push()
                let main_decl2 = cg_stmt(tes2, "  ", main_decl2)
                el_arena_pop(tes_mark)
            }
        }
        let tes_i2 = tes_i2 + 1
    }
    // toplevel_exec_stmts fully consumed — release to free peak heap.
    el_release(toplevel_exec_stmts)

    // Emit fn main() body — per-statement arena scope frees intermediate strings.
    let mn: Int = native_list_len(el_main_body)
    let mi: Int = 0
    while mi < mn {
        let mstmt = native_list_get(el_main_body, mi)
        let stmt_mark: Any = el_arena_push()
        let main_decl2 = cg_stmt(mstmt, "  ", main_decl2)
        el_arena_pop(stmt_mark)
        let mi = mi + 1
    }
    // el_main_body and main_decl2 fully consumed — release to free peak heap.
    el_release(el_main_body)
    el_release(main_decl2)

    emit_line("  return 0;")
    emit_line("}")
    emit_blank()
    el_arena_pop(main_arena_mark)

    ""
}
