// lexer.el — el self-hosting lexer
//
// Tokenises an el source string into a list of token maps.
// Each token is a Map<String, Any> with keys:
//   "kind"  -> String  (e.g. "Int", "Ident", "Plus")
//   "value" -> String  (the raw text of the token)
//
// Entry point: fn lex(source: String) -> [Map<String, Any>]
//
// Uses native_string_chars to split the source into a chars list,
// then indexes it with native_list_get — avoids O(N²) string cloning.

// ── Character helpers ─────────────────────────────────────────────────────────

fn lex_is_digit(ch: String) -> Bool {
    if ch == "0" { return true }
    if ch == "1" { return true }
    if ch == "2" { return true }
    if ch == "3" { return true }
    if ch == "4" { return true }
    if ch == "5" { return true }
    if ch == "6" { return true }
    if ch == "7" { return true }
    if ch == "8" { return true }
    if ch == "9" { return true }
    false
}

fn lex_is_alpha(ch: String) -> Bool {
    if ch == "a" { return true }
    if ch == "b" { return true }
    if ch == "c" { return true }
    if ch == "d" { return true }
    if ch == "e" { return true }
    if ch == "f" { return true }
    if ch == "g" { return true }
    if ch == "h" { return true }
    if ch == "i" { return true }
    if ch == "j" { return true }
    if ch == "k" { return true }
    if ch == "l" { return true }
    if ch == "m" { return true }
    if ch == "n" { return true }
    if ch == "o" { return true }
    if ch == "p" { return true }
    if ch == "q" { return true }
    if ch == "r" { return true }
    if ch == "s" { return true }
    if ch == "t" { return true }
    if ch == "u" { return true }
    if ch == "v" { return true }
    if ch == "w" { return true }
    if ch == "x" { return true }
    if ch == "y" { return true }
    if ch == "z" { return true }
    if ch == "A" { return true }
    if ch == "B" { return true }
    if ch == "C" { return true }
    if ch == "D" { return true }
    if ch == "E" { return true }
    if ch == "F" { return true }
    if ch == "G" { return true }
    if ch == "H" { return true }
    if ch == "I" { return true }
    if ch == "J" { return true }
    if ch == "K" { return true }
    if ch == "L" { return true }
    if ch == "M" { return true }
    if ch == "N" { return true }
    if ch == "O" { return true }
    if ch == "P" { return true }
    if ch == "Q" { return true }
    if ch == "R" { return true }
    if ch == "S" { return true }
    if ch == "T" { return true }
    if ch == "U" { return true }
    if ch == "V" { return true }
    if ch == "W" { return true }
    if ch == "X" { return true }
    if ch == "Y" { return true }
    if ch == "Z" { return true }
    false
}

fn is_alnum_or_underscore(ch: String) -> Bool {
    if lex_is_digit(ch) { return true }
    if lex_is_alpha(ch) { return true }
    if ch == "_" { return true }
    false
}

fn lex_is_whitespace(ch: String) -> Bool {
    if ch == " " { return true }
    if ch == "\t" { return true }
    if ch == "\n" { return true }
    if ch == "\r" { return true }
    false
}

fn make_tok(kind: String, value: String) -> Map<String, Any> {
    { "kind": kind, "value": value }
}

// ── Keyword lookup ────────────────────────────────────────────────────────────

fn keyword_kind(word: String) -> String {
    if word == "let" { return "Let" }
    if word == "fn" { return "Fn" }
    if word == "type" { return "Type" }
    if word == "enum" { return "Enum" }
    if word == "match" { return "Match" }
    if word == "return" { return "Return" }
    if word == "if" { return "If" }
    if word == "else" { return "Else" }
    if word == "for" { return "For" }
    if word == "in" { return "In" }
    if word == "while" { return "While" }
    if word == "import" { return "Import" }
    if word == "from" { return "From" }
    if word == "as" { return "As" }
    if word == "with" { return "With" }
    if word == "sealed" { return "Sealed" }
    if word == "activate" { return "Activate" }
    if word == "where" { return "Where" }
    if word == "test" { return "Test" }
    if word == "seed" { return "Seed" }
    if word == "assert" { return "Assert" }
    if word == "protocol" { return "Protocol" }
    if word == "impl" { return "Impl" }
    if word == "retry" { return "Retry" }
    if word == "times" { return "Times" }
    if word == "fallback" { return "Fallback" }
    if word == "reason" { return "Reason" }
    if word == "parallel" { return "Parallel" }
    if word == "trace" { return "Trace" }
    if word == "requires" { return "Requires" }
    if word == "deploy" { return "Deploy" }
    if word == "to" { return "To" }
    if word == "via" { return "Via" }
    if word == "target" { return "Target" }
    if word == "true" { return "Bool" }
    if word == "false" { return "Bool" }
    if word == "cgi" { return "Cgi" }
    if word == "service" { return "Service" }
    if word == "manager" { return "Manager" }
    if word == "engine" { return "Engine" }
    if word == "accessor" { return "Accessor" }
    if word == "vessel" { return "Vessel" }
    ""
}

// ── Scan helpers ──────────────────────────────────────────────────────────────
// All scan helpers receive the chars list and total length.

// scan_digits — advance i while chars[i] is a digit
// Returns { "text": ..., "pos": i }
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
    let i = start
    let text = ""
    let running = true
    while running {
        if i >= total {
            let running = false
        } else {
            let ch: String = native_list_get(chars, i)
            if lex_is_digit(ch) {
                let text = text + ch
                let i = i + 1
            } else {
                let running = false
            }
        }
    }
    { "text": text, "pos": i }
}

// scan_ident — advance i while chars[i] is alphanumeric or underscore
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
    let i = start
    let text = ""
    let running = true
    while running {
        if i >= total {
            let running = false
        } else {
            let ch: String = native_list_get(chars, i)
            if is_alnum_or_underscore(ch) {
                let text = text + ch
                let i = i + 1
            } else {
                let running = false
            }
        }
    }
    { "text": text, "pos": i }
}

// ── Code-bearing string detection + comment strip ────────────────────────────
// Inline JS/CSS literals embedded in El source (e.g. <script>…</script> blobs
// or stylesheet payloads inside string literals) carry their own line and
// block comments. Those comments leak into the served HTML and reveal build
// notes the visitor should never see. We strip them at the lexer so every
// downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form.
//
// looks_like_code — heuristic gate so we only strip strings that actually
// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim.

fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
    let nchars: [String] = native_string_chars(needle)
    let nlen: Int = native_list_len(nchars)
    if start + nlen > total { return false }
    let i = 0
    let matched = true
    while i < nlen {
        let a: String = native_list_get(chars, start + i)
        let b: String = native_list_get(nchars, i)
        if a == b { let i = i + 1 } else { let matched = false; let i = nlen }
    }
    matched
}

fn str_has(s: String, needle: String) -> Bool {
    let chars: [String] = native_string_chars(s)
    let total: Int = native_list_len(chars)
    let i = 0
    let found = false
    while i < total {
        if substr_at(chars, i, total, needle) {
            let found = true
            let i = total
        } else {
            let i = i + 1
        }
    }
    found
}

fn looks_like_code(s: String) -> Bool {
    if str_has(s, "<script") { return true }
    if str_has(s, "<style") { return true }
    if str_has(s, "function") {
        if str_has(s, ";") { return true }
    }
    false
}

// strip_code_comments — character-by-character walk. Tracks JS string state
// (single, double, backtick) and never strips inside one. Backslash escapes
// inside JS strings consume the next char verbatim. URLs like https:// are
// preserved by checking the previous char before treating // as a line
// comment opener: if the char immediately before '/' is ':', emit the '/'
// literally and advance one position.
fn strip_code_comments(s: String) -> String {
    let chars: [String] = native_string_chars(s)
    let total: Int = native_list_len(chars)
    let out = ""
    let i = 0
    let in_squote = false
    let in_dquote = false
    let in_btick = false
    let prev = ""
    while i < total {
        let ch: String = native_list_get(chars, i)
        let in_js_string = false
        if in_squote { let in_js_string = true }
        if in_dquote { let in_js_string = true }
        if in_btick { let in_js_string = true }

        if in_js_string {
            // Backslash escape: consume next char verbatim regardless of which.
            if ch == "\\" {
                let out = out + ch
                let next_i = i + 1
                if next_i < total {
                    let nc: String = native_list_get(chars, next_i)
                    let out = out + nc
                    let prev = nc
                    let i = next_i + 1
                } else {
                    let prev = ch
                    let i = next_i
                }
            } else {
                if in_squote {
                    if ch == "'" { let in_squote = false }
                } else {
                    if in_dquote {
                        if ch == "\"" { let in_dquote = false }
                    } else {
                        if in_btick {
                            if ch == "`" { let in_btick = false }
                        }
                    }
                }
                let out = out + ch
                let prev = ch
                let i = i + 1
            }
        } else {
            // Not in a JS string. Check for comment openers.
            let next_i = i + 1
            let next_ch = ""
            if next_i < total {
                let next_ch: String = native_list_get(chars, next_i)
            }

            if ch == "/" {
                if next_ch == "/" {
                    // URL guard: prev char ':' means this is "://", not a comment.
                    if prev == ":" {
                        let out = out + ch
                        let prev = ch
                        let i = i + 1
                    } else {
                        // Skip until newline (newline itself is preserved so
                        // surrounding line counts/structure stay sane).
                        let i = i + 2
                        let scanning = true
                        while scanning {
                            if i >= total {
                                let scanning = false
                            } else {
                                let lc: String = native_list_get(chars, i)
                                if lc == "\n" {
                                    let scanning = false
                                } else {
                                    let i = i + 1
                                }
                            }
                        }
                        let prev = ""
                    }
                } else {
                    if next_ch == "*" {
                        // Skip until matching "*/".
                        let i = i + 2
                        let scanning2 = true
                        while scanning2 {
                            if i >= total {
                                let scanning2 = false
                            } else {
                                let bc: String = native_list_get(chars, i)
                                if bc == "*" {
                                    let after = i + 1
                                    if after < total {
                                        let nc2: String = native_list_get(chars, after)
                                        if nc2 == "/" {
                                            let i = after + 1
                                            let scanning2 = false
                                        } else {
                                            let i = i + 1
                                        }
                                    } else {
                                        let i = i + 1
                                    }
                                } else {
                                    let i = i + 1
                                }
                            }
                        }
                        let prev = ""
                    } else {
                        let out = out + ch
                        let prev = ch
                        let i = i + 1
                    }
                }
            } else {
                // Open a JS string?
                if ch == "'" {
                    let in_squote = true
                    let out = out + ch
                    let prev = ch
                    let i = i + 1
                } else {
                    if ch == "\"" {
                        let in_dquote = true
                        let out = out + ch
                        let prev = ch
                        let i = i + 1
                    } else {
                        if ch == "`" {
                            let in_btick = true
                            let out = out + ch
                            let prev = ch
                            let i = i + 1
                        } else {
                            let out = out + ch
                            let prev = ch
                            let i = i + 1
                        }
                    }
                }
            }
        }
    }
    out
}

// scan_string — scan a quoted string literal, handling \" escapes.
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
    let i = start
    let text = ""
    let running = true
    while running {
        if i >= total {
            let running = false
        } else {
            let ch: String = native_list_get(chars, i)
            if ch == "\\" {
                // escape: peek next char
                let next_i = i + 1
                if next_i < total {
                    let next_ch: String = native_list_get(chars, next_i)
                    if next_ch == "\"" {
                        let text = text + "\""
                        let i = next_i + 1
                    } else {
                        if next_ch == "n" {
                            let text = text + "\n"
                            let i = next_i + 1
                        } else {
                            if next_ch == "t" {
                                let text = text + "\t"
                                let i = next_i + 1
                            } else {
                                if next_ch == "r" {
                                    let text = text + "\r"
                                    let i = next_i + 1
                                } else {
                                    if next_ch == "\\" {
                                        let text = text + "\\"
                                        let i = next_i + 1
                                    } else {
                                        let text = text + next_ch
                                        let i = next_i + 1
                                    }
                                }
                            }
                        }
                    }
                } else {
                    let i = i + 1
                }
            } else {
                if ch == "\"" {
                    let i = i + 1
                    let running = false
                } else {
                    let text = text + ch
                    let i = i + 1
                }
            }
        }
    }
    { "text": text, "pos": i }
}

// ── Main lexer ────────────────────────────────────────────────────────────────

fn lex(source: String) -> [Map<String, Any>] {
    let chars: [String] = native_string_chars(source)
    let total: Int = native_list_len(chars)
    let tokens: [Map<String, Any>] = native_list_empty()
    let i: Int = 0

    while i < total {
        let ch: String = native_list_get(chars, i)

        // Skip whitespace
        if lex_is_whitespace(ch) {
            let i = i + 1
        } else {
            // Line comments: //
            if ch == "/" {
                let next_i = i + 1
                if next_i < total {
                    let next_ch: String = native_list_get(chars, next_i)
                    if next_ch == "/" {
                        // skip to end of line
                        let i = i + 2
                        let running2 = true
                        while running2 {
                            if i >= total {
                                let running2 = false
                            } else {
                                let lch: String = native_list_get(chars, i)
                                if lch == "\n" {
                                    let running2 = false
                                } else {
                                    let i = i + 1
                                }
                            }
                        }
                    } else {
                        let tokens = native_list_append(tokens, make_tok("Slash", "/"))
                        let i = i + 1
                    }
                } else {
                    let tokens = native_list_append(tokens, make_tok("Slash", "/"))
                    let i = i + 1
                }
            } else {
                // String literal
                if ch == "\"" {
                    let result = scan_string(chars, i + 1, total)
                    let str_text: String = result["text"]
                    let new_pos: Int = result["pos"]
                    // Compile-time scrub: strings that embed JS or CSS get
                    // their // line comments and /* block comments stripped
                    // before the token reaches the parser. Plain prose passes
                    // through untouched.
                    let clean_text = str_text
                    if looks_like_code(str_text) {
                        let clean_text = strip_code_comments(str_text)
                    }
                    let tokens = native_list_append(tokens, make_tok("Str", clean_text))
                    let i = new_pos
                } else {
                    // Number literal
                    if lex_is_digit(ch) {
                        let result = scan_digits(chars, i, total)
                        let num_text: String = result["text"]
                        let new_pos: Int = result["pos"]
                        // check for float (dot followed by digit)
                        if new_pos < total {
                            let dot_ch: String = native_list_get(chars, new_pos)
                            if dot_ch == "." {
                                let after_dot = new_pos + 1
                                if after_dot < total {
                                    let after_dot_ch: String = native_list_get(chars, after_dot)
                                    if lex_is_digit(after_dot_ch) {
                                        let frac_result = scan_digits(chars, after_dot, total)
                                        let frac_text: String = frac_result["text"]
                                        let frac_pos: Int = frac_result["pos"]
                                        let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text))
                                        let i = frac_pos
                                    } else {
                                        let tokens = native_list_append(tokens, make_tok("Int", num_text))
                                        let i = new_pos
                                    }
                                } else {
                                    let tokens = native_list_append(tokens, make_tok("Int", num_text))
                                    let i = new_pos
                                }
                            } else {
                                let tokens = native_list_append(tokens, make_tok("Int", num_text))
                                let i = new_pos
                            }
                        } else {
                            let tokens = native_list_append(tokens, make_tok("Int", num_text))
                            let i = new_pos
                        }
                    } else {
                        // Identifier or keyword
                        if lex_is_alpha(ch) || ch == "_" {
                            let result = scan_ident(chars, i, total)
                            let word: String = result["text"]
                            let new_pos: Int = result["pos"]
                            let kw = keyword_kind(word)
                            if kw == "" {
                                let tokens = native_list_append(tokens, make_tok("Ident", word))
                            } else {
                                let tokens = native_list_append(tokens, make_tok(kw, word))
                            }
                            let i = new_pos
                        } else {
                            // Multi-char and single-char operators/delimiters
                            let peek_i = i + 1
                            let peek_ch = ""
                            if peek_i < total {
                                let peek_ch: String = native_list_get(chars, peek_i)
                            }

                            if ch == "=" {
                                if peek_ch == "=" {
                                    let tokens = native_list_append(tokens, make_tok("EqEq", "=="))
                                    let i = i + 2
                                } else {
                                    if peek_ch == ">" {
                                        let tokens = native_list_append(tokens, make_tok("FatArrow", "=>"))
                                        let i = i + 2
                                    } else {
                                        let tokens = native_list_append(tokens, make_tok("Eq", "="))
                                        let i = i + 1
                                    }
                                }
                            } else {
                                if ch == "!" {
                                    if peek_ch == "=" {
                                        let tokens = native_list_append(tokens, make_tok("NotEq", "!="))
                                        let i = i + 2
                                    } else {
                                        let tokens = native_list_append(tokens, make_tok("Not", "!"))
                                        let i = i + 1
                                    }
                                } else {
                                    if ch == "<" {
                                        if peek_ch == "=" {
                                            let tokens = native_list_append(tokens, make_tok("LtEq", "<="))
                                            let i = i + 2
                                        } else {
                                            let tokens = native_list_append(tokens, make_tok("Lt", "<"))
                                            let i = i + 1
                                        }
                                    } else {
                                        if ch == ">" {
                                            if peek_ch == "=" {
                                                let tokens = native_list_append(tokens, make_tok("GtEq", ">="))
                                                let i = i + 2
                                            } else {
                                                let tokens = native_list_append(tokens, make_tok("Gt", ">"))
                                                let i = i + 1
                                            }
                                        } else {
                                            if ch == "&" {
                                                if peek_ch == "&" {
                                                    let tokens = native_list_append(tokens, make_tok("And", "&&"))
                                                    let i = i + 2
                                                } else {
                                                    let i = i + 1
                                                }
                                            } else {
                                                if ch == "|" {
                                                    if peek_ch == "|" {
                                                        let tokens = native_list_append(tokens, make_tok("Or", "||"))
                                                        let i = i + 2
                                                    } else {
                                                        if peek_ch == ">" {
                                                            let tokens = native_list_append(tokens, make_tok("PipeOp", "|>"))
                                                            let i = i + 2
                                                        } else {
                                                            let tokens = native_list_append(tokens, make_tok("Pipe", "|"))
                                                            let i = i + 1
                                                        }
                                                    }
                                                } else {
                                                    if ch == "-" {
                                                        if peek_ch == ">" {
                                                            let tokens = native_list_append(tokens, make_tok("Arrow", "->"))
                                                            let i = i + 2
                                                        } else {
                                                            let tokens = native_list_append(tokens, make_tok("Minus", "-"))
                                                            let i = i + 1
                                                        }
                                                    } else {
                                                        if ch == ":" {
                                                            if peek_ch == ":" {
                                                                let tokens = native_list_append(tokens, make_tok("ColonColon", "::"))
                                                                let i = i + 2
                                                            } else {
                                                                let tokens = native_list_append(tokens, make_tok("Colon", ":"))
                                                                let i = i + 1
                                                            }
                                                        } else {
                                                            if ch == "+" {
                                                                let tokens = native_list_append(tokens, make_tok("Plus", "+"))
                                                                let i = i + 1
                                                            } else {
                                                                if ch == "*" {
                                                                    let tokens = native_list_append(tokens, make_tok("Star", "*"))
                                                                    let i = i + 1
                                                                } else {
                                                                if ch == "%" {
                                                                    let tokens = native_list_append(tokens, make_tok("Percent", "%"))
                                                                    let i = i + 1
                                                                } else {
                                                                    if ch == "(" {
                                                                        let tokens = native_list_append(tokens, make_tok("LParen", "("))
                                                                        let i = i + 1
                                                                    } else {
                                                                        if ch == ")" {
                                                                            let tokens = native_list_append(tokens, make_tok("RParen", ")"))
                                                                            let i = i + 1
                                                                        } else {
                                                                            if ch == "{" {
                                                                                let tokens = native_list_append(tokens, make_tok("LBrace", "{"))
                                                                                let i = i + 1
                                                                            } else {
                                                                                if ch == "}" {
                                                                                    let tokens = native_list_append(tokens, make_tok("RBrace", "}"))
                                                                                    let i = i + 1
                                                                                } else {
                                                                                    if ch == "[" {
                                                                                        let tokens = native_list_append(tokens, make_tok("LBracket", "["))
                                                                                        let i = i + 1
                                                                                    } else {
                                                                                        if ch == "]" {
                                                                                            let tokens = native_list_append(tokens, make_tok("RBracket", "]"))
                                                                                            let i = i + 1
                                                                                        } else {
                                                                                            if ch == "," {
                                                                                                let tokens = native_list_append(tokens, make_tok("Comma", ","))
                                                                                                let i = i + 1
                                                                                            } else {
                                                                                                if ch == "." {
                                                                                                    let tokens = native_list_append(tokens, make_tok("Dot", "."))
                                                                                                    let i = i + 1
                                                                                                } else {
                                                                                                    if ch == ";" {
                                                                                                        let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
                                                                                                        let i = i + 1
                                                                                                    } else {
                                                                                                        if ch == "@" {
                                                                                                            let tokens = native_list_append(tokens, make_tok("At", "@"))
                                                                                                            let i = i + 1
                                                                                                        } else {
                                                                                                            if ch == "?" {
                                                                                                                let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
                                                                                                                let i = i + 1
                                                                                                            } else {
                                                                                                                // unknown char — skip
                                                                                                                let i = i + 1
                                                                                                            }
                                                                                                        }
                                                                                                    }
                                                                                                }
                                                                                            }
                                                                                        }
                                                                                    }
                                                                                }
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    let tokens = native_list_append(tokens, make_tok("Eof", ""))
    tokens
}
// parser.el — el self-hosting recursive descent parser
//
// Consumes the token list produced by lexer.el and builds a list of AST
// statement maps.  Each statement and expression is a Map<String, Any>.
//
// The cursor (integer position into the token list) is threaded through every
// parse function.  Functions return { "node": <map>, "pos": <int> }.
//
// The token list is passed as a parameter to all parse functions.
// native_list_get is used to index into it without cloning.
//
// Entry point: fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>]

// ── Token access helpers ──────────────────────────────────────────────────────

fn tok_at(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    native_list_get(tokens, pos)
}

fn tok_kind(tokens: [Map<String, Any>], pos: Int) -> String {
    let t = native_list_get(tokens, pos)
    t["kind"]
}

fn tok_value(tokens: [Map<String, Any>], pos: Int) -> String {
    let t = native_list_get(tokens, pos)
    t["value"]
}

fn expect(tokens: [Map<String, Any>], pos: Int, kind: String) -> Int {
    let k = tok_kind(tokens, pos)
    if k == kind {
        return pos + 1
    }
    // On mismatch just advance; error recovery is best-effort
    pos + 1
}

// ── Result helpers ────────────────────────────────────────────────────────────

fn make_result(node: Map<String, Any>, pos: Int) -> Map<String, Any> {
    { "node": node, "pos": pos }
}

// ── Type annotation parser ────────────────────────────────────────────────────
// Skips over a type annotation, returning the new position.
// Types can be: Ident, [Type], Map<K,V>, Type?, Type<Type,...>

fn skip_type(tokens: [Map<String, Any>], pos: Int) -> Int {
    let k = tok_kind(tokens, pos)
    // Array type: [Type]
    if k == "LBracket" {
        let p = pos + 1
        let p = skip_type(tokens, p)
        let p = expect(tokens, p, "RBracket")
        return p
    }
    // Named type (possibly generic)
    if k == "Ident" {
        let p = pos + 1
        let k2 = tok_kind(tokens, p)
        if k2 == "Lt" {
            // Generic params: skip until matching >
            let p = p + 1
            let depth = 1
            let running = true
            while running {
                let kk = tok_kind(tokens, p)
                if kk == "Eof" {
                    let running = false
                } else {
                    if kk == "Lt" {
                        let depth = depth + 1
                        let p = p + 1
                    } else {
                        if kk == "Gt" {
                            let depth = depth - 1
                            let p = p + 1
                            if depth <= 0 {
                                let running = false
                            }
                        } else {
                            let p = p + 1
                        }
                    }
                }
            }
            let k3 = tok_kind(tokens, p)
            if k3 == "QuestionMark" {
                let p = p + 1
            }
            return p
        }
        // Optional marker
        if k2 == "QuestionMark" {
            return p + 1
        }
        return p
    }
    pos + 1
}

// ── Parameter list ────────────────────────────────────────────────────────────
// Parses (name: Type, name: Type, ...) — returns { "params": [...], "pos": ... }

fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    let p = expect(tokens, pos, "LParen")
    let params: [Map<String, Any>] = native_list_empty()
    let running = true
    while running {
        let k = tok_kind(tokens, p)
        if k == "RParen" {
            let running = false
        } else {
            if k == "Eof" {
                let running = false
            } else {
                // param name
                let pname = tok_value(tokens, p)
                let p = p + 1
                let p = expect(tokens, p, "Colon")
                // Capture the leading type identifier so codegen can dispatch
                // arithmetic vs string-concat on `+` based on declared types.
                let ptype = ""
                let kt = tok_kind(tokens, p)
                if kt == "Ident" {
                    let ptype = tok_value(tokens, p)
                }
                let p = skip_type(tokens, p)
                let param = { "name": pname, "type": ptype }
                let params = native_list_append(params, param)
                let k2 = tok_kind(tokens, p)
                if k2 == "Comma" {
                    let p = p + 1
                }
            }
        }
    }
    let p = expect(tokens, p, "RParen")
    { "params": params, "pos": p }
}

// ── Expression parsing ────────────────────────────────────────────────────────

fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    let k = tok_kind(tokens, pos)
    let v = tok_value(tokens, pos)

    // Integer literal
    if k == "Int" {
        return make_result({ "expr": "Int", "value": v }, pos + 1)
    }

    // Float literal
    if k == "Float" {
        return make_result({ "expr": "Float", "value": v }, pos + 1)
    }

    // String literal
    if k == "Str" {
        return make_result({ "expr": "Str", "value": v }, pos + 1)
    }

    // Bool literal
    if k == "Bool" {
        return make_result({ "expr": "Bool", "value": v }, pos + 1)
    }

    // Identifier
    if k == "Ident" {
        return make_result({ "expr": "Ident", "name": v }, pos + 1)
    }

    // Grouped expression
    if k == "LParen" {
        let r = parse_expr(tokens, pos + 1)
        let node = r["node"]
        let p = r["pos"]
        let p = expect(tokens, p, "RParen")
        return make_result(node, p)
    }

    // Array literal: [e1, e2, ...]
    if k == "LBracket" {
        let p = pos + 1
        let elems: [Map<String, Any>] = native_list_empty()
        let running = true
        while running {
            let k2 = tok_kind(tokens, p)
            if k2 == "RBracket" {
                let running = false
            } else {
                if k2 == "Eof" {
                    let running = false
                } else {
                    let r = parse_expr(tokens, p)
                    let elem = r["node"]
                    let p = r["pos"]
                    let elems = native_list_append(elems, elem)
                    let k3 = tok_kind(tokens, p)
                    if k3 == "Comma" {
                        let p = p + 1
                    }
                }
            }
        }
        let p = expect(tokens, p, "RBracket")
        return make_result({ "expr": "Array", "elems": elems }, p)
    }

    // Map literal: { "key": val, ... }
    //
    // Suppression: when parse_if / parse_while / parse_for / parse_match
    // are parsing a head expression, they set __no_block_expr=1 so a stray
    // `{` here doesn't get gobbled as a Map literal — it belongs to the
    // following block. Without this, `if a || b { ... }` could mis-parse
    // (the `||` recursion lands at `{` and tries to read the if-body as a
    // Map, then loops forever when keys don't match `Str: expr`).
    if k == "LBrace" {
        let no_block: String = state_get("__no_block_expr")
        if str_eq(no_block, "1") {
            // Fall through to fallback — caller will see `{` and treat it
            // as the start of the block they're expecting.
            return make_result({ "expr": "Nil" }, pos)
        }
        let p = pos + 1
        let pairs: [Map<String, Any>] = native_list_empty()
        let running = true
        while running {
            let k2 = tok_kind(tokens, p)
            if k2 == "RBrace" {
                let running = false
            } else {
                if k2 == "Eof" {
                    let running = false
                } else {
                    // key: Str token
                    let key = tok_value(tokens, p)
                    let new_p: Int = p + 1
                    let new_p = expect(tokens, new_p, "Colon")
                    let r = parse_expr(tokens, new_p)
                    let val_node = r["node"]
                    let new_p = r["pos"]
                    let pair = { "key": key, "value": val_node }
                    let pairs = native_list_append(pairs, pair)
                    let k3 = tok_kind(tokens, new_p)
                    if k3 == "Comma" {
                        let new_p = new_p + 1
                    }
                    // Non-progress guard: malformed map content can leave
                    // parse_expr returning the same pos. Force advance.
                    if new_p <= p {
                        let p = p + 1
                    } else {
                        let p = new_p
                    }
                }
            }
        }
        let p = expect(tokens, p, "RBrace")
        return make_result({ "expr": "Map", "pairs": pairs }, p)
    }

    // if expression
    if k == "If" {
        let r = parse_if(tokens, pos)
        return r
    }

    // match expression
    if k == "Match" {
        let r = parse_match(tokens, pos)
        return r
    }

    // for expression (used as statement)
    if k == "For" {
        let r = parse_for_expr(tokens, pos)
        return r
    }

    // Unary not
    if k == "Not" {
        let r = parse_primary(tokens, pos + 1)
        let inner = r["node"]
        let p = r["pos"]
        return make_result({ "expr": "Not", "inner": inner }, p)
    }

    // Unary minus
    if k == "Minus" {
        let r = parse_primary(tokens, pos + 1)
        let inner = r["node"]
        let p = r["pos"]
        return make_result({ "expr": "Neg", "inner": inner }, p)
    }

    // Soft keywords usable as identifiers in expression position. The lexer
    // turns words like `target`, `to`, `via`, `deploy`, etc. into dedicated
    // token kinds for the deploy/retry DSLs, but they're also valid as
    // parameter names and local variables. When one of these appears in
    // expression position (where only an Ident makes sense), treat it as
    // an Ident carrying the original text — otherwise references to a
    // parameter named `target` compile to EL_NULL.
    if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Via" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Deploy" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Reason" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Times" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Fallback" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Retry" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Parallel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Trace" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Requires" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Where" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "As" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "With" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Manager" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Engine" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Accessor" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
    if k == "Vessel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }

    // Fallback: skip unknown token
    make_result({ "expr": "Nil" }, pos + 1)
}

fn parse_if(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    let p = expect(tokens, pos, "If")
    // Suppress Map-literal parsing in the cond so a stray `{` (the start
    // of the then-block) isn't gobbled as a Map.
    let prev_no_block: String = state_get("__no_block_expr")
    state_set("__no_block_expr", "1")
    let r = parse_expr(tokens, p)
    state_set("__no_block_expr", prev_no_block)
    let cond = r["node"]
    let p = r["pos"]
    let r2 = parse_block(tokens, p)
    let then_stmts = r2["stmts"]
    let p = r2["pos"]
    let has_else = false
    let else_stmts: [Map<String, Any>] = native_list_empty()
    let k2 = tok_kind(tokens, p)
    if k2 == "Else" {
        let p = p + 1
        let k3 = tok_kind(tokens, p)
        if k3 == "If" {
            // else-if chain: parse as nested if
            let r3 = parse_if(tokens, p)
            let nested = r3["node"]
            let p = r3["pos"]
            let else_stmts = native_list_append(else_stmts, { "stmt": "Expr", "value": nested })
            let has_else = true
        } else {
            let r3 = parse_block(tokens, p)
            let else_stmts = r3["stmts"]
            let p = r3["pos"]
            let has_else = true
        }
    }
    make_result({ "expr": "If", "cond": cond, "then": then_stmts, "else": else_stmts, "has_else": has_else }, p)
}

fn parse_match(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    let p = expect(tokens, pos, "Match")
    let prev_no_block: String = state_get("__no_block_expr")
    state_set("__no_block_expr", "1")
    let r = parse_expr(tokens, p)
    state_set("__no_block_expr", prev_no_block)
    let subject = r["node"]
    let p = r["pos"]
    let p = expect(tokens, p, "LBrace")
    let arms: [Map<String, Any>] = native_list_empty()
    let running = true
    while running {
        let k = tok_kind(tokens, p)
        if k == "RBrace" {
            let running = false
        } else {
            if k == "Eof" {
                let running = false
            } else {
                // parse pattern => body
                let r2 = parse_pattern(tokens, p)
                let pattern = r2["node"]
                let p = r2["pos"]
                let p = expect(tokens, p, "FatArrow")
                let r3 = parse_expr(tokens, p)
                let body = r3["node"]
                let p = r3["pos"]
                let arm = { "pattern": pattern, "body": body }
                let arms = native_list_append(arms, arm)
                let k2 = tok_kind(tokens, p)
                if k2 == "Comma" {
                    let p = p + 1
                }
            }
        }
    }
    let p = expect(tokens, p, "RBrace")
    make_result({ "expr": "Match", "subject": subject, "arms": arms }, p)
}

fn parse_pattern(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    let k = tok_kind(tokens, pos)
    if k == "Ident" {
        let v = tok_value(tokens, pos)
        if v == "_" {
            return make_result({ "pattern": "Wildcard" }, pos + 1)
        }
        return make_result({ "pattern": "Binding", "name": v }, pos + 1)
    }
    if k == "Int" {
        return make_result({ "pattern": "LitInt", "value": tok_value(tokens, pos) }, pos + 1)
    }
    if k == "Str" {
        return make_result({ "pattern": "LitStr", "value": tok_value(tokens, pos) }, pos + 1)
    }
    if k == "Bool" {
        return make_result({ "pattern": "LitBool", "value": tok_value(tokens, pos) }, pos + 1)
    }
    // Wildcard _
    make_result({ "pattern": "Wildcard" }, pos + 1)
}

fn parse_for_expr(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    let p = expect(tokens, pos, "For")
    let item_name = tok_value(tokens, p)
    let p = p + 1
    let p = expect(tokens, p, "In")
    let prev_no_block: String = state_get("__no_block_expr")
    state_set("__no_block_expr", "1")
    let r = parse_expr(tokens, p)
    state_set("__no_block_expr", prev_no_block)
    let list_expr = r["node"]
    let p = r["pos"]
    let r2 = parse_block(tokens, p)
    let body = r2["stmts"]
    let p = r2["pos"]
    make_result({ "expr": "For", "item": item_name, "list": list_expr, "body": body }, p)
}

fn parse_block(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    let p = expect(tokens, pos, "LBrace")
    let stmts: [Map<String, Any>] = native_list_empty()
    let running = true
    while running {
        let k = tok_kind(tokens, p)
        if k == "RBrace" {
            let running = false
        } else {
            if k == "Eof" {
                let running = false
            } else {
                let r = parse_stmt(tokens, p)
                let stmt = r["node"]
                let new_p: Int = r["pos"]
                let stmts = native_list_append(stmts, stmt)
                // Non-progress guard: a malformed input (e.g. `||` that
                // dragged the parser into Map-literal mode partway through
                // the if-body) can leave parse_stmt returning the same pos.
                // Force advance so the outer compile doesn't hang.
                if new_p <= p {
                    let p = p + 1
                } else {
                    let p = new_p
                }
            }
        }
    }
    let p = expect(tokens, p, "RBrace")
    { "stmts": stmts, "pos": p }
}

// ── Postfix expressions (calls, field access, index) ─────────────────────────

// is_duration_unit — recognise the postfix unit suffix on a numeric literal.
// Used by parse_postfix to detect `30.seconds`-shape time literals before
// falling back to the generic `obj.field` field-access lowering. Singular
// and plural forms map to the same nanosecond multiplier; codegen does the
// arithmetic at compile time.
fn is_duration_unit(name: String) -> Bool {
    if name == "nanos" { return true }
    if name == "nano" { return true }
    if name == "millis" { return true }
    if name == "milli" { return true }
    if name == "millisecond" { return true }
    if name == "milliseconds" { return true }
    if name == "second" { return true }
    if name == "seconds" { return true }
    if name == "minute" { return true }
    if name == "minutes" { return true }
    if name == "hour" { return true }
    if name == "hours" { return true }
    if name == "day" { return true }
    if name == "days" { return true }
    false
}

fn parse_postfix(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    let r = parse_primary(tokens, pos)
    let node = r["node"]
    let p = r["pos"]

    // Postfix duration literal: `<Int>.<unit>` where <unit> is one of
    // nanos | millis | seconds | minutes | hours | days (each with an
    // optional plural). We recognise this before the generic Dot-as-field
    // path so `30.seconds` lowers to a DurationLit AST node carrying the
    // count and the unit, not a field access on an Int.
    let primary_kind: String = node["expr"]
    if primary_kind == "Int" {
        let dot_kind = tok_kind(tokens, p)
        if dot_kind == "Dot" {
            let unit_kind = tok_kind(tokens, p + 1)
            if unit_kind == "Ident" {
                let unit_name = tok_value(tokens, p + 1)
                if is_duration_unit(unit_name) {
                    let count_str: String = node["value"]
                    let node = { "expr": "DurationLit", "count": count_str, "unit": unit_name }
                    let p = p + 2
                }
            }
        }
    }

    let running = true
    while running {
        let k = tok_kind(tokens, p)
        if k == "LParen" {
            // function call
            let p = p + 1
            let args: [Map<String, Any>] = native_list_empty()
            let run2 = true
            while run2 {
                let k2 = tok_kind(tokens, p)
                if k2 == "RParen" {
                    let run2 = false
                } else {
                    if k2 == "Eof" {
                        let run2 = false
                    } else {
                        let r2 = parse_expr(tokens, p)
                        let arg = r2["node"]
                        let p = r2["pos"]
                        let args = native_list_append(args, arg)
                        let k3 = tok_kind(tokens, p)
                        if k3 == "Comma" {
                            let p = p + 1
                        }
                    }
                }
            }
            let p = expect(tokens, p, "RParen")
            let node = { "expr": "Call", "func": node, "args": args }
        } else {
            if k == "Dot" {
                let field = tok_value(tokens, p + 1)
                let p = p + 2
                let node = { "expr": "Field", "object": node, "field": field }
            } else {
                if k == "LBracket" {
                    let r2 = parse_expr(tokens, p + 1)
                    let idx = r2["node"]
                    let p = r2["pos"]
                    let p = expect(tokens, p, "RBracket")
                    let node = { "expr": "Index", "object": node, "index": idx }
                } else {
                    if k == "QuestionMark" {
                        let p = p + 1
                        let node = { "expr": "Try", "inner": node }
                    } else {
                        let running = false
                    }
                }
            }
        }
    }
    make_result(node, p)
}

// ── Binary expression precedence climbing ────────────────────────────────────

fn op_precedence(kind: String) -> Int {
    if kind == "Or" { return 1 }
    if kind == "And" { return 2 }
    if kind == "EqEq" { return 3 }
    if kind == "NotEq" { return 3 }
    if kind == "Lt" { return 4 }
    if kind == "Gt" { return 4 }
    if kind == "LtEq" { return 4 }
    if kind == "GtEq" { return 4 }
    if kind == "Plus" { return 5 }
    if kind == "Minus" { return 5 }
    if kind == "Star" { return 6 }
    if kind == "Slash" { return 6 }
    0
}

fn is_binop(kind: String) -> Bool {
    if kind == "Or" { return true }
    if kind == "And" { return true }
    if kind == "EqEq" { return true }
    if kind == "NotEq" { return true }
    if kind == "Lt" { return true }
    if kind == "Gt" { return true }
    if kind == "LtEq" { return true }
    if kind == "GtEq" { return true }
    if kind == "Plus" { return true }
    if kind == "Minus" { return true }
    if kind == "Star" { return true }
    if kind == "Slash" { return true }
    false
}

fn parse_binop(tokens: [Map<String, Any>], pos: Int, min_prec: Int) -> Map<String, Any> {
    let r = parse_postfix(tokens, pos)
    let left = r["node"]
    let p = r["pos"]
    let running = true
    while running {
        let k = tok_kind(tokens, p)
        let prec = op_precedence(k)
        if is_binop(k) {
            if prec >= min_prec {
                let op = k
                let r2 = parse_binop(tokens, p + 1, prec + 1)
                let right = r2["node"]
                let p = r2["pos"]
                let left = { "expr": "BinOp", "op": op, "left": left, "right": right }
            } else {
                let running = false
            }
        } else {
            let running = false
        }
    }
    make_result(left, p)
}

fn parse_expr(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    parse_binop(tokens, pos, 1)
}

// ── Statement parsing ─────────────────────────────────────────────────────────

fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
    let k = tok_kind(tokens, pos)

    // let binding
    if k == "Let" {
        let p = pos + 1
        let name = tok_value(tokens, p)
        let p = p + 1
        let ltype = ""
        let k2 = tok_kind(tokens, p)
        // optional type annotation: name: Type — capture the leading
        // identifier so codegen can dispatch arithmetic vs concat on
        // `+` between two typed Idents.
        if k2 == "Colon" {
            let p = p + 1
            let kt = tok_kind(tokens, p)
            if kt == "Ident" {
                let ltype = tok_value(tokens, p)
            }
            let p = skip_type(tokens, p)
        }
        let p = expect(tokens, p, "Eq")
        let r = parse_expr(tokens, p)
        let val = r["node"]
        let p = r["pos"]
        return make_result({ "stmt": "Let", "name": name, "value": val, "type": ltype }, p)
    }

    // return statement
    if k == "Return" {
        let p = pos + 1
        let k2 = tok_kind(tokens, p)
        if k2 == "RBrace" {
            return make_result({ "stmt": "Return", "value": { "expr": "Nil" } }, p)
        }
        if k2 == "Eof" {
            return make_result({ "stmt": "Return", "value": { "expr": "Nil" } }, p)
        }
        let r = parse_expr(tokens, p)
        let val = r["node"]
        let p = r["pos"]
        return make_result({ "stmt": "Return", "value": val }, p)
    }

    // fn definition
    if k == "Fn" {
        let p = pos + 1
        let name = tok_value(tokens, p)
        let p = p + 1
        let r = parse_params(tokens, p)
        let params = r["params"]
        let p = r["pos"]
        // return type annotation: -> Type. Capture the leading identifier
        // so codegen can distinguish Void-returning functions from value-
        // returning ones. Anything not "Void" is treated as a value type.
        let ret_type = ""
        let k2 = tok_kind(tokens, p)
        if k2 == "Arrow" {
            let p = p + 1
            let kt = tok_kind(tokens, p)
            if kt == "Ident" {
                let ret_type = tok_value(tokens, p)
            }
            let p = skip_type(tokens, p)
        }
        let r2 = parse_block(tokens, p)
        let body = r2["stmts"]
        let p = r2["pos"]
        return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p)
    }

    // type definition
    if k == "Type" {
        let p = pos + 1
        let name = tok_value(tokens, p)
        let p = p + 1
        let p = expect(tokens, p, "LBrace")
        let fields: [Map<String, Any>] = native_list_empty()
        let running = true
        while running {
            let k2 = tok_kind(tokens, p)
            if k2 == "RBrace" {
                let running = false
            } else {
                if k2 == "Eof" {
                    let running = false
                } else {
                    let fname = tok_value(tokens, p)
                    let p = p + 1
                    let p = expect(tokens, p, "Colon")
                    let p = skip_type(tokens, p)
                    let fields = native_list_append(fields, { "name": fname })
                    let k3 = tok_kind(tokens, p)
                    if k3 == "Comma" {
                        let p = p + 1
                    }
                }
            }
        }
        let p = expect(tokens, p, "RBrace")
        return make_result({ "stmt": "TypeDef", "name": name, "fields": fields }, p)
    }

    // enum definition
    if k == "Enum" {
        let p = pos + 1
        let name = tok_value(tokens, p)
        let p = p + 1
        let p = expect(tokens, p, "LBrace")
        let variants: [Map<String, Any>] = native_list_empty()
        let running = true
        while running {
            let k2 = tok_kind(tokens, p)
            if k2 == "RBrace" {
                let running = false
            } else {
                if k2 == "Eof" {
                    let running = false
                } else {
                    let vname = tok_value(tokens, p)
                    let p = p + 1
                    let variants = native_list_append(variants, { "name": vname })
                    let k3 = tok_kind(tokens, p)
                    if k3 == "Comma" {
                        let p = p + 1
                    }
                }
            }
        }
        let p = expect(tokens, p, "RBrace")
        return make_result({ "stmt": "EnumDef", "name": name, "variants": variants }, p)
    }

    // import statement
    if k == "Import" {
        let p = pos + 1
        let path = tok_value(tokens, p)
        let p = p + 1
        return make_result({ "stmt": "Import", "path": path }, p)
    }

    // from ... import { ... }
    if k == "From" {
        let p = pos + 1
        let module_name = tok_value(tokens, p)
        let p = p + 1
        // skip "import" keyword
        let k2 = tok_kind(tokens, p)
        if k2 == "Import" {
            let p = p + 1
        }
        // skip { Name, ... }
        let k3 = tok_kind(tokens, p)
        if k3 == "LBrace" {
            let p = p + 1
            let running = true
            while running {
                let k4 = tok_kind(tokens, p)
                if k4 == "RBrace" {
                    let running = false
                } else {
                    if k4 == "Eof" {
                        let running = false
                    } else {
                        let p = p + 1
                        let k5 = tok_kind(tokens, p)
                        if k5 == "Comma" {
                            let p = p + 1
                        }
                    }
                }
            }
            let p = expect(tokens, p, "RBrace")
        }
        return make_result({ "stmt": "Import", "path": module_name }, p)
    }

    // while loop
    if k == "While" {
        let p = pos + 1
        let prev_no_block: String = state_get("__no_block_expr")
        state_set("__no_block_expr", "1")
        let r = parse_expr(tokens, p)
        state_set("__no_block_expr", prev_no_block)
        let cond = r["node"]
        let p = r["pos"]
        let r2 = parse_block(tokens, p)
        let body = r2["stmts"]
        let p = r2["pos"]
        return make_result({ "stmt": "While", "cond": cond, "body": body }, p)
    }

    // for loop
    if k == "For" {
        let p = pos + 1
        let item_name = tok_value(tokens, p)
        let p = p + 1
        let p = expect(tokens, p, "In")
        let prev_no_block: String = state_get("__no_block_expr")
        state_set("__no_block_expr", "1")
        let r = parse_expr(tokens, p)
        state_set("__no_block_expr", prev_no_block)
        let list_expr = r["node"]
        let p = r["pos"]
        let r2 = parse_block(tokens, p)
        let body = r2["stmts"]
        let p = r2["pos"]
        return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
    }

    // @decorator — capture decorator name and attach to following stmt
    if k == "At" {
        let p = pos + 1
        let dec_name = tok_value(tokens, p)
        let p = p + 1
        let r = parse_stmt(tokens, p)
        let inner = r["node"]
        let p2 = r["pos"]
        let inner_kind: String = inner["stmt"]
        if str_eq(inner_kind, "FnDef") {
            let with_dec = {
                "stmt": "FnDef",
                "name": inner["name"],
                "params": inner["params"],
                "body": inner["body"],
                "ret_type": inner["ret_type"],
                "decorator": dec_name
            }
            return make_result(with_dec, p2)
        }
        return r
    }

    // cgi block: cgi "name" { field: "val", ... }
    if k == "Cgi" {
        let p = pos + 1
        let name = tok_value(tokens, p)
        let p = p + 1
        let p = expect(tokens, p, "LBrace")
        let dharma_id = ""
        let principal = ""
        let network = ""
        let engram = ""
        let has_dharma_id = false
        let has_principal = false
        let has_network = false
        let has_engram = false
        let running = true
        while running {
            let k2 = tok_kind(tokens, p)
            if k2 == "RBrace" {
                let running = false
            } else {
                if k2 == "Eof" {
                    let running = false
                } else {
                    let fname = tok_value(tokens, p)
                    let p = p + 1
                    let p = expect(tokens, p, "Colon")
                    let fval = tok_value(tokens, p)
                    let p = p + 1
                    if str_eq(fname, "dharma_id") {
                        let dharma_id = fval
                        let has_dharma_id = true
                    }
                    if str_eq(fname, "principal") {
                        let principal = fval
                        let has_principal = true
                    }
                    if str_eq(fname, "network") {
                        let network = fval
                        let has_network = true
                    }
                    if str_eq(fname, "engram") {
                        let engram = fval
                        let has_engram = true
                    }
                    let k3 = tok_kind(tokens, p)
                    if k3 == "Comma" {
                        let p = p + 1
                    }
                }
            }
        }
        let p = expect(tokens, p, "RBrace")
        return make_result({
            "stmt": "CgiBlock",
            "name": name,
            "dharma_id": dharma_id,
            "principal": principal,
            "network": network,
            "engram": engram,
            "has_dharma_id": has_dharma_id,
            "has_principal": has_principal,
            "has_network": has_network,
            "has_engram": has_engram
        }, p)
    }

    // service block: service "name" { sponsor: "...", domain: "...", ... }
    //
    // A `service` declaration restricts the program's capabilities at
    // compile time: services CANNOT call self-formation primitives
    // (llm_call_agentic, llm_register_tool, dharma_emit, dharma_field,
    // mindlink-creation). Codegen enforces this with #error directives.
    if k == "Service" {
        let p = pos + 1
        let name = tok_value(tokens, p)
        let p = p + 1
        let p = expect(tokens, p, "LBrace")
        let sponsor = ""
        let domain = ""
        let running = true
        while running {
            let k2 = tok_kind(tokens, p)
            if k2 == "RBrace" {
                let running = false
            } else {
                if k2 == "Eof" {
                    let running = false
                } else {
                    let fname = tok_value(tokens, p)
                    let p = p + 1
                    let p = expect(tokens, p, "Colon")
                    let fval = tok_value(tokens, p)
                    let p = p + 1
                    if str_eq(fname, "sponsor") {
                        let sponsor = fval
                    }
                    if str_eq(fname, "domain") {
                        let domain = fval
                    }
                    let k3 = tok_kind(tokens, p)
                    if k3 == "Comma" {
                        let p = p + 1
                    }
                }
            }
        }
        let p = expect(tokens, p, "RBrace")
        return make_result({
            "stmt": "ServiceBlock",
            "name": name,
            "sponsor": sponsor,
            "domain": domain
        }, p)
    }

    // Bare reassignment: `name = expr`. Handled BEFORE the expression
    // fallback so we don't drop the assign on the floor and emit three
    // orphan expressions (the original silent-miscompile bug). El's `let`
    // already permits redeclaration, so this only applies when the parser
    // sees an Ident followed directly by `=`. `==` is a separate kind
    // (EqEq) so there's no ambiguity.
    if k == "Ident" {
        let k2 = tok_kind(tokens, pos + 1)
        if k2 == "Eq" {
            let name = tok_value(tokens, pos)
            let p = pos + 2
            let r = parse_expr(tokens, p)
            let val = r["node"]
            let p = r["pos"]
            return make_result({ "stmt": "Assign", "name": name, "value": val }, p)
        }
    }

    // bare expression or if/match statement
    let r = parse_expr(tokens, pos)
    let val = r["node"]
    let p = r["pos"]
    make_result({ "stmt": "Expr", "value": val }, p)
}

// ── Top-level parse ────────────────────────────────────────────────────────────

fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
    let total: Int = native_list_len(tokens)
    let stmts: [Map<String, Any>] = native_list_empty()
    let pos: Int = 0
    let running = true
    while running {
        if pos >= total {
            let running = false
        } else {
            let k = tok_kind(tokens, pos)
            if k == "Eof" {
                let running = false
            } else {
                let r = parse_stmt(tokens, pos)
                let stmt = r["node"]
                let new_pos: Int = r["pos"]
                let stmts = native_list_append(stmts, stmt)
                // Guard against infinite loops — if pos didn't advance, force it
                if new_pos <= pos {
                    let pos = pos + 1
                } else {
                    let pos = new_pos
                }
            }
        }
    }
    stmts
}
// 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).
fn c_escape(s: String) -> String {
    let chars: [String] = native_string_chars(s)
    let total: Int = native_list_len(chars)
    let out = ""
    let i = 0
    while i < total {
        let ch: String = native_list_get(chars, i)
        if ch == "\"" {
            let out = out + "\\\""
        } else {
            if ch == "\\" {
                let out = out + "\\\\"
            } else {
                if ch == "\n" {
                    let out = out + "\\n"
                } else {
                    if ch == "\r" {
                        let out = out + "\\r"
                    } else {
                        if ch == "\t" {
                            let out = out + "\\t"
                        } else {
                            let out = out + ch
                        }
                    }
                }
            }
        }
        let i = i + 1
    }
    out
}

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

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

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_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"]

        // ── 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 {
                    time_record_violation("caltime_plus_caltime", "CalendarTime + CalendarTime is not allowed (use cal_to_instant + Duration)")
                    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 {
                        time_record_violation("instant_plus_instant", "Instant + Instant is not allowed")
                        return "0 /* TIME_TYPE_ERROR: Instant + Instant */"
                    }
                }
                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) {
                        time_record_violation("duration_plus_int", "Duration + Int is not allowed (use duration_seconds(n) or N.seconds)")
                        return "0 /* TIME_TYPE_ERROR: Duration + Int */"
                    }
                }
                if right_is_dur {
                    if is_int_expr(left) {
                        time_record_violation("duration_plus_int", "Int + Duration is not allowed")
                        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) {
                        time_record_violation("duration_minus_int", "Duration - Int is not allowed")
                        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 {
                        time_record_violation("instant_cmp_duration", "Instant < Duration is not allowed")
                        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 {
                        time_record_violation("duration_cmp_instant", "Duration < Instant is not allowed")
                        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).
        }

        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.
            if is_int_expr(left) {
                if is_int_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.
            if is_int_expr(left) {
                if is_int_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_c = ""
        let i = 0
        while i < arity {
            let arg = native_list_get(args, i)
            let arg_c: String = cg_expr(arg)
            if i > 0 {
                let args_c = args_c + ", "
            }
            let args_c = args_c + arg_c
            let i = i + 1
        }

        if func_kind == "Ident" {
            let fn_name: String = func["name"]
            // Capability-kind enforcement: services can't call
            // self-formation primitives; utilities can't call any
            // DHARMA or LLM primitives. cap_check_call records
            // violations to be emitted as #error directives at the
            // top of the generated C, so cc fails with a clear msg.
            cap_check_call(fn_name)
            // 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).
            arity_check_call(fn_name, arity)
            // 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 + ")"
                    }
                }
            }
            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 = ""
        let i = 0
        while i < n {
            let elem = native_list_get(elems, i)
            let elem_c: String = cg_expr(elem)
            if i > 0 {
                let items = items + ", "
            }
            let items = items + elem_c
            let i = i + 1
        }
        return "el_list_new(" + native_int_to_str(n) + ", " + items + ")"
    }

    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 = ""
        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)
            if i > 0 {
                let items = items + ", "
            }
            let items = items + c_str_lit(key) + ", " + val_c
            let i = i + 1
        }
        return "el_map_new(" + native_int_to_str(n) + ", " + items + ")"
    }

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

    "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
    let out: String = "({ 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 out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
        } else {
            if str_eq(pkind, "Binding") {
                let bname: String = pat["name"]
                let out = out + "{ 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 out = out + "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
                } else {
                    if str_eq(pkind, "LitStr") {
                        let v: String = pat["value"]
                        let out = out + "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 out = out + "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
                        } else {
                            // unknown pattern → wildcard
                            let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
                        }
                    }
                }
            }
        }
        let i = i + 1
    }
    let out = out + done_label + ":; " + result_var + "; })"
    out
}

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

// 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)
    let out = ""
    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)
            let out = out + "el_val_t " + name + " = " + val_c + "; "
        } else {
            if str_eq(sk, "Return") {
                let val = s["value"]
                let val_c: String = cg_expr(val)
                let out = out + result_var + " = (" + val_c + "); "
            } else {
                if str_eq(sk, "Expr") {
                    let val = s["value"]
                    let val_c: String = cg_expr(val)
                    if is_last {
                        let out = out + result_var + " = (" + val_c + "); "
                    } else {
                        let out = out + "(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 out = out + 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
    }
    out
}

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

    // 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
        }
        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 == "FnDef" { return declared }
    if kind == "TypeDef" { return declared }
    if kind == "EnumDef" { return declared }
    if kind == "Import"  { return declared }
    if kind == "CgiBlock" { 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 {
                let inner = ""
                let j = 1
                while j < n - 1 {
                    let ch: String = native_list_get(chars, j)
                    let inner = inner + ch
                    let j = j + 1
                }
                return inner
            }
        }
    }
    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)
        let decl = cg_stmt(stmt, indent, decl)
        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 out = ""
    let i = 0
    while i < n {
        let param = native_list_get(params, i)
        let decl: String = param_decl(param, i)
        if i > 0 {
            let out = out + ", "
        }
        let out = out + decl
        let i = i + 1
    }
    out
}

// 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 + ",")
}

// 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 {
    let func = call_expr["func"]
    let fk: String = func["expr"]
    if !str_eq(fk, "Ident") { return false }
    let name: String = func["name"]
    if str_eq(name, "str_len") { return true }
    if str_eq(name, "str_index_of") { return true }
    if str_eq(name, "str_to_int") { return true }
    if str_eq(name, "str_char_code") { return true }
    if str_eq(name, "str_count") { return true }
    if str_eq(name, "str_count_chars") { return true }
    if str_eq(name, "str_count_bytes") { return true }
    if str_eq(name, "str_count_lines") { return true }
    if str_eq(name, "str_count_words") { return true }
    if str_eq(name, "str_count_letters") { return true }
    if str_eq(name, "str_count_digits") { return true }
    if str_eq(name, "str_last_index_of") { return true }
    if str_eq(name, "str_find_chars") { return true }
    if str_eq(name, "native_list_len") { return true }
    if str_eq(name, "el_list_len") { return true }
    if str_eq(name, "len") { return true }
    if str_eq(name, "json_get_int") { return true }
    if str_eq(name, "json_array_len") { return true }
    if str_eq(name, "engram_node_count") { return true }
    if str_eq(name, "engram_edge_count") { return true }
    if str_eq(name, "time_now") { return true }
    if str_eq(name, "time_now_utc") { return true }
    if str_eq(name, "time_diff") { return true }
    if str_eq(name, "time_add") { return true }
    if str_eq(name, "time_from_parts") { return true }
    if str_eq(name, "el_abs") { return true }
    if str_eq(name, "el_max") { return true }
    if str_eq(name, "el_min") { return true }
    if str_eq(name, "float_to_int") { return true }
    if str_eq(name, "unix_timestamp") { return true }
    if str_eq(name, "instant_to_unix_seconds") { return true }
    if str_eq(name, "instant_to_unix_millis") { return true }
    if str_eq(name, "duration_to_seconds") { return true }
    if str_eq(name, "duration_to_millis") { return true }
    if str_eq(name, "duration_to_nanos") { 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.
fn is_instant_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") { return true }
    if str_eq(name, "el_now_instant") { return true }
    if str_eq(name, "unix_seconds") { return true }
    if str_eq(name, "unix_millis") { return true }
    if str_eq(name, "instant_from_iso8601") { return true }
    if str_eq(name, "el_instant_add_dur") { return true }
    if str_eq(name, "el_instant_sub_dur") { return true }
    return false
}

// Builtins that return a Duration. Same role as is_instant_call.
fn is_duration_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, "el_duration_from_nanos") { return true }
    if str_eq(name, "duration_seconds") { return true }
    if str_eq(name, "duration_millis") { return true }
    if str_eq(name, "duration_nanos") { return true }
    if str_eq(name, "el_instant_diff") { return true }
    if str_eq(name, "el_duration_add") { return true }
    if str_eq(name, "el_duration_sub") { return true }
    if str_eq(name, "el_duration_scale") { return true }
    if str_eq(name, "el_duration_div") { return true }
    if str_eq(name, "ttl_cache_age") { return true }
    return false
}

// 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.
fn time_record_violation(kind: String, detail: String) -> Bool {
    let csv: String = state_get("__time_violations")
    if str_eq(csv, "") { let csv = "," }
    let entry: String = kind + ":" + detail
    let key: String = "," + entry + ","
    if str_contains(csv, key) { return true }
    state_set("__time_violations", csv + entry + ",")
    return true
}

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

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

fn cap_record_violation(kind: String, fn_name: String) -> Bool {
    let csv: String = state_get("__cap_violations")
    if str_eq(csv, "") { let csv = "," }
    let entry: String = kind + ":" + fn_name
    let key: String = "," + entry + ","
    if str_contains(csv, key) { return true }
    state_set("__cap_violations", csv + entry + ",")
    return true
}

// Self-formation primitives — the cut between CGI and service. A program
// that emits these calls IS structurally a CGI; we forbid them everywhere
// else.
fn is_self_formation_call(fn_name: String) -> Bool {
    if str_eq(fn_name, "llm_call_agentic") { return true }
    if str_eq(fn_name, "llm_register_tool") { return true }
    if str_eq(fn_name, "dharma_emit") { return true }
    if str_eq(fn_name, "dharma_field") { return true }
    return false
}

// Any DHARMA primitive — utilities have zero network presence.
fn is_dharma_call(fn_name: String) -> Bool {
    if str_eq(fn_name, "dharma_connect") { return true }
    if str_eq(fn_name, "dharma_send") { return true }
    if str_eq(fn_name, "dharma_activate") { return true }
    if str_eq(fn_name, "dharma_emit") { return true }
    if str_eq(fn_name, "dharma_field") { return true }
    if str_eq(fn_name, "dharma_strengthen") { return true }
    if str_eq(fn_name, "dharma_relationship") { return true }
    if str_eq(fn_name, "dharma_peers") { return true }
    return false
}

// Any LLM primitive — utilities have no LLM access at all.
fn is_llm_call(fn_name: String) -> Bool {
    if str_eq(fn_name, "llm_call") { return true }
    if str_eq(fn_name, "llm_call_system") { return true }
    if str_eq(fn_name, "llm_call_agentic") { return true }
    if str_eq(fn_name, "llm_vision") { return true }
    if str_eq(fn_name, "llm_register_tool") { return true }
    if str_eq(fn_name, "llm_models") { return true }
    return false
}

fn cap_check_call(fn_name: String) -> Bool {
    let kind: String = state_get("__program_kind")
    if str_eq(kind, "cgi") { return true }
    if str_eq(kind, "service") {
        if is_self_formation_call(fn_name) {
            cap_record_violation("service", fn_name)
            return false
        }
        return true
    }
    // utility (default)
    if is_dharma_call(fn_name) {
        cap_record_violation("utility", fn_name)
        return false
    }
    if is_llm_call(fn_name) {
        cap_record_violation("utility", fn_name)
        return false
    }
    return true
}

// 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.
fn emit_cap_violations() -> Void {
    let csv: String = state_get("__cap_violations")
    if str_eq(csv, "") { return }
    if str_eq(csv, ",") { return }
    let n: Int = str_len(csv)
    let i: Int = 1
    while i < n {
        let next_comma: Int = str_index_of(str_slice(csv, i, n), ",")
        if next_comma < 0 { return }
        let entry: String = str_slice(csv, i, i + next_comma)
        let colon: Int = str_index_of(entry, ":")
        if colon > 0 {
            let kind: String = str_slice(entry, 0, colon)
            let fn_name: String = str_slice(entry, colon + 1, str_len(entry))
            emit_line("#error \"capability violation: '" + kind + "' programs may not call '" + fn_name + "' (self-formation primitive — only 'cgi' programs may use it)\"")
        }
        let i = i + next_comma + 1
    }
}

// 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.
fn emit_time_violations() -> Void {
    let csv: String = state_get("__time_violations")
    if str_eq(csv, "") { return }
    if str_eq(csv, ",") { return }
    let n: Int = str_len(csv)
    let i: Int = 1
    while i < n {
        let next_comma: Int = str_index_of(str_slice(csv, i, n), ",")
        if next_comma < 0 { return }
        let entry: String = str_slice(csv, i, i + next_comma)
        let colon: Int = str_index_of(entry, ":")
        if colon > 0 {
            let detail: String = str_slice(entry, colon + 1, str_len(entry))
            emit_line("#error \"temporal type error: " + detail + "\"")
        }
        let i = i + next_comma + 1
    }
}

// ── 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.
fn builtin_arity(name: String) -> Int {
    // I/O
    if str_eq(name, "println") { return 1 }
    if str_eq(name, "print") { return 1 }
    if str_eq(name, "readline") { return 0 }
    // String
    if str_eq(name, "el_str_concat") { return 2 }
    if str_eq(name, "str_eq") { return 2 }
    if str_eq(name, "str_starts_with") { return 2 }
    if str_eq(name, "str_ends_with") { return 2 }
    if str_eq(name, "str_len") { return 1 }
    if str_eq(name, "str_concat") { return 2 }
    if str_eq(name, "int_to_str") { return 1 }
    if str_eq(name, "str_to_int") { return 1 }
    if str_eq(name, "str_slice") { return 3 }
    if str_eq(name, "str_contains") { return 2 }
    if str_eq(name, "str_replace") { return 3 }
    if str_eq(name, "str_to_upper") { return 1 }
    if str_eq(name, "str_to_lower") { return 1 }
    if str_eq(name, "str_trim") { return 1 }
    if str_eq(name, "str_index_of") { return 2 }
    if str_eq(name, "str_split") { return 2 }
    if str_eq(name, "str_char_at") { return 2 }
    if str_eq(name, "str_char_code") { return 2 }
    if str_eq(name, "str_pad_left") { return 3 }
    if str_eq(name, "str_pad_right") { return 3 }
    if str_eq(name, "str_format") { return 2 }
    if str_eq(name, "str_lower") { return 1 }
    if str_eq(name, "str_upper") { return 1 }
    // Text-processing primitives (Phase 1)
    if str_eq(name, "str_count") { return 2 }
    if str_eq(name, "str_count_chars") { return 1 }
    if str_eq(name, "str_count_bytes") { return 1 }
    if str_eq(name, "str_count_lines") { return 1 }
    if str_eq(name, "str_count_words") { return 1 }
    if str_eq(name, "str_count_letters") { return 1 }
    if str_eq(name, "str_count_digits") { return 1 }
    if str_eq(name, "str_index_of_all") { return 2 }
    if str_eq(name, "str_last_index_of") { return 2 }
    if str_eq(name, "str_find_chars") { return 2 }
    if str_eq(name, "str_repeat") { return 2 }
    if str_eq(name, "str_reverse") { return 1 }
    if str_eq(name, "str_strip_prefix") { return 2 }
    if str_eq(name, "str_strip_suffix") { return 2 }
    if str_eq(name, "str_strip_chars") { return 2 }
    if str_eq(name, "str_lstrip") { return 1 }
    if str_eq(name, "str_rstrip") { return 1 }
    if str_eq(name, "is_letter") { return 1 }
    if str_eq(name, "is_digit") { return 1 }
    if str_eq(name, "is_alphanumeric") { return 1 }
    if str_eq(name, "is_whitespace") { return 1 }
    if str_eq(name, "is_punctuation") { return 1 }
    if str_eq(name, "is_uppercase") { return 1 }
    if str_eq(name, "is_lowercase") { return 1 }
    if str_eq(name, "str_split_lines") { return 1 }
    if str_eq(name, "str_split_chars") { return 1 }
    if str_eq(name, "str_split_n") { return 3 }
    if str_eq(name, "str_join") { return 2 }
    // HTML sanitizer
    if str_eq(name, "el_html_sanitize") { return 2 }
    // Math
    if str_eq(name, "el_abs") { return 1 }
    if str_eq(name, "el_max") { return 2 }
    if str_eq(name, "el_min") { return 2 }
    // List
    if str_eq(name, "el_list_len") { return 1 }
    if str_eq(name, "el_list_get") { return 2 }
    if str_eq(name, "el_list_append") { return 2 }
    if str_eq(name, "el_list_empty") { return 0 }
    if str_eq(name, "el_list_clone") { return 1 }
    if str_eq(name, "list_push") { return 2 }
    if str_eq(name, "list_push_front") { return 2 }
    if str_eq(name, "list_join") { return 2 }
    if str_eq(name, "list_range") { return 2 }
    // Map
    if str_eq(name, "el_get_field") { return 2 }
    if str_eq(name, "el_map_get") { return 2 }
    if str_eq(name, "el_map_set") { return 3 }
    // HTTP
    if str_eq(name, "http_get") { return 1 }
    if str_eq(name, "http_post") { return 2 }
    if str_eq(name, "http_post_json") { return 2 }
    if str_eq(name, "http_get_with_headers") { return 2 }
    if str_eq(name, "http_post_with_headers") { return 3 }
    if str_eq(name, "http_post_form_auth") { return 3 }
    if str_eq(name, "http_serve") { return 2 }
    if str_eq(name, "http_set_handler") { return 1 }
    // Filesystem
    if str_eq(name, "fs_read") { return 1 }
    if str_eq(name, "fs_write") { return 2 }
    if str_eq(name, "fs_list") { return 1 }
    // JSON
    if str_eq(name, "json_get") { return 2 }
    if str_eq(name, "json_parse") { return 1 }
    if str_eq(name, "json_stringify") { return 1 }
    if str_eq(name, "json_get_string") { return 2 }
    if str_eq(name, "json_get_int") { return 2 }
    if str_eq(name, "json_get_float") { return 2 }
    if str_eq(name, "json_get_bool") { return 2 }
    if str_eq(name, "json_get_raw") { return 2 }
    if str_eq(name, "json_set") { return 3 }
    if str_eq(name, "json_array_len") { return 1 }
    // Time
    if str_eq(name, "time_now") { return 0 }
    if str_eq(name, "time_now_utc") { return 0 }
    if str_eq(name, "sleep_secs") { return 1 }
    if str_eq(name, "sleep_ms") { return 1 }
    if str_eq(name, "time_format") { return 2 }
    if str_eq(name, "time_to_parts") { return 1 }
    if str_eq(name, "time_from_parts") { return 3 }
    if str_eq(name, "time_add") { return 3 }
    if str_eq(name, "time_diff") { return 3 }
    // UUID
    if str_eq(name, "uuid_new") { return 0 }
    if str_eq(name, "uuid_v4") { return 0 }
    // Env / state
    if str_eq(name, "env") { return 1 }
    if str_eq(name, "state_set") { return 2 }
    if str_eq(name, "state_get") { return 1 }
    if str_eq(name, "state_del") { return 1 }
    if str_eq(name, "state_keys") { return 0 }
    // Float
    if str_eq(name, "float_to_str") { return 1 }
    if str_eq(name, "int_to_float") { return 1 }
    if str_eq(name, "float_to_int") { return 1 }
    if str_eq(name, "format_float") { return 2 }
    if str_eq(name, "decimal_round") { return 2 }
    if str_eq(name, "str_to_float") { return 1 }
    // Math (Float)
    if str_eq(name, "math_sqrt") { return 1 }
    if str_eq(name, "math_log") { return 1 }
    if str_eq(name, "math_ln") { return 1 }
    if str_eq(name, "math_sin") { return 1 }
    if str_eq(name, "math_cos") { return 1 }
    if str_eq(name, "math_pi") { return 0 }
    // Bool
    if str_eq(name, "bool_to_str") { return 1 }
    // Process
    if str_eq(name, "exit_program") { return 1 }
    // CGI / DHARMA
    if str_eq(name, "dharma_connect") { return 1 }
    if str_eq(name, "dharma_send") { return 2 }
    if str_eq(name, "dharma_activate") { return 1 }
    if str_eq(name, "dharma_emit") { return 2 }
    if str_eq(name, "dharma_field") { return 1 }
    if str_eq(name, "dharma_strengthen") { return 2 }
    if str_eq(name, "dharma_relationship") { return 1 }
    if str_eq(name, "dharma_peers") { return 0 }
    // Engram
    if str_eq(name, "engram_node") { return 3 }
    if str_eq(name, "engram_node_full") { return 8 }
    if str_eq(name, "engram_get_node") { return 1 }
    if str_eq(name, "engram_strengthen") { return 1 }
    if str_eq(name, "engram_forget") { return 1 }
    if str_eq(name, "engram_node_count") { return 0 }
    if str_eq(name, "engram_search") { return 2 }
    if str_eq(name, "engram_scan_nodes") { return 2 }
    if str_eq(name, "engram_connect") { return 4 }
    if str_eq(name, "engram_edge_between") { return 2 }
    if str_eq(name, "engram_neighbors") { return 1 }
    if str_eq(name, "engram_neighbors_filtered") { return 3 }
    if str_eq(name, "engram_edge_count") { return 0 }
    if str_eq(name, "engram_activate") { return 2 }
    if str_eq(name, "engram_save") { return 1 }
    if str_eq(name, "engram_load") { return 1 }
    if str_eq(name, "engram_store_boot") { return 1 }
    if str_eq(name, "engram_store_checkpoint") { return 0 }
    if str_eq(name, "engram_store_close") { return 0 }
    if str_eq(name, "engram_get_node_json") { return 1 }
    if str_eq(name, "engram_search_json") { return 2 }
    if str_eq(name, "engram_scan_nodes_json") { return 2 }
    if str_eq(name, "engram_neighbors_json") { return 3 }
    if str_eq(name, "engram_activate_json") { return 2 }
    if str_eq(name, "engram_stats_json") { return 0 }
    // LLM
    if str_eq(name, "llm_call") { return 2 }
    if str_eq(name, "llm_call_system") { return 3 }
    if str_eq(name, "llm_call_agentic") { return 4 }
    if str_eq(name, "llm_vision") { return 4 }
    if str_eq(name, "llm_models") { return 0 }
    if str_eq(name, "llm_register_tool") { return 2 }
    // Crypto
    if str_eq(name, "sha256_hex") { return 1 }
    if str_eq(name, "sha256_bytes") { return 1 }
    if str_eq(name, "hmac_sha256_hex") { return 2 }
    if str_eq(name, "hmac_sha256_bytes") { return 2 }
    if str_eq(name, "base64_encode") { return 1 }
    if str_eq(name, "base64_decode") { return 1 }
    if str_eq(name, "base64url_encode") { return 1 }
    if str_eq(name, "base64url_decode") { return 1 }
    // Native VM aliases
    if str_eq(name, "native_list_get") { return 2 }
    if str_eq(name, "native_list_len") { return 1 }
    if str_eq(name, "native_list_append") { return 2 }
    if str_eq(name, "native_list_empty") { return 0 }
    if str_eq(name, "native_list_clone") { return 1 }
    if str_eq(name, "native_string_chars") { return 1 }
    if str_eq(name, "native_int_to_str") { return 1 }
    // Method-call aliases
    if str_eq(name, "append") { return 2 }
    if str_eq(name, "len") { return 1 }
    if str_eq(name, "get") { return 2 }
    if str_eq(name, "map_get") { return 2 }
    if str_eq(name, "map_set") { return 3 }
    // -1 sentinel: variadic / unknown / user-defined → no check.
    return -1
}

fn arity_record_violation(fn_name: String, expected: Int, actual: Int) -> Bool {
    let csv: String = state_get("__arity_violations")
    if str_eq(csv, "") { let csv = "," }
    // Encode as fn_name|expected|actual to recover all three at emit time.
    let entry: String = fn_name + "|" + native_int_to_str(expected) + "|" + native_int_to_str(actual)
    let key: String = "," + entry + ","
    if str_contains(csv, key) { return true }
    state_set("__arity_violations", csv + entry + ",")
    return true
}

// 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.
fn arity_check_call(fn_name: String, actual: Int) -> Bool {
    let expected: Int = builtin_arity(fn_name)
    if expected < 0 { return true }
    if expected == actual { return true }
    arity_record_violation(fn_name, expected, actual)
    return true
}

// Emit recorded arity violations as #error directives.
fn emit_arity_violations() -> Void {
    let csv: String = state_get("__arity_violations")
    if str_eq(csv, "") { return }
    if str_eq(csv, ",") { return }
    let n: Int = str_len(csv)
    let i: Int = 1
    while i < n {
        let next_comma: Int = str_index_of(str_slice(csv, i, n), ",")
        if next_comma < 0 { return }
        let entry: String = str_slice(csv, i, i + next_comma)
        let p1: Int = str_index_of(entry, "|")
        if p1 > 0 {
            let fn_name: String = str_slice(entry, 0, p1)
            let rest: String = str_slice(entry, p1 + 1, str_len(entry))
            let p2: Int = str_index_of(rest, "|")
            if p2 > 0 {
                let exp_s: String = str_slice(rest, 0, p2)
                let act_s: String = str_slice(rest, p2 + 1, str_len(rest))
                emit_line("#error \"arity error: '" + fn_name + "' takes " + exp_s + " arguments, but called with " + act_s + "\"")
            }
        }
        let i = i + next_comma + 1
    }
}

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_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("__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)
        }
        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 cg_fn(stmt: Map<String, Any>) -> Void {
    let fn_name: String = stmt["name"]
    // 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.
    let decorator: String = stmt["decorator"]
    if vbd_has_restricted_call(body) {
        if !str_eq(decorator, "manager") {
            emit_line("#error \"VBD violation: dharma_emit/dharma_field called from non-@manager fn '" + fn_name + "'\"")
        }
    }
    // 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)
    emit_line("el_val_t " + fn_name + "(" + params_c + ") {")
    // 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)
    }
    cg_stmts(body_xformed, "  ", decl)
    emit_line("  return 0;")
    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 }
    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"
}

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

fn vbd_is_restricted_name(name: String) -> Bool {
    if str_eq(name, "dharma_emit") { return true }
    if str_eq(name, "dharma_field") { return true }
    false
}

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"]
            if vbd_is_restricted_name(fname) { return true }
        }
        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.
    state_set("__cap_violations", "")
    // Clear arity-violation accumulator from any prior compile.
    state_set("__arity_violations", "")
    // Clear temporal-type-violation accumulator from any prior compile.
    state_set("__time_violations", "")

    // Preamble
    emit_line("#include <stdint.h>")
    emit_line("#include <stdlib.h>")
    emit_line("#include \"el_runtime.h\"")
    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 + ");")
            }
        }
        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()
    }

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

    // 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.
    emit_line("int main(int _argc, char** _argv) {")
    emit_line("  el_runtime_init_args(_argc, _argv);")
    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.
    emit_cap_violations()
    // 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.
    emit_arity_violations()
    // Temporal-type violations (Instant + Instant, Duration + Int, …).
    emit_time_violations()

    // Return empty string — output was streamed via println
    ""
}
// codegen-js.el — El compiler JavaScript source code generator
//
// Input:  list of AST statement maps (from parser.el)
// Output: JavaScript source printed to stdout (streamed, one line at a time)
//
// Each El program compiles to a single .js file that imports el_runtime.js
// (which side-effects globals so call sites stay flat — println(x), not
// el.println(x)). Functions map to JS function declarations; top-level
// statements run at module load.
//
// Entry point: fn codegen_js(stmts: [Map<String, Any>], source: String) -> String
//   Returns "" — output goes to stdout via println().
//
// This file mirrors codegen.el (the C backend). Where the C backend has to
// fight the int64_t-everywhere convention to dispatch arithmetic vs concat
// or `==` vs `str_eq`, the JS backend can usually let JS's own operator
// semantics do the right thing. We retain the dispatch logic for clarity
// and so that explicit calls to `el_str_concat` or `str_eq` still work.

// ── String helpers ────────────────────────────────────────────────────────────

// Escape a JS string literal (double-quotes, backslashes, newlines, etc.).
fn js_escape(s: String) -> String {
    let chars: [String] = native_string_chars(s)
    let total: Int = native_list_len(chars)
    let out = ""
    let i = 0
    while i < total {
        let ch: String = native_list_get(chars, i)
        if ch == "\"" {
            let out = out + "\\\""
        } else {
            if ch == "\\" {
                let out = out + "\\\\"
            } else {
                if ch == "\n" {
                    let out = out + "\\n"
                } else {
                    if ch == "\r" {
                        let out = out + "\\r"
                    } else {
                        if ch == "\t" {
                            let out = out + "\\t"
                        } else {
                            let out = out + ch
                        }
                    }
                }
            }
        }
        let i = i + 1
    }
    out
}

fn js_str_lit(s: String) -> String {
    "\"" + js_escape(s) + "\""
}

// ── Code emission ─────────────────────────────────────────────────────────────

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

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

// ── Operator helpers ──────────────────────────────────────────────────────────

fn js_binop(op: String) -> String {
    if op == "Plus"  { return "+" }
    if op == "Minus" { return "-" }
    if op == "Star"  { return "*" }
    if op == "Slash" { return "/" }
    if op == "Percent" { return "%" }
    if op == "EqEq"  { return "===" }
    if op == "NotEq" { return "!==" }
    if op == "Lt"    { return "<" }
    if op == "Gt"    { return ">" }
    if op == "LtEq"  { return "<=" }
    if op == "GtEq"  { return ">=" }
    if op == "And"   { return "&&" }
    if op == "Or"    { return "||" }
    op
}

// ── Int-name tracking (mirrors codegen.el) ────────────────────────────────────

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

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

fn js_build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
    state_set("__js_int_names", ",")
    let np: Int = native_list_len(params)
    let pi = 0
    while pi < np {
        let param = native_list_get(params, pi)
        let pname: String = param["name"]
        let ptype: String = param["type"]
        if str_eq(ptype, "Int") {
            js_add_int_name(pname)
        }
        let pi = pi + 1
    }
    return true
}

fn js_is_int_call(call_expr: Map<String, Any>) -> Bool {
    let func = call_expr["func"]
    let fk: String = func["expr"]
    if !str_eq(fk, "Ident") { return false }
    let name: String = func["name"]
    if str_eq(name, "str_len") { return true }
    if str_eq(name, "str_index_of") { return true }
    if str_eq(name, "str_to_int") { return true }
    if str_eq(name, "str_char_code") { return true }
    if str_eq(name, "native_list_len") { return true }
    if str_eq(name, "el_list_len") { return true }
    if str_eq(name, "len") { return true }
    if str_eq(name, "json_get_int") { return true }
    if str_eq(name, "time_now") { return true }
    if str_eq(name, "time_now_utc") { return true }
    if str_eq(name, "el_abs") { return true }
    if str_eq(name, "el_max") { return true }
    if str_eq(name, "el_min") { return true }
    return false
}

// ── Expression codegen ────────────────────────────────────────────────────────
//
// js_cg_expr returns a JS expression string (not a statement).
//
// Note: the C backend's `+` dispatch is preserved here for two reasons:
//   1) Generated output stays grep-equivalent across targets
//   2) Explicit `el_str_concat()` lives in the runtime; codegen routes
//      through it for ambiguous (Ident+Ident, Call+Call) cases. JS's
//      own `+` would also work, but el_str_concat coerces both sides
//      to strings — closer to the C semantics.

fn js_cg_expr(expr: Map<String, Any>) -> String {
    let kind: String = expr["expr"]

    if kind == "Int" {
        let v: String = expr["value"]
        return v
    }

    // DurationLit — postfix-literal time value (e.g. 30.seconds, 1.hour).
    // The JS backend lowers to a literal integer nanosecond count. The C
    // backend uses the typed wrapper el_duration_from_nanos to make intent
    // explicit at the runtime boundary; JS has no equivalent shim yet, so
    // we lower directly. A future Phase 2 JS time runtime can route through
    // a wrapper once added.
    if kind == "DurationLit" {
        let count: String = expr["count"]
        let unit: String = expr["unit"]
        let mult_ns = "1"
        if str_eq(unit, "nano") { let mult_ns = "1" }
        if str_eq(unit, "nanos") { let mult_ns = "1" }
        if str_eq(unit, "milli") { let mult_ns = "1000000" }
        if str_eq(unit, "millis") { let mult_ns = "1000000" }
        if str_eq(unit, "millisecond") { let mult_ns = "1000000" }
        if str_eq(unit, "milliseconds") { let mult_ns = "1000000" }
        if str_eq(unit, "second") { let mult_ns = "1000000000" }
        if str_eq(unit, "seconds") { let mult_ns = "1000000000" }
        if str_eq(unit, "minute") { let mult_ns = "60000000000" }
        if str_eq(unit, "minutes") { let mult_ns = "60000000000" }
        if str_eq(unit, "hour") { let mult_ns = "3600000000000" }
        if str_eq(unit, "hours") { let mult_ns = "3600000000000" }
        if str_eq(unit, "day") { let mult_ns = "86400000000000" }
        if str_eq(unit, "days") { let mult_ns = "86400000000000" }
        return "(" + count + " * " + mult_ns + ")"
    }

    if kind == "Float" {
        // JS numbers are already doubles — no bit-cast trick needed.
        let v: String = expr["value"]
        return v
    }

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

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

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

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

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

    if kind == "Neg" {
        let inner = expr["inner"]
        let inner_c: String = js_cg_expr(inner)
        return "(-" + inner_c + ")"
    }

    if kind == "BinOp" {
        let op: String = expr["op"]
        let left = expr["left"]
        let right = expr["right"]
        let left_c: String = js_cg_expr(left)
        let right_c: String = js_cg_expr(right)
        let left_kind: String = left["expr"]
        let right_kind: String = right["expr"]

        // Plus dispatch — same shape as C backend, but we route through
        // el_str_concat for the string-concat path (its JS impl coerces
        // and matches C's behavior). Arithmetic uses bare JS `+`.
        if op == "Plus" {
            if left_kind == "Str" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Str" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            if left_kind == "Int" {
                return "(" + left_c + " + " + right_c + ")"
            }
            if right_kind == "Int" {
                return "(" + left_c + " + " + right_c + ")"
            }
            if left_kind == "Ident" {
                if right_kind == "Ident" {
                    let lname: String = left["name"]
                    let rname: String = right["name"]
                    if js_is_int_name(lname) {
                        if js_is_int_name(rname) {
                            return "(" + left_c + " + " + right_c + ")"
                        }
                    }
                }
            }
            if left_kind == "Ident" {
                if right_kind == "Call" {
                    let lname: String = left["name"]
                    if js_is_int_name(lname) {
                        if js_is_int_call(right) {
                            return "(" + left_c + " + " + right_c + ")"
                        }
                    }
                }
            }
            if right_kind == "Ident" {
                if left_kind == "Call" {
                    let rname: String = right["name"]
                    if js_is_int_name(rname) {
                        if js_is_int_call(left) {
                            return "(" + left_c + " + " + right_c + ")"
                        }
                    }
                }
            }
            if left_kind == "Call" {
                if right_kind == "Call" {
                    if js_is_int_call(left) {
                        if js_is_int_call(right) {
                            return "(" + left_c + " + " + right_c + ")"
                        }
                    }
                }
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Call" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            // Fallback: when in doubt, route through el_str_concat. JS's
            // own + handles strings and numbers natively, but el_str_concat
            // gives us a single point of control if behavior needs to diverge.
            if left_kind == "Ident" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
            if right_kind == "Ident" {
                return "el_str_concat(" + left_c + ", " + right_c + ")"
            }
        }

        // Equality dispatch — C backend disambiguates via str_eq for
        // strings and == for ints. JS does both with === if we know
        // the types are uniform; for ambiguous identifier pairs we
        // route through str_eq for safety (it falls back to === in JS).
        if op == "EqEq" {
            if left_kind == "Int" { return "(" + left_c + " === " + right_c + ")" }
            if right_kind == "Int" { return "(" + left_c + " === " + right_c + ")" }
            if left_kind == "Bool" { return "(" + left_c + " === " + right_c + ")" }
            if right_kind == "Bool" { return "(" + left_c + " === " + right_c + ")" }
            if left_kind == "Nil" { return "(" + left_c + " === " + right_c + ")" }
            if right_kind == "Nil" { return "(" + left_c + " === " + right_c + ")" }
            if left_kind == "Ident" {
                if right_kind == "Ident" {
                    let lname: String = left["name"]
                    let rname: String = right["name"]
                    if js_is_int_name(lname) {
                        if js_is_int_name(rname) {
                            return "(" + left_c + " === " + right_c + ")"
                        }
                    }
                }
            }
            if left_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" }
            if right_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" }
            // Default: === (works for strings, numbers, bools in JS)
            return "(" + left_c + " === " + right_c + ")"
        }

        if op == "NotEq" {
            if left_kind == "Int" { return "(" + left_c + " !== " + right_c + ")" }
            if right_kind == "Int" { return "(" + left_c + " !== " + right_c + ")" }
            if left_kind == "Bool" { return "(" + left_c + " !== " + right_c + ")" }
            if right_kind == "Bool" { return "(" + left_c + " !== " + right_c + ")" }
            if left_kind == "Nil" { return "(" + left_c + " !== " + right_c + ")" }
            if right_kind == "Nil" { return "(" + left_c + " !== " + right_c + ")" }
            if left_kind == "Ident" {
                if right_kind == "Ident" {
                    let lname: String = left["name"]
                    let rname: String = right["name"]
                    if js_is_int_name(lname) {
                        if js_is_int_name(rname) {
                            return "(" + left_c + " !== " + right_c + ")"
                        }
                    }
                }
            }
            if left_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" }
            if right_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" }
            return "(" + left_c + " !== " + right_c + ")"
        }

        let op_c: String = js_binop(op)
        return "(" + left_c + " " + op_c + " " + right_c + ")"
    }

    if kind == "Call" {
        let func = expr["func"]
        let args = expr["args"]
        let arity: Int = native_list_len(args)
        let func_kind: String = func["expr"]

        let args_c = ""
        let i = 0
        while i < arity {
            let arg = native_list_get(args, i)
            let arg_c: String = js_cg_expr(arg)
            if i > 0 {
                let args_c = args_c + ", "
            }
            let args_c = args_c + arg_c
            let i = i + 1
        }

        if func_kind == "Ident" {
            let fn_name: String = func["name"]
            return fn_name + "(" + args_c + ")"
        }

        if func_kind == "Field" {
            // El's `obj.method(args)` becomes `method(obj, args)` — same
            // convention as the C backend. The runtime exports method
            // shortforms (append, len, get, map_get, map_set) that match.
            let obj = func["object"]
            let field: String = func["field"]
            let obj_c: String = js_cg_expr(obj)
            if arity > 0 {
                return field + "(" + obj_c + ", " + args_c + ")"
            }
            return field + "(" + obj_c + ")"
        }

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

    if kind == "Field" {
        // El's `obj.foo` becomes JS `obj["foo"]` — works on plain objects
        // (maps) and on JS objects with prototype. el_get_field is a
        // runtime helper for callers that want EL_NULL on missing keys.
        let obj = expr["object"]
        let field: String = expr["field"]
        let obj_c: String = js_cg_expr(obj)
        return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")"
    }

    if kind == "Index" {
        // Map vs list dispatch on the index expression kind, same as C.
        let obj = expr["object"]
        let idx = expr["index"]
        let obj_c: String = js_cg_expr(obj)
        let idx_c: String = js_cg_expr(idx)
        let idx_kind: String = idx["expr"]
        if str_eq(idx_kind, "Str") {
            return "el_get_field(" + obj_c + ", " + idx_c + ")"
        }
        return "el_list_get(" + obj_c + ", " + idx_c + ")"
    }

    if kind == "Array" {
        let elems = expr["elems"]
        let n: Int = native_list_len(elems)
        if n == 0 { return "[]" }
        let items = ""
        let i = 0
        while i < n {
            let elem = native_list_get(elems, i)
            let elem_c: String = js_cg_expr(elem)
            if i > 0 {
                let items = items + ", "
            }
            let items = items + elem_c
            let i = i + 1
        }
        return "[" + items + "]"
    }

    if kind == "Map" {
        let pairs = expr["pairs"]
        let n: Int = native_list_len(pairs)
        if n == 0 { return "{}" }
        let items = ""
        let i = 0
        while i < n {
            let pair = native_list_get(pairs, i)
            let key: String = pair["key"]
            let val = pair["value"]
            let val_c: String = js_cg_expr(val)
            if i > 0 {
                let items = items + ", "
            }
            let items = items + js_str_lit(key) + ": " + val_c
            let i = i + 1
        }
        return "{" + items + "}"
    }

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

    if kind == "If" {
        let cond = expr["cond"]
        let cond_c: String = js_cg_expr(cond)
        // If as expression: ternary. Body of the if-expression is not
        // currently emitted as expression-form for compound bodies; this
        // matches the C backend's if-expr stub.
        return "(" + cond_c + " ? 1 : 0)"
    }

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

    "null"
}

// ── Match codegen (basic) ─────────────────────────────────────────────────────
//
// Lower a match expression to an IIFE with if/else chain. Works for
// LitInt / LitStr / LitBool / Wildcard / Binding patterns. Tagged-union
// destructuring is not implemented — it's stubbed and falls through to
// the wildcard path.

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

fn js_cg_match(expr: Map<String, Any>) -> String {
    let subject = expr["subject"]
    let arms = expr["arms"]
    let subj_c: String = js_cg_expr(subject)
    let id: String = js_next_match_id()
    let subj_var: String = "_match_subj_" + id
    let out: String = "((" + subj_var + ") => { "
    let n: Int = native_list_len(arms)
    let i = 0
    while i < n {
        let arm = native_list_get(arms, i)
        let pat = arm["pattern"]
        let body = arm["body"]
        let pkind: String = pat["pattern"]
        let body_c: String = js_cg_expr(body)
        if str_eq(pkind, "Wildcard") {
            let out = out + "return (" + body_c + "); "
        } else {
            if str_eq(pkind, "Binding") {
                let bname: String = pat["name"]
                let out = out + "{ const " + bname + " = " + subj_var + "; return (" + body_c + "); } "
            } else {
                if str_eq(pkind, "LitInt") {
                    let v: String = pat["value"]
                    let out = out + "if (" + subj_var + " === " + v + ") return (" + body_c + "); "
                } else {
                    if str_eq(pkind, "LitStr") {
                        let v: String = pat["value"]
                        let out = out + "if (str_eq(" + subj_var + ", " + js_str_lit(v) + ")) return (" + body_c + "); "
                    } else {
                        if str_eq(pkind, "LitBool") {
                            let v: String = pat["value"]
                            let bv = "false"
                            if str_eq(v, "true") { let bv = "true" }
                            let out = out + "if (" + subj_var + " === " + bv + ") return (" + body_c + "); "
                        } else {
                            // unknown pattern → wildcard
                            let out = out + "return (" + body_c + "); "
                        }
                    }
                }
            }
        }
        let i = i + 1
    }
    let out = out + "return null; })(" + subj_c + ")"
    out
}

// ── Variable scope tracking ───────────────────────────────────────────────────
//
// El allows `let x = ...` to redeclare in the same scope. JS would throw
// with `let` (Identifier already declared). We track declared names and
// emit bare `x = ...` on redeclaration, `let x = ...` first time.

fn js_list_contains(lst: [String], s: String) -> Bool {
    let n: Int = native_list_len(lst)
    let i = 0
    while i < n {
        let item: String = native_list_get(lst, i)
        if item == s { return true }
        let i = i + 1
    }
    false
}

// ── Statement codegen ─────────────────────────────────────────────────────────

fn js_cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [String] {
    let kind: String = stmt["stmt"]

    if kind == "Let" {
        let name: String = stmt["name"]
        let val = stmt["value"]
        let val_c: String = js_cg_expr(val)
        let ltype: String = stmt["type"]
        if str_eq(ltype, "Int") {
            js_add_int_name(name)
        }
        let vk: String = val["expr"]
        if str_eq(vk, "Int") {
            js_add_int_name(name)
        }
        if js_list_contains(declared, name) {
            js_emit_line(indent + name + " = " + val_c + ";")
            return declared
        } else {
            // Use `let` (not `const`) — El semantics allow rebinding.
            js_emit_line(indent + "let " + name + " = " + val_c + ";")
            return native_list_append(declared, name)
        }
    }

    if kind == "Return" {
        let val = stmt["value"]
        let val_kind: String = val["expr"]
        if val_kind == "Nil" {
            js_emit_line(indent + "return null;")
        } else {
            let val_c: String = js_cg_expr(val)
            js_emit_line(indent + "return " + val_c + ";")
        }
        return declared
    }

    // Bare reassignment: `name = expr`. Mirrors the C backend — emits a
    // plain JS assignment without `let` so we don't shadow an outer binding.
    if kind == "Assign" {
        let name: String = stmt["name"]
        let val = stmt["value"]
        let val_c: String = js_cg_expr(val)
        js_emit_line(indent + name + " = " + val_c + ";")
        return declared
    }

    if kind == "Expr" {
        let val = stmt["value"]
        let val_kind: String = val["expr"]
        if val_kind == "If" {
            js_cg_if_stmt(val, indent, declared)
            return declared
        }
        if val_kind == "For" {
            js_cg_for_stmt(val, indent, declared)
            return declared
        }
        let val_c: String = js_cg_expr(val)
        js_emit_line(indent + val_c + ";")
        return declared
    }

    if kind == "While" {
        let cond = stmt["cond"]
        let body = stmt["body"]
        let cond_c: String = js_cg_expr(cond)
        let cond_c = js_strip_outer_parens(cond_c)
        js_emit_line(indent + "while (" + cond_c + ") {")
        js_cg_stmts(body, indent + "  ", native_list_clone(declared))
        js_emit_line(indent + "}")
        return declared
    }

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

    if kind == "FnDef" { return declared }
    if kind == "TypeDef" { return declared }
    if kind == "EnumDef" { return declared }
    if kind == "Import"  { return declared }
    if kind == "CgiBlock" {
        // CGI blocks compile to a no-op + warning comment in JS target.
        // The runtime cgi identity is server-side; UI code is not a CGI
        // principal. See spec/codegen-js.md §7.
        let cname: String = stmt["name"]
        js_emit_line(indent + "// cgi block '" + cname + "' — no-op in JS target (server-side concept)")
        return declared
    }
    if kind == "ServiceBlock" {
        let sname: String = stmt["name"]
        js_emit_line(indent + "// service block '" + sname + "' — no-op in JS target")
        return declared
    }
    declared
}

// Strip a single layer of surrounding parentheses from a JS expression string.
fn js_strip_outer_parens(s: String) -> String {
    let chars: [String] = native_string_chars(s)
    let n: Int = native_list_len(chars)
    if n < 2 { return s }
    let first: String = native_list_get(chars, 0)
    let last: String = native_list_get(chars, n - 1)
    if first == "(" {
        if last == ")" {
            let depth = 1
            let i = 1
            let balanced = true
            while i < n - 1 {
                let ch: String = native_list_get(chars, i)
                if ch == "(" {
                    let depth = depth + 1
                }
                if ch == ")" {
                    let depth = depth - 1
                    if depth == 0 {
                        let balanced = false
                        let i = n
                    }
                }
                let i = i + 1
            }
            if balanced {
                let inner = ""
                let j = 1
                while j < n - 1 {
                    let ch: String = native_list_get(chars, j)
                    let inner = inner + ch
                    let j = j + 1
                }
                return inner
            }
        }
    }
    s
}

fn js_cg_if_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
    let cond = expr["cond"]
    let then_stmts = expr["then"]
    let else_stmts = expr["else"]
    let has_else: Bool = expr["has_else"]
    let cond_c: String = js_cg_expr(cond)
    let cond_c = js_strip_outer_parens(cond_c)
    js_emit_line(indent + "if (" + cond_c + ") {")
    js_cg_stmts(then_stmts, indent + "  ", native_list_clone(declared))
    if has_else {
        js_emit_line(indent + "} else {")
        js_cg_stmts(else_stmts, indent + "  ", native_list_clone(declared))
    }
    js_emit_line(indent + "}")
}

fn js_cg_for_body(item: String, list_expr: Map<String, Any>, body: [Map<String, Any>], indent: String, declared: [String]) -> Void {
    let list_c: String = js_cg_expr(list_expr)
    js_emit_line(indent + "for (const " + item + " of " + list_c + ") {")
    let body_decl = native_list_clone(declared)
    let body_decl = native_list_append(body_decl, item)
    js_cg_stmts(body, indent + "  ", body_decl)
    js_emit_line(indent + "}")
}

fn js_cg_for_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
    let item: String = expr["item"]
    let list_expr = expr["list"]
    let body = expr["body"]
    js_cg_for_body(item, list_expr, body, indent, declared)
}

fn js_cg_stmts(stmts: [Map<String, Any>], indent: String, declared: [String]) -> [String] {
    let n: Int = native_list_len(stmts)
    let i = 0
    let decl = declared
    while i < n {
        let stmt = native_list_get(stmts, i)
        let decl = js_cg_stmt(stmt, indent, decl)
        let i = i + 1
    }
    decl
}

// ── Function declaration codegen ──────────────────────────────────────────────

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

// Same implicit-return transform as the C backend.
fn js_transform_implicit_return(body: [Map<String, Any>]) -> [Map<String, Any>] {
    let n: Int = native_list_len(body)
    if n == 0 { return body }
    let last: Map<String, Any> = native_list_get(body, n - 1)
    let last_kind: String = last["stmt"]
    if last_kind == "Expr" {
        let val = last["value"]
        let val_kind: String = val["expr"]
        if val_kind == "If" { return body }
        if val_kind == "For" { return body }
        let new_body: [Map<String, Any>] = native_list_empty()
        let i = 0
        while i < n - 1 {
            let new_body = native_list_append(new_body, native_list_get(body, i))
            let i = i + 1
        }
        let return_stmt: Map<String, Any> = { "stmt": "Return", "value": val }
        let new_body = native_list_append(new_body, return_stmt)
        return new_body
    }
    body
}

fn js_cg_fn(stmt: Map<String, Any>) -> Void {
    let fn_name: String = stmt["name"]
    let params = stmt["params"]
    let body = stmt["body"]
    let ret_type: String = stmt["ret_type"]
    let params_str: String = js_params_str(params)
    js_build_int_names_for_params(params)

    // Special-case `fn main` — emit as a regular function and call it
    // at module bottom (after all top-level statements). This matches
    // the C backend's behavior where `fn main` is the entry point.
    if fn_name == "main" {
        js_emit_line("function main(" + params_str + ") {")
    } else {
        js_emit_line("function " + fn_name + "(" + params_str + ") {")
    }

    let decl = native_list_empty()
    let np: Int = native_list_len(params)
    let pi = 0
    while pi < np {
        let param = native_list_get(params, pi)
        let pname: String = param["name"]
        let decl = native_list_append(decl, pname)
        let pi = pi + 1
    }
    let body_xformed = body
    if !str_eq(ret_type, "Void") {
        let body_xformed = js_transform_implicit_return(body)
    }
    js_cg_stmts(body_xformed, "  ", decl)
    js_emit_line("}")
    js_emit_blank()
}

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

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

fn js_is_top_level_decl(stmt: Map<String, Any>) -> Bool {
    let kind: String = stmt["stmt"]
    if kind == "TypeDef" { return true }
    if kind == "EnumDef" { return true }
    if kind == "Import"  { return true }
    if kind == "CgiBlock" { return true }
    if kind == "ServiceBlock" { return true }
    false
}

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

fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
    // Reset per-compile state.
    state_set("__js_int_names", "")
    state_set("__js_match_counter", "")

    // Preamble: inline the runtime via a single import that side-effects
    // globalThis. The runtime path is resolved relative to the generated
    // output; users running `elc --target=js` are responsible for ensuring
    // el_runtime.js is reachable. For self-contained output, the runtime
    // could be inlined; that is a follow-up.
    js_emit_line("// Generated by elc --target=js")
    js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")
    js_emit_line("import \"./el_runtime.js\";")
    js_emit_line("const {")
    js_emit_line("  println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,")
    js_emit_line("  str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,")
    js_emit_line("  str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,")
    js_emit_line("  str_char_code, str_lower, str_upper, el_abs, el_max, el_min,")
    js_emit_line("  el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,")
    js_emit_line("  list_push, list_join, list_range,")
    js_emit_line("  el_map_new, el_get_field, el_map_get, el_map_set,")
    js_emit_line("  http_get, http_post, http_post_json,")
    js_emit_line("  fs_read, fs_write, fs_list,")
    js_emit_line("  json_parse, json_stringify, json_get, json_get_string, json_get_int,")
    js_emit_line("  time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,")
    js_emit_line("  el_retain, el_release,")
    js_emit_line("  append, len, get, map_get, map_set,")
    js_emit_line("  native_list_get, native_list_len, native_list_append, native_list_empty,")
    js_emit_line("  native_list_clone, native_string_chars, native_int_to_str,")
    js_emit_line("  args, state_set, state_get, state_del, state_keys, env,")
    js_emit_line("  dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")
    js_emit_line("  engram_node, engram_search, engram_activate,")
    js_emit_line("  llm_call, llm_call_system,")
    js_emit_line("} = globalThis.__el;")
    js_emit_blank()

    // Function definitions
    let n: Int = native_list_len(stmts)
    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        if js_is_fndef(stmt) {
            js_cg_fn(stmt)
        }
        let i = i + 1
    }

    // Top-level statements (those that are not FnDef and not declarative)
    // run at module load. If the program defines `fn main`, we additionally
    // call main() at the end so the C-backend mental model of "fn main is
    // the entry point" carries over.
    let has_main = false
    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        let sk: String = stmt["stmt"]
        if str_eq(sk, "FnDef") {
            let fn_name: String = stmt["name"]
            if str_eq(fn_name, "main") {
                let has_main = true
            }
        }
        let i = i + 1
    }

    let main_decl = native_list_empty()
    let i = 0
    while i < n {
        let stmt = native_list_get(stmts, i)
        if js_is_fndef(stmt) {
            // skip
        } else {
            if js_is_top_level_decl(stmt) {
                // skip
            } else {
                let main_decl = js_cg_stmt(stmt, "", main_decl)
            }
        }
        let i = i + 1
    }

    if has_main {
        js_emit_blank()
        js_emit_line("main();")
    }

    // Return empty string — output was streamed via println
    ""
}
// compiler.el — el self-hosting compiler pipeline
//
// Wires lexer -> parser -> codegen into a single compile() function.
// This is the bootstrap entry point: compiled once by the Rust el-compiler,
// then self-hosted from that point forward.
//
// Two backends:
//   - C    (default)   compile()    -> emits C source linked against el_runtime.c
//   - JS   (--target=js) compile_js() -> emits JS source linked against el_runtime.js
//
// Compile the C output with:
//   cc -o <prog> <prog>.c el_runtime.c
//
// Run the JS output with:
//   node <prog>.js   (after copying el_runtime.js next to it)


// compile — full pipeline (C target): source string -> C source string
fn compile(source: String) -> String {
    let tokens: [Map<String, Any>] = lex(source)
    let stmts: [Map<String, Any>] = parse(tokens)
    // Token list is no longer needed after parsing — release it to free memory
    // before codegen allocates its own working data on large source files.
    el_release(tokens)
    codegen(stmts, source)
}

// compile_js — full pipeline (JS target): source string -> JS source string
fn compile_js(source: String) -> String {
    let tokens: [Map<String, Any>] = lex(source)
    let stmts: [Map<String, Any>] = parse(tokens)
    // Token list is no longer needed after parsing — release it to free memory.
    el_release(tokens)
    codegen_js(stmts, source)
}

// compile_dispatch — pick a backend based on the requested target.
//   tgt = "c"  | "js"
// (The parameter is named `tgt` because `target` is a reserved keyword
//  in El's lexer — it would be tokenised as `Target`, breaking the
//  parser's identifier resolution.)
fn compile_dispatch(tgt: String, source: String) -> String {
    if str_eq(tgt, "js") { return compile_js(source) }
    compile(source)
}

// Detect a `--target=<lang>` flag in argv and return the target.
// Returns "c" if none specified or unrecognized.
fn detect_target(argv: [String]) -> String {
    let n: Int = native_list_len(argv)
    let i = 0
    while i < n {
        let a: String = native_list_get(argv, i)
        if str_starts_with(a, "--target=") {
            let v: String = str_slice(a, 9, str_len(a))
            return v
        }
        let i = i + 1
    }
    return "c"
}

// Strip flags from argv, leaving only positional arguments.
fn strip_flags(argv: [String]) -> [String] {
    let out: [String] = native_list_empty()
    let n: Int = native_list_len(argv)
    let i = 0
    while i < n {
        let a: String = native_list_get(argv, i)
        if !str_starts_with(a, "--") {
            let out = native_list_append(out, a)
        }
        let i = i + 1
    }
    return out
}

// ── Import resolution ────────────────────────────────────────────────────────
//
// elc supports two forms of import:
//   import "path/to/file.el"       — quoted relative path
//   from module import { Name }    — bare module name resolves to module.el
//                                    in the entry source's directory
//
// Codegen treats Import statements as no-ops (declarations only), so to
// actually link bodies across files we textually concatenate every imported
// source ahead of the entry source before lex/parse. resolve_imports does a
// depth-first traversal with deduplication so any module that gets pulled in
// transitively is included exactly once.

fn dirname_of(path: String) -> String {
    let n: Int = str_len(path)
    let i: Int = n - 1
    while i >= 0 {
        let c: String = str_slice(path, i, i + 1)
        if str_eq(c, "/") {
            return str_slice(path, 0, i)
        }
        let i = i - 1
    }
    return "."
}

// Extract the resolved file path from a single trimmed source line. Returns
// "" if the line is not an import.
fn parse_import_line(trimmed: String, dir: String) -> String {
    if str_starts_with(trimmed, "import \"") {
        let after: String = str_slice(trimmed, 8, str_len(trimmed))
        let q: Int = str_index_of(after, "\"")
        if q > 0 {
            let mod: String = str_slice(after, 0, q)
            return dir + "/" + mod
        }
    }
    if str_starts_with(trimmed, "from ") {
        let after: String = str_slice(trimmed, 5, str_len(trimmed))
        // module name is the first whitespace-delimited token
        let sp: Int = str_index_of(after, " ")
        if sp > 0 {
            let mod_raw: String = str_slice(after, 0, sp)
            let mod: String = str_trim(mod_raw)
            if !str_eq(mod, "") {
                return dir + "/" + mod + ".el"
            }
        }
    }
    return ""
}

// Recursively resolve imports starting from src_path. Returns the combined
// source text with every imported module's body inlined ahead of the entry
// source, deduplicated by absolute path. Uses state_set to track which paths
// have already been pulled in for this run.
fn resolve_imports(src_path: String) -> String {
    let seen_key: String = "__elc_imp__:" + src_path
    let already: String = state_get(seen_key)
    if !str_eq(already, "") { return "" }
    state_set(seen_key, "1")

    let source: String = fs_read(src_path)
    let dir: String = dirname_of(src_path)
    let lines: [String] = str_split(source, "\n")
    let n: Int = native_list_len(lines)

    // First pass: pull in every import body ahead of this file's body.
    let prefix: String = ""
    let body: String = ""
    let i: Int = 0
    while i < n {
        let line: String = native_list_get(lines, i)
        let trimmed: String = str_trim(line)
        let imp_path: String = parse_import_line(trimmed, dir)
        if !str_eq(imp_path, "") {
            let prefix = prefix + resolve_imports(imp_path)
        } else {
            let body = body + line + "\n"
        }
        let i = i + 1
    }
    return prefix + body
}

// main — CLI entry point.
//
//   elc <source.el>                            # emit C to stdout
//   elc --target=js <source.el>                # emit JS to stdout
//   elc --target=c  <source.el> <out.c>        # write C to file
//   elc --target=js <source.el> <out.js>       # write JS to file
fn main() -> Void {
    let argv: [String] = args()
    // Use `tgt` not `target`: `target` is a reserved keyword in the lexer
    // (Section 1.5 of the language spec). detect_target itself is fine
    // because the function-name position has no token-class restriction.
    let tgt: String = detect_target(argv)
    let positional: [String] = strip_flags(argv)
    let argc: Int = native_list_len(positional)
    if argc < 1 {
        println("el-compiler: usage: elc [--target=c|js] <source.el> [<output>]")
        exit(1)
    }
    let src_path: String = native_list_get(positional, 0)
    let source: String = resolve_imports(src_path)
    let out: String = compile_dispatch(tgt, source)
    if argc >= 2 {
        let out_path: String = native_list_get(positional, 1)
        let ok: Bool = fs_write(out_path, out)
        if ok {
            exit(0)
        } else {
            println("el-compiler: failed to write output")
            exit(1)
        }
    }
    // No output path: codegen streamed to stdout already; out is "".
}
