// 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: [Any]) -> [Map<String, Any>]

// -- Token access helpers ------------------------------------------------------
// Tokens is a flat [Any] list: tokens[2*i] = kind, tokens[2*i+1] = value.
// This avoids one ElMap allocation per token (~112B each), saving ~4MB on large
// programs. All callers use these helpers -- only these three need updating.

fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
    let kind: String = native_list_get(tokens, pos * 3)
    let value: String = native_list_get(tokens, pos * 3 + 1)
    { "kind": kind, "value": value }
}

fn tok_kind(tokens: [Any], pos: Int) -> String {
    // Out-of-range reads must report the Eof sentinel so every `== "Eof"`
    // termination guard in the parser fires. Without this, reading past the
    // single trailing Eof token returns runtime null (el_list_get OOB -> 0),
    // which matches no delimiter, letting inner parse loops append AST nodes
    // forever on malformed input -> unbounded allocation -> OOM.
    let n: Int = native_list_len(tokens) / 3
    if pos < 0 {
        return "Eof"
    }
    if pos >= n {
        return "Eof"
    }
    native_list_get(tokens, pos * 3)
}

fn tok_line(tokens: [Any], pos: Int) -> String {
    let n: Int = native_list_len(tokens) / 3
    if pos < 0 { return "0" }
    if pos >= n { return "0" }
    native_list_get(tokens, pos * 3 + 2)
}

fn tok_value(tokens: [Any], pos: Int) -> String {
    let n: Int = native_list_len(tokens) / 3
    if pos < 0 {
        return ""
    }
    if pos >= n {
        return ""
    }
    native_list_get(tokens, pos * 3 + 1)
}

// parse_progress_fatal — robustness backstop. Called by the token-consuming
// driver loops when they detect they have iterated more times than there are
// tokens (impossible for a well-formed program, where every iteration consumes
// at least one token). Names the offending token and exits non-zero instead of
// looping forever / exhausting memory.
fn parse_progress_fatal(where: String, tokens: [Any], pos: Int) -> Void {
    let k: String = tok_kind(tokens, pos)
    let v: String = tok_value(tokens, pos)
    println("elc: FATAL: parser made no forward progress in " + where
        + " at token index " + native_int_to_str(pos) + " (kind=" + k + ")")
    println("elc: likely a malformed construct near '" + v
        + "' — e.g. an unterminated string or an unescaped double-quote inside a string literal (use \\\" ).")
    exit(1)
}

fn expect(tokens: [Any], pos: Int, kind: String) -> Int {
    let k = tok_kind(tokens, pos)
    if k == kind {
        return pos + 1
    }
    // On mismatch, error recovery is best-effort. But never step PAST the Eof
    // sentinel: once at Eof a mismatch means the input ended early, and
    // advancing would run the cursor off the token list.
    if k == "Eof" {
        return pos
    }
    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: [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: [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 --------------------------------------------------------

// ── HTML template parser ──────────────────────────────────────────────────────
//
// HTML templates are written as unquoted HTML in expression position:
//   return <div class="x"><h1>{title}</h1></div>
//
// The parser detects an HTML template when parse_primary sees Lt followed
// by a lowercase ident (a known or assumed HTML element name) or `!` (for
// <!doctype html>). It then recursively parses the HTML into an AST.
//
// AST nodes produced:
//   { "expr": "HtmlTemplate", "root": child_node }
//   { "html": "Element", "tag": "div", "attrs": [...], "children": [...], "self_closing": bool }
//   { "html": "Text", "text": "..." }
//   { "html": "Interp", "value": expr_node }
//   { "html": "Each", "list": expr_node, "item": "name", "body": [...] }
//   { "html": "Doctype" }
//   { "html": "Raw", "value": expr_node }

fn is_html_tag_name(name: String) -> Bool {
    if str_eq(name, "a") { return true }
    if str_eq(name, "abbr") { return true }
    if str_eq(name, "address") { return true }
    if str_eq(name, "area") { return true }
    if str_eq(name, "article") { return true }
    if str_eq(name, "aside") { return true }
    if str_eq(name, "audio") { return true }
    if str_eq(name, "b") { return true }
    if str_eq(name, "base") { return true }
    if str_eq(name, "blockquote") { return true }
    if str_eq(name, "body") { return true }
    if str_eq(name, "br") { return true }
    if str_eq(name, "button") { return true }
    if str_eq(name, "canvas") { return true }
    if str_eq(name, "caption") { return true }
    if str_eq(name, "cite") { return true }
    if str_eq(name, "code") { return true }
    if str_eq(name, "col") { return true }
    if str_eq(name, "colgroup") { return true }
    if str_eq(name, "data") { return true }
    if str_eq(name, "datalist") { return true }
    if str_eq(name, "dd") { return true }
    if str_eq(name, "del") { return true }
    if str_eq(name, "details") { return true }
    if str_eq(name, "dfn") { return true }
    if str_eq(name, "dialog") { return true }
    if str_eq(name, "div") { return true }
    if str_eq(name, "dl") { return true }
    if str_eq(name, "dt") { return true }
    if str_eq(name, "em") { return true }
    if str_eq(name, "embed") { return true }
    if str_eq(name, "fieldset") { return true }
    if str_eq(name, "figcaption") { return true }
    if str_eq(name, "figure") { return true }
    if str_eq(name, "footer") { return true }
    if str_eq(name, "form") { return true }
    if str_eq(name, "h1") { return true }
    if str_eq(name, "h2") { return true }
    if str_eq(name, "h3") { return true }
    if str_eq(name, "h4") { return true }
    if str_eq(name, "h5") { return true }
    if str_eq(name, "h6") { return true }
    if str_eq(name, "head") { return true }
    if str_eq(name, "header") { return true }
    if str_eq(name, "hr") { return true }
    if str_eq(name, "html") { return true }
    if str_eq(name, "i") { return true }
    if str_eq(name, "iframe") { return true }
    if str_eq(name, "img") { return true }
    if str_eq(name, "input") { return true }
    if str_eq(name, "ins") { return true }
    if str_eq(name, "kbd") { return true }
    if str_eq(name, "label") { return true }
    if str_eq(name, "legend") { return true }
    if str_eq(name, "li") { return true }
    if str_eq(name, "link") { return true }
    if str_eq(name, "main") { return true }
    if str_eq(name, "map") { return true }
    if str_eq(name, "mark") { return true }
    if str_eq(name, "menu") { return true }
    if str_eq(name, "meta") { return true }
    if str_eq(name, "meter") { return true }
    if str_eq(name, "nav") { return true }
    if str_eq(name, "noscript") { return true }
    if str_eq(name, "object") { return true }
    if str_eq(name, "ol") { return true }
    if str_eq(name, "optgroup") { return true }
    if str_eq(name, "option") { return true }
    if str_eq(name, "output") { return true }
    if str_eq(name, "p") { return true }
    if str_eq(name, "param") { return true }
    if str_eq(name, "picture") { return true }
    if str_eq(name, "pre") { return true }
    if str_eq(name, "progress") { return true }
    if str_eq(name, "q") { return true }
    if str_eq(name, "rp") { return true }
    if str_eq(name, "rt") { return true }
    if str_eq(name, "ruby") { return true }
    if str_eq(name, "s") { return true }
    if str_eq(name, "samp") { return true }
    if str_eq(name, "script") { return true }
    if str_eq(name, "section") { return true }
    if str_eq(name, "select") { return true }
    if str_eq(name, "small") { return true }
    if str_eq(name, "source") { return true }
    if str_eq(name, "span") { return true }
    if str_eq(name, "strong") { return true }
    if str_eq(name, "style") { return true }
    if str_eq(name, "sub") { return true }
    if str_eq(name, "summary") { return true }
    if str_eq(name, "sup") { return true }
    if str_eq(name, "table") { return true }
    if str_eq(name, "tbody") { return true }
    if str_eq(name, "td") { return true }
    if str_eq(name, "template") { return true }
    if str_eq(name, "textarea") { return true }
    if str_eq(name, "tfoot") { return true }
    if str_eq(name, "th") { return true }
    if str_eq(name, "thead") { return true }
    if str_eq(name, "time") { return true }
    if str_eq(name, "title") { return true }
    if str_eq(name, "tr") { return true }
    if str_eq(name, "track") { return true }
    if str_eq(name, "u") { return true }
    if str_eq(name, "ul") { return true }
    if str_eq(name, "var") { return true }
    if str_eq(name, "video") { return true }
    if str_eq(name, "wbr") { return true }
    false
}

fn is_void_element(name: String) -> Bool {
    if str_eq(name, "area") { return true }
    if str_eq(name, "base") { return true }
    if str_eq(name, "br") { return true }
    if str_eq(name, "col") { return true }
    if str_eq(name, "embed") { return true }
    if str_eq(name, "hr") { return true }
    if str_eq(name, "img") { return true }
    if str_eq(name, "input") { return true }
    if str_eq(name, "link") { return true }
    if str_eq(name, "meta") { return true }
    if str_eq(name, "param") { return true }
    if str_eq(name, "source") { return true }
    if str_eq(name, "track") { return true }
    if str_eq(name, "wbr") { return true }
    false
}

// Collect all tokens as raw text until </tag_name> is encountered.
// Used for <style> and <script> elements to avoid parsing CSS/JS as El.
// Returns { "text": "...", "pos": p_after_closing_tag }
fn parse_raw_text_content(tokens: [Any], pos: Int, tag_name: String) -> Map<String, Any> {
    let parts: [String] = native_list_empty()
    let p = pos
    let running = true
    while running {
        let k = tok_kind(tokens, p)
        if str_eq(k, "Eof") {
            let running = false
        } else {
            if str_eq(k, "Lt") {
                let k2 = tok_kind(tokens, p + 1)
                if str_eq(k2, "Slash") {
                    // Check if this is </tag_name>
                    let close_name = tok_value(tokens, p + 2)
                    if str_eq(close_name, tag_name) {
                        // consume </tag_name>
                        let p = p + 3
                        let p = expect(tokens, p, "Gt")
                        let running = false
                    } else {
                        let v = tok_value(tokens, p)
                        let parts = native_list_append(parts, v)
                        let p = p + 1
                    }
                } else {
                    let v = tok_value(tokens, p)
                    let parts = native_list_append(parts, v)
                    let p = p + 1
                }
            } else {
                let v = tok_value(tokens, p)
                let parts = native_list_append(parts, v)
                let p = p + 1
            }
        }
    }
    { "text": str_join(parts, ""), "pos": p }
}

// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
fn parse_html_text_tokens(tokens: [Any], pos: Int) -> Map<String, Any> {
    let parts: [String] = native_list_empty()
    let p = pos
    let running = true
    while running {
        let k = tok_kind(tokens, p)
        if str_eq(k, "Eof") {
            let running = false
        } else {
            if str_eq(k, "Lt") {
                let running = false
            } else {
                if str_eq(k, "LBrace") {
                    let running = false
                } else {
                    // Check for </: Lt already stops us, but Slash alone
                    // (after consuming whitespace) also stops text.
                    // Anything else is text content.
                    let v = tok_value(tokens, p)
                    let parts = native_list_append(parts, v)
                    let p = p + 1
                }
            }
        }
    }
    { "text": str_join(parts, ""), "pos": p }
}

// Parse an attribute list: (attrname | attrname="val" | attrname={expr})*
// Stops at Gt or Slash (for self-closing />).
fn parse_html_attrs(tokens: [Any], pos: Int) -> Map<String, Any> {
    let attrs: [Map<String, Any>] = native_list_empty()
    let p = pos
    let running = true
    while running {
        let k = tok_kind(tokens, p)
        if str_eq(k, "Gt") {
            let running = false
        } else {
            if str_eq(k, "Slash") {
                let running = false
            } else {
                if str_eq(k, "Eof") {
                    let running = false
                } else {
                    // Attribute name: could be Ident or keyword used as attr name
                    let attr_name = tok_value(tokens, p)
                    let p = p + 1
                    let k2 = tok_kind(tokens, p)
                    if str_eq(k2, "Eq") {
                        let p = p + 1
                        let k3 = tok_kind(tokens, p)
                        if str_eq(k3, "Str") {
                            // static: attr="value"
                            let attr_val = tok_value(tokens, p)
                            let p = p + 1
                            let attrs = native_list_append(attrs, { "name": attr_name, "kind": "static", "value": attr_val })
                        } else {
                            if str_eq(k3, "LBrace") {
                                // dynamic: attr={expr}
                                let r = parse_expr(tokens, p + 1)
                                let val_node = r["node"]
                                let p = r["pos"]
                                // r result map fully consumed — release to free peak heap.
                                el_release(r)
                                let p = expect(tokens, p, "RBrace")
                                let attrs = native_list_append(attrs, { "name": attr_name, "kind": "dynamic", "value": val_node })
                            } else {
                                // malformed, skip
                            }
                        }
                    } else {
                        // boolean attribute
                        let attrs = native_list_append(attrs, { "name": attr_name, "kind": "bool" })
                    }
                }
            }
        }
    }
    { "attrs": attrs, "pos": p }
}

// Parse the children of an HTML element until we see the closing tag </tag>
// or EOF. Returns { "children": [...], "pos": p_after_closing_tag }
fn parse_html_children(tokens: [Any], pos: Int, parent_tag: String) -> Map<String, Any> {
    let children: [Map<String, Any>] = native_list_empty()
    let p = pos
    let running = true
    while running {
        let k = tok_kind(tokens, p)
        if str_eq(k, "Eof") {
            let running = false
        } else {
            if str_eq(k, "Lt") {
                // Check for closing tag: </
                let k2 = tok_kind(tokens, p + 1)
                if str_eq(k2, "Slash") {
                    // </tagname> — consume and stop
                    let p = p + 2
                    // skip tag name
                    let close_name = tok_value(tokens, p)
                    let p = p + 1
                    // consume >
                    let p = expect(tokens, p, "Gt")
                    let running = false
                } else {
                    if str_eq(k2, "Not") {
                        // Possible <!doctype html>
                        let k3_v = tok_value(tokens, p + 2)
                        if str_eq(k3_v, "doctype") {
                            // consume <!doctype html>
                            let p = p + 2
                            // skip until >
                            let scanning = true
                            while scanning {
                                let ck = tok_kind(tokens, p)
                                if str_eq(ck, "Eof") { let scanning = false }
                                if str_eq(ck, "Gt") {
                                    let p = p + 1
                                    let scanning = false
                                } else {
                                    let p = p + 1
                                }
                            }
                            let children = native_list_append(children, { "html": "Doctype" })
                        } else {
                            let p = p + 1
                        }
                    } else {
                        // nested element
                        let r = parse_html_element(tokens, p)
                        let child = r["node"]
                        let p = r["pos"]
                        // r result map fully consumed — release to free peak heap.
                        el_release(r)
                        let children = native_list_append(children, child)
                    }
                }
            } else {
                if str_eq(k, "LBrace") {
                    // Interpolation: {expr}, {#each ...}, {#if ...}, {#else}, {/each}, {/if}
                    // Note: '#' (ASCII 35) is skipped by the lexer, so {#each} lexes as
                    // LBrace Ident:"each" ... and {#if} lexes as LBrace If ... and
                    // {#else} lexes as LBrace Else RBrace.
                    let k2 = tok_kind(tokens, p + 1)
                    if str_eq(k2, "Slash") {
                        // {/each} or {/if} — end of block, stop
                        // skip { /
                        let p = p + 2
                        // skip the close-tag name token (each, if, etc.)
                        let p = p + 1
                        // skip }
                        let p = expect(tokens, p, "RBrace")
                        let running = false
                    } else {
                        if str_eq(k2, "If") {
                            // {#if condition} ... {/if}
                            // Skip { if (2 tokens; '#' was silently skipped by lexer)
                            let p = p + 2
                            // Parse condition expression (no block expr)
                            let prev_no_block: String = state_get("__no_block_expr")
                            state_set("__no_block_expr", "1")
                            let r_cond = parse_expr(tokens, p)
                            state_set("__no_block_expr", prev_no_block)
                            let cond_expr = r_cond["node"]
                            let p = r_cond["pos"]
                            el_release(r_cond)
                            // consume closing }
                            let p = expect(tokens, p, "RBrace")
                            // parse then-children until {#else} or {/if}
                            let r_then = parse_html_children(tokens, p, "__if_then__")
                            let then_children = r_then["children"]
                            let p = r_then["pos"]
                            el_release(r_then)
                            // check for {#else} — lexed as LBrace Else RBrace
                            let else_children: [Map<String, Any>] = native_list_empty()
                            let ck = tok_kind(tokens, p)
                            if str_eq(ck, "LBrace") {
                                let ck2 = tok_kind(tokens, p + 1)
                                if str_eq(ck2, "Else") {
                                    // consume { else }
                                    let p = p + 2
                                    let p = expect(tokens, p, "RBrace")
                                    // parse else-children until {/if}
                                    let r_else = parse_html_children(tokens, p, "__if_else__")
                                    let else_children = r_else["children"]
                                    let p = r_else["pos"]
                                    el_release(r_else)
                                }
                            }
                            let if_node: Map<String, Any> = { "html": "HtmlIf", "cond": cond_expr, "then": then_children, "else": else_children }
                            let children = native_list_append(children, if_node)
                        } else {
                            if str_eq(k2, "Else") {
                                // {#else} sentinel — lexed as LBrace Else RBrace
                                // Do NOT consume — leave position for caller ({#if} handler checks for it)
                                let running = false
                            } else {
                                // Check for {#each list as item} — lexed as LBrace Ident:"each" ...
                                let k2_v = tok_value(tokens, p + 1)
                                if str_eq(k2_v, "each") {
                                    let p = p + 2
                                    // parse list expr up to "as" keyword
                                    let prev_no_block: String = state_get("__no_block_expr")
                                    state_set("__no_block_expr", "1")
                                    let r_list = parse_expr(tokens, p)
                                    state_set("__no_block_expr", prev_no_block)
                                    let list_expr = r_list["node"]
                                    let p = r_list["pos"]
                                    // r_list result map fully consumed — release to free peak heap.
                                    el_release(r_list)
                                    // expect "as"
                                    let p = expect(tokens, p, "As")
                                    // item variable name
                                    let item_name = tok_value(tokens, p)
                                    let p = p + 1
                                    // consume closing }
                                    let p = expect(tokens, p, "RBrace")
                                    // parse body until {/each}
                                    let r_body = parse_html_each_body(tokens, p)
                                    let body_children = r_body["children"]
                                    let p = r_body["pos"]
                                    // r_body result map fully consumed — release to free peak heap.
                                    el_release(r_body)
                                    let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
                                    let children = native_list_append(children, each_node)
                                } else {
                                    // regular {expr} — disable map-literal parsing so {fn(a,b)}
                                    // does not trigger the LBrace→map path inside parse_primary
                                    let prev_no_block: String = state_get("__no_block_expr")
                                    state_set("__no_block_expr", "1")
                                    let r = parse_expr(tokens, p + 1)
                                    state_set("__no_block_expr", prev_no_block)
                                    let interp_val = r["node"]
                                    let p = r["pos"]
                                    // r result map fully consumed — release to free peak heap.
                                    el_release(r)
                                    let p = expect(tokens, p, "RBrace")
                                    // Check if the expr is a call to raw()
                                    let is_raw_call = false
                                    let interp_kind: String = interp_val["expr"]
                                    if str_eq(interp_kind, "Call") {
                                        let fn_node = interp_val["func"]
                                        let fn_kind: String = fn_node["expr"]
                                        if str_eq(fn_kind, "Ident") {
                                            let fn_name_v: String = fn_node["name"]
                                            if str_eq(fn_name_v, "raw") {
                                                let is_raw_call = true
                                            }
                                        }
                                    }
                                    if is_raw_call {
                                        let raw_args = interp_val["args"]
                                        let raw_inner = native_list_get(raw_args, 0)
                                        let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
                                    } else {
                                        let children = native_list_append(children, { "html": "Interp", "value": interp_val })
                                    }
                                }
                            }
                        }
                    }
                } else {
                    // Text tokens — collect run of non-special tokens
                    let r_text = parse_html_text_tokens(tokens, p)
                    let text_str: String = r_text["text"]
                    let p = r_text["pos"]
                    // r_text result map fully consumed — release to free peak heap.
                    el_release(r_text)
                    let text_trimmed: String = str_trim(text_str)
                    if !str_eq(text_trimmed, "") {
                        let children = native_list_append(children, { "html": "Text", "text": text_trimmed })
                    }
                }
            }
        }
    }
    { "children": children, "pos": p }
}

// Parse body of {#each} until {/each}. Mirrors parse_html_children but
// stops at the {/each} sentinel rather than a closing element tag.
fn parse_html_each_body(tokens: [Any], pos: Int) -> Map<String, Any> {
    parse_html_children(tokens, pos, "__each__")
}

// Parse a single HTML element: <tag attrs> children </tag>
// or self-closing: <tag attrs/>
// Pos points to the Lt token.
fn parse_html_element(tokens: [Any], pos: Int) -> Map<String, Any> {
    let p = pos
    // consume <
    let p = expect(tokens, p, "Lt")
    // tag name
    let tag_name = tok_value(tokens, p)
    let p = p + 1
    // parse attributes
    let r_attrs = parse_html_attrs(tokens, p)
    let attrs = r_attrs["attrs"]
    let p = r_attrs["pos"]
    // r_attrs result map fully consumed — release to free peak heap.
    el_release(r_attrs)
    // check for self-closing /> or void element
    let k = tok_kind(tokens, p)
    let self_closing = false
    if str_eq(k, "Slash") {
        // />
        let p = p + 1
        let p = expect(tokens, p, "Gt")
        let self_closing = true
        return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
    }
    // consume >
    let p = expect(tokens, p, "Gt")
    // void elements have no children, no closing tag
    if is_void_element(tag_name) {
        return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
    }
    // raw-text mode for style/script — collect content as plain text without parsing CSS/JS as El
    if str_eq(tag_name, "style") {
        let r_raw = parse_raw_text_content(tokens, p, "style")
        let raw_text: String = r_raw["text"]
        let p = r_raw["pos"]
        el_release(r_raw)
        let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
        let raw_children: [Map<String, Any>] = native_list_empty()
        let raw_children = native_list_append(raw_children, raw_child)
        return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
    }
    if str_eq(tag_name, "script") {
        let r_raw = parse_raw_text_content(tokens, p, "script")
        let raw_text: String = r_raw["text"]
        let p = r_raw["pos"]
        el_release(r_raw)
        let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
        let raw_children: [Map<String, Any>] = native_list_empty()
        let raw_children = native_list_append(raw_children, raw_child)
        return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
    }
    // parse children
    let r_children = parse_html_children(tokens, p, tag_name)
    let children = r_children["children"]
    let p = r_children["pos"]
    // r_children result map fully consumed — release to free peak heap.
    el_release(r_children)
    make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": children, "self_closing": false }, p)
}

// Entry point for HTML template parsing.
// Pos points to Lt (or Lt Not for <!doctype>).
// May parse an optional <!doctype html> prefix followed by the root element.
fn parse_html_template(tokens: [Any], pos: Int) -> Map<String, Any> {
    let p = pos
    // Check for <!doctype html>
    let doctype = false
    let k = tok_kind(tokens, p)
    let k2 = tok_kind(tokens, p + 1)
    if str_eq(k, "Lt") {
        if str_eq(k2, "Not") {
            let k3_v = tok_value(tokens, p + 2)
            if str_eq(k3_v, "doctype") {
                let doctype = true
                // consume <!doctype html>
                let p = p + 2
                let scanning = true
                while scanning {
                    let ck = tok_kind(tokens, p)
                    if str_eq(ck, "Eof") { let scanning = false }
                    if str_eq(ck, "Gt") {
                        let p = p + 1
                        let scanning = false
                    } else {
                        let p = p + 1
                    }
                }
            }
        }
    }
    // Parse root element
    let r = parse_html_element(tokens, p)
    let root = r["node"]
    let p = r["pos"]
    // r result map fully consumed — release to free peak heap.
    el_release(r)
    let root_with_doctype = root
    if doctype {
        let root_with_doctype = { "html": root["html"], "tag": root["tag"], "attrs": root["attrs"], "children": root["children"], "self_closing": root["self_closing"], "doctype": true }
    }
    make_result({ "expr": "HtmlTemplate", "root": root_with_doctype }, p)
}

fn parse_primary(tokens: [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)
    }

    // HTML template literal: <tagname ...>...</tagname> or <!doctype html>...
    // Detected in value position only; `<` in comparison position is already
    // consumed by parse_binop before parse_primary is reached.
    if k == "Lt" {
        let k2 = tok_kind(tokens, pos + 1)
        if str_eq(k2, "Not") {
            return parse_html_template(tokens, pos)
        }
        if str_eq(k2, "Ident") {
            let tag_candidate = tok_value(tokens, pos + 1)
            if is_html_tag_name(tag_candidate) {
                return parse_html_template(tokens, pos)
            }
        }
    }

    // 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"]
        // r result map fully consumed — release to free peak heap.
        el_release(r)
        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"]
                    // r result map fully consumed — release to free peak heap.
                    el_release(r)
                    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)
        }
        // Distinguish map literal from interpolation chain.
        // A map literal requires { key: value } — the second token inside { must be Colon.
        // An empty {} is a map literal.  Everything else is an interpolation chain.
        let first_k: String = tok_kind(tokens, pos + 1)
        let second_k: String = tok_kind(tokens, pos + 2)
        if str_eq(first_k, "RBrace") {
            // Empty map literal {}
            return make_result({ "expr": "Map", "pairs": native_list_empty() }, pos + 2)
        }
        if str_eq(second_k, "Colon") {
            // MAP LITERAL: { key: value, ... }
            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 or Ident 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"]
                        // r result map fully consumed — release to free peak heap.
                        el_release(r)
                        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)
        }
        // INTERPOLATION CHAIN: {expr}, {expr}{expr}, {expr}<html>, etc.
        // Build a BinOp(Plus, ...) concatenation chain.
        let p = pos
        let chain_node: Map<String, Any> = { "expr": "Nil" }
        let chain_started = false
        let chain_running = true
        while chain_running {
            let ck: String = tok_kind(tokens, p)
            if str_eq(ck, "LBrace") {
                let prev_no_block: String = state_get("__no_block_expr")
                state_set("__no_block_expr", "1")
                let r = parse_expr(tokens, p + 1)
                state_set("__no_block_expr", prev_no_block)
                let part = r["node"]
                let p = r["pos"]
                // r result map fully consumed — release to free peak heap.
                el_release(r)
                let p = expect(tokens, p, "RBrace")
                if !chain_started {
                    let chain_node = part
                    let chain_started = true
                } else {
                    let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
                }
            } else {
                if str_eq(ck, "Lt") {
                    let ck2: String = tok_kind(tokens, p + 1)
                    if str_eq(ck2, "Not") {
                        let r = parse_html_template(tokens, p)
                        let part = r["node"]
                        let p = r["pos"]
                        // r result map fully consumed — release to free peak heap.
                        el_release(r)
                        if !chain_started {
                            let chain_node = part
                            let chain_started = true
                        } else {
                            let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
                        }
                    } else {
                        if str_eq(ck2, "Ident") {
                            let tag_candidate: String = tok_value(tokens, p + 1)
                            if is_html_tag_name(tag_candidate) {
                                let r = parse_html_template(tokens, p)
                                let part = r["node"]
                                let p = r["pos"]
                                // r result map fully consumed — release to free peak heap.
                                el_release(r)
                                if !chain_started {
                                    let chain_node = part
                                    let chain_started = true
                                } else {
                                    let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
                                }
                            } else {
                                let chain_running = false
                            }
                        } else {
                            let chain_running = false
                        }
                    }
                } else {
                    let chain_running = false
                }
            }
        }
        return make_result(chain_node, 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
    }

    // Anonymous function literal (lambda): fn(params) -> RetType { body }
    // Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... })
    // Produces a Lambda expression node (distinct from a named FnDef statement).
    if k == "Fn" {
        let p = pos + 1
        let r = parse_params(tokens, p)
        let params = r["params"]
        let p = r["pos"]
        // r result map fully consumed — release to free peak heap.
        el_release(r)
        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"]
        // r2 result map fully consumed — release to free peak heap.
        el_release(r2)
        return make_result({ "expr": "Lambda", "params": params, "body": body, "ret_type": ret_type }, p)
    }

    // Unary not
    if k == "Not" {
        let r = parse_primary(tokens, pos + 1)
        let inner = r["node"]
        let p = r["pos"]
        // r result map fully consumed — release to free peak heap.
        el_release(r)
        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"]
        // r result map fully consumed — release to free peak heap.
        el_release(r)
        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: [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"]
    // r result map fully consumed — release to free peak heap.
    el_release(r)
    let r2 = parse_block(tokens, p)
    let then_stmts = r2["stmts"]
    let p = r2["pos"]
    // r2 result map fully consumed — release to free peak heap.
    el_release(r2)
    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"]
            // r3 result map fully consumed — release to free peak heap.
            el_release(r3)
            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"]
            // r3 result map fully consumed — release to free peak heap.
            el_release(r3)
            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: [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"]
    // r result map fully consumed — release to free peak heap.
    el_release(r)
    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"]
                // r2 result map fully consumed — release to free peak heap.
                el_release(r2)
                let p = expect(tokens, p, "FatArrow")
                let r3 = parse_expr(tokens, p)
                let body = r3["node"]
                let p = r3["pos"]
                // r3 result map fully consumed — release to free peak heap.
                el_release(r3)
                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: [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)
        }
        // Check for Enum::Variant pattern (Color::Red, Status::Ok, etc.)
        // Lexed as: Ident ColonColon Ident
        let next_k = tok_kind(tokens, pos + 1)
        if next_k == "ColonColon" {
            let variant_name = tok_value(tokens, pos + 2)
            return make_result({ "pattern": "Variant", "enum_name": v, "variant": variant_name }, pos + 3)
        }
        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: [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"]
    // r result map fully consumed — release to free peak heap.
    el_release(r)
    let r2 = parse_block(tokens, p)
    let body = r2["stmts"]
    let p = r2["pos"]
    // r2 result map fully consumed — release to free peak heap.
    el_release(r2)
    make_result({ "expr": "For", "item": item_name, "list": list_expr, "body": body }, p)
}

fn parse_block(tokens: [Any], pos: Int) -> Map<String, Any> {
    let p = expect(tokens, pos, "LBrace")
    let stmts: [Map<String, Any>] = native_list_empty()
    let running = true
    // Runaway backstop: a block can hold at most (token count) statements, since
    // every iteration consumes >= 1 token. If we exceed that, the cursor has run
    // off the end without terminating (malformed input) -> fail fast, don't hang.
    let blk_total: Int = native_list_len(tokens) / 3
    let blk_iters: Int = 0
    while running {
        let blk_iters = blk_iters + 1
        if blk_iters > blk_total + 8 {
            parse_progress_fatal("parse_block", tokens, p)
        }
        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"]
                // r result map fully consumed — release to free peak heap.
                el_release(r)
                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: [Any], pos: Int) -> Map<String, Any> {
    let r = parse_primary(tokens, pos)
    let node = r["node"]
    let p = r["pos"]
    // r result map fully consumed — release to free peak heap.
    el_release(r)

    // 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"]
                        // r2 result map fully consumed — release to free peak heap.
                        el_release(r2)
                        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"]
                    // r2 result map fully consumed — release to free peak heap.
                    el_release(r2)
                    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 }
    if kind == "Percent" { 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 }
    if kind == "Percent" { return true }
    false
}

fn parse_binop(tokens: [Any], pos: Int, min_prec: Int) -> Map<String, Any> {
    let r = parse_postfix(tokens, pos)
    let left = r["node"]
    let p = r["pos"]
    // r result map fully consumed — release to free peak heap.
    el_release(r)
    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"]
                // r2 result map fully consumed — release to free peak heap.
                el_release(r2)
                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: [Any], pos: Int) -> Map<String, Any> {
    parse_binop(tokens, pos, 1)
}

// -- Statement parsing ---------------------------------------------------------

fn parse_stmt(tokens: [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"]
        // r result map fully consumed — release to free peak heap.
        el_release(r)
        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"]
        // r result map fully consumed — release to free peak heap.
        el_release(r)
        return make_result({ "stmt": "Return", "value": val }, p)
    }

    // extern fn declaration (no body - forward declaration for separate compilation)
    if k == "Extern" {
        let p = pos + 1
        let k2: String = tok_kind(tokens, p)
        if str_eq(k2, "Fn") {
            let p = p + 1
            let name: String = tok_value(tokens, p)
            let p = p + 1
            let r = parse_params(tokens, p)
            let params = r["params"]
            let p = r["pos"]
            // r result map fully consumed — release to free peak heap.
            el_release(r)
            let ret_type = ""
            let k3: String = tok_kind(tokens, p)
            if str_eq(k3, "Arrow") {
                let p = p + 1
                let kt: String = tok_kind(tokens, p)
                if str_eq(kt, "Ident") { let ret_type = tok_value(tokens, p) }
                let p = skip_type(tokens, p)
            }
            return make_result({ "stmt": "ExternFn", "name": name, "params": params, "ret_type": ret_type }, 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"]
        // r result map fully consumed — release to free peak heap.
        el_release(r)
        // 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"]
        // r2 result map fully consumed — release to free peak heap.
        el_release(r2)
        // The definition carries the line it was written on. Without it no
        // diagnostic can name a place, and after textual inlining there is no
        // way to say which FILE a definition came from.
        return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type, "line": tok_line(tokens, pos) }, p)
    }

    // type definition: `type Name = { field: Type, ... }`
    // The `=` between the name and the brace is optional in the spec but
    // present in practice. Skip it if present before consuming the LBrace.
    if k == "Type" {
        let p = pos + 1
        let name = tok_value(tokens, p)
        let p = p + 1
        // Consume optional `=` before the opening brace
        let pk = tok_kind(tokens, p)
        if pk == "Eq" { 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"]
        // r result map fully consumed — release to free peak heap.
        el_release(r)
        let r2 = parse_block(tokens, p)
        let body = r2["stmts"]
        let p = r2["pos"]
        // r2 result map fully consumed — release to free peak heap.
        el_release(r2)
        return make_result({ "stmt": "While", "cond": cond, "body": body }, p)
    }

    // break statement
    if k == "Break" {
        return make_result({ "stmt": "Break" }, pos + 1)
    }

    // continue statement
    if k == "Continue" {
        return make_result({ "stmt": "Continue" }, pos + 1)
    }

    // for loop (range or list iteration)
    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 start_expr = r["node"]
        let p = r["pos"]
        // r result map fully consumed — release to free peak heap.
        el_release(r)
        // Check for range operator: .. (exclusive) or ..= (inclusive)
        let range_k = tok_kind(tokens, p)
        if range_k == "DotDot" {
            // exclusive range: for i in start..end
            let p = p + 1
            let r2 = parse_expr(tokens, p)
            let end_expr = r2["node"]
            let p = r2["pos"]
            el_release(r2)
            let r3 = parse_block(tokens, p)
            let body = r3["stmts"]
            let p = r3["pos"]
            el_release(r3)
            return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": false, "body": body }, p)
        }
        if range_k == "DotDotEq" {
            // inclusive range: for i in start..=end
            let p = p + 1
            let r2 = parse_expr(tokens, p)
            let end_expr = r2["node"]
            let p = r2["pos"]
            el_release(r2)
            let r3 = parse_block(tokens, p)
            let body = r3["stmts"]
            let p = r3["pos"]
            el_release(r3)
            return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": true, "body": body }, p)
        }
        // No range operator: regular for-in (list iteration)
        let list_expr = start_expr
        let r2 = parse_block(tokens, p)
        let body = r2["stmts"]
        let p = r2["pos"]
        el_release(r2)
        return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
    }

    // try/catch statement
    // try { body } catch (name: Type) { handler }
    // The catch variable name and type are both captured; type is skipped.
    if k == "Try" {
        let p = pos + 1
        let r_try = parse_block(tokens, p)
        let try_body = r_try["stmts"]
        let p = r_try["pos"]
        // r_try result map fully consumed — release to free peak heap.
        el_release(r_try)
        let catch_name = "err"
        let k2 = tok_kind(tokens, p)
        if str_eq(k2, "Catch") {
            let p = p + 1
            let p = expect(tokens, p, "LParen")
            // catch variable name
            let kn = tok_kind(tokens, p)
            if str_eq(kn, "Ident") {
                let catch_name = tok_value(tokens, p)
                let p = p + 1
            }
            // optional type annotation: : Type
            let k3 = tok_kind(tokens, p)
            if str_eq(k3, "Colon") {
                let p = p + 1
                let p = skip_type(tokens, p)
            }
            let p = expect(tokens, p, "RParen")
            let r_catch = parse_block(tokens, p)
            let catch_body = r_catch["stmts"]
            let p = r_catch["pos"]
            // r_catch result map fully consumed — release to free peak heap.
            el_release(r_catch)
            return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": catch_body }, p)
        }
        return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": native_list_empty() }, p)
    }

    // @decorator - capture decorator name (and optional string args) and
    // attach to the following stmt. Backward-compatible: bare @manager /
    // @engine / @accessor still parse (no parens -> empty args). Decorators
    // STACK: `@route("/p","GET") @manager fn f()` attaches BOTH to f via a
    // `decorators` list [{name, args}]. The legacy `decorator` string is kept
    // populated (topmost decorator) so the JS backend keeps working unchanged.
    if k == "At" {
        let p = pos + 1
        let dec_name = tok_value(tokens, p)
        let p = p + 1
        // Optional decorator argument list: @name("a", "b", ...)
        let dec_args = native_list_empty()
        let ka = tok_kind(tokens, p)
        if str_eq(ka, "LParen") {
            let p = p + 1
            let running_da = true
            while running_da {
                let kd = tok_kind(tokens, p)
                if str_eq(kd, "RParen") {
                    let running_da = false
                } else {
                    if str_eq(kd, "Eof") {
                        let running_da = false
                    } else {
                        if str_eq(kd, "Str") {
                            let dec_args = native_list_append(dec_args, tok_value(tokens, p))
                        }
                        let p = p + 1
                        let kc = tok_kind(tokens, p)
                        if str_eq(kc, "Comma") {
                            let p = p + 1
                        }
                    }
                }
            }
            let p = expect(tokens, p, "RParen")
        }
        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") {
            // Stack this decorator (topmost-first) onto any decorators the inner
            // FnDef already carries from decorators written below this one.
            let this_dec = { "name": dec_name, "args": dec_args }
            let existing = inner["decorators"]
            let dlist = native_list_empty()
            let dlist = native_list_append(dlist, this_dec)
            let ne: Int = native_list_len(existing)
            let ei = 0
            while ei < ne {
                let dlist = native_list_append(dlist, native_list_get(existing, ei))
                let ei = ei + 1
            }
            let with_dec = {
                "stmt": "FnDef",
                "name": inner["name"],
                "params": inner["params"],
                "body": inner["body"],
                "ret_type": inner["ret_type"],
                "line": inner["line"],
                "decorator": dec_name,
                "decorators": dlist
            }
            // r result map fully consumed — release to free peak heap.
            el_release(r)
            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)
    }

    // program block: program "name" { singleton: "id", env NAME: Type = "default", ... }
    //
    // The program block is El's declaration surface for CROSS-CUTTING CONCERNS —
    // properties of the whole process rather than of any one function, which
    // otherwise degrade into "remember to call this at every site" conventions.
    //
    //   singleton: "id"    — process identity. The runtime takes an exclusive
    //                        lock at startup; a SECOND start is refused, loudly,
    //                        instead of two processes sharing one data dir.
    //   guards: <expr>     — WHAT that singleton protects: an expression yielding
    //                        the path of the guarded state directory, evaluated at
    //                        startup. MANDATORY with `singleton:`, because a lock
    //                        keyed on a program's NAME rather than on its STATE is
    //                        not a guard — measured 2026-08-16, the name-keyed
    //                        version refused unrelated instances (different data
    //                        dirs) AND permitted concurrent ones (same data dir,
    //                        different $TMPDIR). It is an expression and not a
    //                        string so a program can point at the resolver that
    //                        already OWNS the path (§18.4) instead of restating
    //                        its default here, which would give the path two
    //                        owners that can disagree.
    //   env NAME: T = "d"  — one configuration entry. Its type and its default
    //                        are declared ONCE, here, and resolved+validated
    //                        before main() body runs.
    //   env NAME: T required
    //                      — no default; the program refuses to start unless the
    //                        variable is set.
    //
    // Both compile into calls injected at the head of main() — the same boundary
    // seam `cgi` already uses (codegen.el emit_program_init). No call site in the
    // program body has to remember anything, which is the whole point.
    if k == "Program" {
        let p = pos + 1
        let name = tok_value(tokens, p)
        let p = p + 1
        let p = expect(tokens, p, "LBrace")
        let singleton = ""
        let has_singleton = false
        let guards_node = { "expr": "Str", "value": "" }
        let has_guards = false
        let entries = native_list_empty()
        // Entry-scratch declared at loop-body level (not inside the branch) so
        // that inner `let` forms compile to assignment rather than a C-scoped
        // redeclaration — the same idiom the service block above relies on.
        let ename = ""
        let etype = ""
        let edefault = ""
        let has_default = false
        let erequired = false
        let fname = ""
        let fval = ""
        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
                    if str_eq(fname, "env") {
                        // env NAME: Type [= "default"] [required]
                        let ename = tok_value(tokens, p)
                        let p = p + 1
                        let p = expect(tokens, p, "Colon")
                        let etype = tok_value(tokens, p)
                        let p = p + 1
                        let edefault = ""
                        let has_default = false
                        let erequired = false
                        let k3 = tok_kind(tokens, p)
                        if str_eq(k3, "Eq") {
                            let p = p + 1
                            let edefault = tok_value(tokens, p)
                            let has_default = true
                            let p = p + 1
                        }
                        let k4 = tok_kind(tokens, p)
                        if str_eq(k4, "Ident") {
                            let w = tok_value(tokens, p)
                            if str_eq(w, "required") {
                                let erequired = true
                                let p = p + 1
                            }
                        }
                        let entries = native_list_append(entries, {
                            "name": ename,
                            "etype": etype,
                            "default": edefault,
                            "has_default": has_default,
                            "required": erequired
                        })
                    } else {
                        if str_eq(fname, "guards") {
                            // guards: <expr>  — the STATE the singleton protects.
                            // Parsed as a full expression, not a string literal, so
                            // it can name the resolver that owns the path
                            // (`guards: engram_resolve_data_dir()`) rather than
                            // duplicating that resolver's default here.
                            let p = expect(tokens, p, "Colon")
                            let g_r = parse_expr(tokens, p)
                            let guards_node = g_r["node"]
                            let p = g_r["pos"]
                            let has_guards = true
                        } else {
                            // scalar field: `name: "value"`
                            let p = expect(tokens, p, "Colon")
                            let fval = tok_value(tokens, p)
                            let p = p + 1
                            if str_eq(fname, "singleton") {
                                let singleton = fval
                                let has_singleton = true
                            }
                        }
                    }
                    let k5 = tok_kind(tokens, p)
                    if k5 == "Comma" {
                        let p = p + 1
                    }
                }
            }
        }
        let p = expect(tokens, p, "RBrace")
        return make_result({
            "stmt": "ProgramBlock",
            "name": name,
            "singleton": singleton,
            "has_singleton": has_singleton,
            "guards": guards_node,
            "has_guards": has_guards,
            "entries": entries
        }, p)
    }

    // assert <cond_expr> [ , <msg_expr> ]
    // The message is optional — if the next token after the condition is not a
    // Comma, emit an empty string placeholder so the test still works.
    if k == "Assert" {
        let p: Int = pos + 1
        let cond_r = parse_expr(tokens, p)
        let cond_node = cond_r["node"]
        let p: Int = cond_r["pos"]
        el_release(cond_r)
        let after_k: String = tok_kind(tokens, p)
        if str_eq(after_k, "Comma") {
            let p = p + 1
            let msg_r = parse_expr(tokens, p)
            let msg_node = msg_r["node"]
            let p: Int = msg_r["pos"]
            el_release(msg_r)
            return make_result({ "stmt": "Assert", "cond": cond_node, "msg": msg_node }, p)
        }
        // No message — use empty string placeholder.
        return make_result({ "stmt": "Assert", "cond": cond_node, "msg": { "expr": "Str", "value": "" } }, 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"]
            // r result map fully consumed — release to free peak heap.
            el_release(r)
            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"]
    // r result map fully consumed — release to free peak heap.
    el_release(r)
    make_result({ "stmt": "Expr", "value": val }, p)
}

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

fn parse(tokens: [Any]) -> [Map<String, Any>] {
    // Flat list: 2 entries per token, so divide by 2 for token count.
    let total: Int = native_list_len(tokens) / 3
    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"]
                // r result map fully consumed — release to free peak heap.
                el_release(r)
                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
}

// -- Streaming parse helpers ---------------------------------------------------

// parse_one — parse exactly one top-level statement at position `pos`.
// Returns { "node": stmt_map, "pos": new_pos }.
// Enables the streaming compiler pipeline (parse one → emit C → discard).
fn parse_one(tokens: [Any], pos: Int) -> Map<String, Any> {
    parse_stmt(tokens, pos)
}

// skip_to_rbrace — advance past a balanced { ... } block.
// On entry, pos must point at the LBrace token.
// Returns the position of the token AFTER the matching RBrace.
fn skip_to_rbrace(tokens: [Any], pos: Int) -> Int {
    let total: Int = native_list_len(tokens) / 3
    let p: Int = pos + 1
    let depth: Int = 1
    let going: Bool = true
    while going {
        if p >= total {
            let going = false
        } else {
            let kk: String = tok_kind(tokens, p)
            if str_eq(kk, "Eof") {
                let going = false
            } else {
                if str_eq(kk, "LBrace") {
                    let depth = depth + 1
                    let p = p + 1
                } else {
                    if str_eq(kk, "RBrace") {
                        let depth = depth - 1
                        let p = p + 1
                        if depth <= 0 {
                            let going = false
                        }
                    } else {
                        let p = p + 1
                    }
                }
            }
        }
    }
    p
}

// is_stmt_start_kind — true if `k` is a token kind that can start a new
// top-level statement (used to find safe stopping points during token skips).
fn is_stmt_start_kind(k: String) -> Bool {
    if str_eq(k, "Fn")      { return true }
    if str_eq(k, "Let")     { return true }
    if str_eq(k, "Extern")  { return true }
    if str_eq(k, "Cgi")     { return true }
    if str_eq(k, "Service") { return true }
    if str_eq(k, "Type")    { return true }
    if str_eq(k, "Enum")    { return true }
    if str_eq(k, "Import")  { return true }
    if str_eq(k, "From")    { return true }
    if str_eq(k, "Eof")     { return true }
    false
}

// skip_expr_to_stmt_boundary — skip tokens from `pos` until we reach a
// token that could start a new top-level statement, staying depth-aware
// so that braces inside expressions don't fool us.
fn skip_expr_to_stmt_boundary(tokens: [Any], pos: Int) -> Int {
    let total: Int = native_list_len(tokens) / 3
    let p: Int = pos
    let depth: Int = 0
    let going: Bool = true
    while going {
        if p >= total {
            let going = false
        } else {
            let kk: String = tok_kind(tokens, p)
            if str_eq(kk, "Eof") {
                let going = false
            } else {
                if str_eq(kk, "LBrace") {
                    let depth = depth + 1
                    let p = p + 1
                } else {
                    if str_eq(kk, "RBrace") {
                        if depth <= 0 {
                            let going = false
                        } else {
                            let depth = depth - 1
                            let p = p + 1
                        }
                    } else {
                        if depth == 0 {
                            if is_stmt_start_kind(kk) {
                                let going = false
                            } else {
                                let p = p + 1
                            }
                        } else {
                            let p = p + 1
                        }
                    }
                }
            }
        }
    }
    p
}

// scan_type_el — read a type annotation starting at pos and return its El
// source representation as a string, plus the new position.
// Returns { "el": String, "pos": Int }.
// Handles: Ident, [Type], Map<K,V>, Type?, Type<T,...> (same shapes as skip_type).
fn scan_type_el(tokens: [Any], pos: Int) -> Map<String, Any> {
    let k: String = tok_kind(tokens, pos)
    // Array type: [Type]
    if str_eq(k, "LBracket") {
        let p: Int = pos + 1
        let inner = scan_type_el(tokens, p)
        let inner_str: String = inner["el"]
        let p = inner["pos"]
        el_release(inner)
        let p = expect(tokens, p, "RBracket")
        return { "el": "[" + inner_str + "]", "pos": p }
    }
    // Named type (possibly generic or optional)
    if str_eq(k, "Ident") {
        let name: String = tok_value(tokens, pos)
        let p: Int = pos + 1
        let k2: String = tok_kind(tokens, p)
        if str_eq(k2, "Lt") {
            // Generic params: collect until matching >
            let p = p + 1
            let depth: Int = 1
            let parts: [String] = native_list_empty()
            let parts = native_list_append(parts, name + "<")
            let running: Bool = true
            while running {
                let kk: String = tok_kind(tokens, p)
                if str_eq(kk, "Eof") {
                    let running = false
                } else {
                    if str_eq(kk, "Lt") {
                        let depth = depth + 1
                        let parts = native_list_append(parts, "<")
                        let p = p + 1
                    } else {
                        if str_eq(kk, "Gt") {
                            let depth = depth - 1
                            let p = p + 1
                            if depth <= 0 {
                                let parts = native_list_append(parts, ">")
                                let running = false
                            } else {
                                let parts = native_list_append(parts, ">")
                            }
                        } else {
                            if str_eq(kk, "Comma") {
                                let parts = native_list_append(parts, ", ")
                                let p = p + 1
                            } else {
                                let parts = native_list_append(parts, tok_value(tokens, p))
                                let p = p + 1
                            }
                        }
                    }
                }
            }
            let k3: String = tok_kind(tokens, p)
            if str_eq(k3, "QuestionMark") { let p = p + 1 }
            let result: String = str_join(parts, "")
            el_release(parts)
            return { "el": result, "pos": p }
        }
        // Optional marker
        if str_eq(k2, "QuestionMark") {
            return { "el": name + "?", "pos": p + 1 }
        }
        return { "el": name, "pos": p }
    }
    // Fallback: unknown token, treat as Any
    { "el": "Any", "pos": pos + 1 }
}

// scan_params_el — scan a parameter list `(name: Type, ...)` starting at
// position `pos` (which should point at LParen) and return the El parameter
// declaration string (e.g. "a: String, b: Int") along with the new position.
// Returns { "el": String, "pos": Int }.
// Used by scan_fn_sigs_el for --emit-header without building full AST.
fn scan_params_el(tokens: [Any], pos: Int) -> Map<String, Any> {
    let p: Int = expect(tokens, pos, "LParen")
    let parts: [String] = native_list_empty()
    let going: Bool = true
    while going {
        let kk: String = tok_kind(tokens, p)
        if str_eq(kk, "RParen") {
            let going = false
        } else {
            if str_eq(kk, "Eof") {
                let going = false
            } else {
                let pname: String = tok_value(tokens, p)
                let p = p + 1
                let p = expect(tokens, p, "Colon")
                let tr = scan_type_el(tokens, p)
                let ptype: String = tr["el"]
                let p = tr["pos"]
                el_release(tr)
                let parts = native_list_append(parts, pname + ": " + ptype)
                let k2: String = tok_kind(tokens, p)
                if str_eq(k2, "Comma") {
                    let p = p + 1
                }
            }
        }
    }
    let p = expect(tokens, p, "RParen")
    let el_str: String = str_join(parts, ", ")
    el_release(parts)
    { "el": el_str, "pos": p }
}

// scan_fn_sigs_el — lightweight token-level pre-scan for --emit-header.
//
// Like scan_fn_sigs but captures El-style type strings instead of C types.
// Only records fn/extern_fn entries (header generation ignores lets/blocks).
//
// Descriptor shape:
//   { "kind": "fn"|"extern_fn", "name": String,
//     "params_el": String,   <- El param list, e.g. "a: String, b: Int"
//     "ret_el": String }     <- El return type, e.g. "String" or "Void"
//
// Peak memory: O(tokens) with no expression AST allocation.
fn scan_fn_sigs_el(tokens: [Any]) -> [Map<String, Any>] {
    let total: Int = native_list_len(tokens) / 3
    let sigs: [Map<String, Any>] = native_list_empty()
    let pos: Int = 0
    let going: Bool = true
    while going {
        if pos >= total {
            let going = false
        } else {
            let k: String = tok_kind(tokens, pos)
            if str_eq(k, "Eof") {
                let going = false
            } else {
                // --- fn definition ---
                if str_eq(k, "Fn") {
                    let p: Int = pos + 1
                    let name: String = tok_value(tokens, p)
                    let p = p + 1
                    let pr = scan_params_el(tokens, p)
                    let params_el: String = pr["el"]
                    let p = pr["pos"]
                    el_release(pr)
                    // read return type
                    let ret_el: String = "Any"
                    let k2: String = tok_kind(tokens, p)
                    if str_eq(k2, "Arrow") {
                        let p = p + 1
                        let tr = scan_type_el(tokens, p)
                        let ret_el = tr["el"]
                        let p = tr["pos"]
                        el_release(tr)
                    }
                    // skip body
                    let k3: String = tok_kind(tokens, p)
                    if str_eq(k3, "LBrace") {
                        let p = skip_to_rbrace(tokens, p)
                    }
                    if !str_eq(name, "main") {
                        let sigs = native_list_append(sigs, {
                            "kind": "fn",
                            "name": name,
                            "params_el": params_el,
                            "ret_el": ret_el
                        })
                    }
                    let pos = p
                } else {
                // --- extern fn ---
                if str_eq(k, "Extern") {
                    let p: Int = pos + 1
                    let k2: String = tok_kind(tokens, p)
                    if str_eq(k2, "Fn") {
                        let p = p + 1
                        let name: String = tok_value(tokens, p)
                        let p = p + 1
                        let pr = scan_params_el(tokens, p)
                        let params_el: String = pr["el"]
                        let p = pr["pos"]
                        el_release(pr)
                        let ret_el: String = "Any"
                        let k3: String = tok_kind(tokens, p)
                        if str_eq(k3, "Arrow") {
                            let p = p + 1
                            let tr = scan_type_el(tokens, p)
                            let ret_el = tr["el"]
                            let p = tr["pos"]
                            el_release(tr)
                        }
                        let sigs = native_list_append(sigs, {
                            "kind": "extern_fn",
                            "name": name,
                            "params_el": params_el,
                            "ret_el": ret_el
                        })
                        let pos = p
                    } else {
                        let pos = pos + 1
                    }
                } else {
                    // Let, Cgi, Service, Import, Type, Enum, From — skip to boundary.
                    let p: Int = pos + 1
                    let p = skip_expr_to_stmt_boundary(tokens, p)
                    let pos = p
                }}}
            }
        }
    sigs
}

// scan_params_c — scan a parameter list `(name: Type, ...)` starting at
// position `pos` (which should point at LParen) and return the C parameter
// declaration string along with the new position.
// Returns { "c": String, "pos": Int }.
// This avoids allocating param-map objects during the pre-scan phase.
fn scan_params_c(tokens: [Any], pos: Int) -> Map<String, Any> {
    let p: Int = expect(tokens, pos, "LParen")
    let parts: [String] = native_list_empty()
    let going: Bool = true
    while going {
        let kk: String = tok_kind(tokens, p)
        if str_eq(kk, "RParen") {
            let going = false
        } else {
            if str_eq(kk, "Eof") {
                let going = false
            } else {
                let pname: String = tok_value(tokens, p)
                let p = p + 1
                let p = expect(tokens, p, "Colon")
                let p = skip_type(tokens, p)
                let parts = native_list_append(parts, "el_val_t " + pname)
                let k2: String = tok_kind(tokens, p)
                if str_eq(k2, "Comma") {
                    let p = p + 1
                }
            }
        }
    }
    let p = expect(tokens, p, "RParen")
    let c_str: String = str_join(parts, ", ")
    // parts list fully consumed — release to free peak heap.
    el_release(parts)
    if str_eq(c_str, "") { let c_str = "void" }
    { "c": c_str, "pos": p }
}

// scan_fn_sigs — lightweight token-level pre-scan.
//
// Returns a list of descriptor maps (one per top-level item) without building
// full expression ASTs or param-map objects.  Descriptors have these shapes:
//
//   fn/extern_fn:  { "kind": "fn"|"extern_fn", "name": String,
//                    "params_c": String,  <- C param decl string, e.g. "el_val_t a, el_val_t b"
//                    "is_main": Bool }
//   toplevel_let:  { "kind": "toplevel_let", "name": String, "ltype": String }
//   cgi_block:     { "kind": "cgi_block",  "name": String }
//   service_block: { "kind": "service_block", "name": String }
//   program_block: { "kind": "program_block", "name": String }
//
// Import/TypeDef/EnumDef nodes are skipped (codegen treats them as no-ops).
//
// The scan allocates only small string values per entry, keeping peak RSS low.
fn scan_fn_sigs(tokens: [Any]) -> [Map<String, Any>] {
    let total: Int = native_list_len(tokens) / 3
    let sigs: [Map<String, Any>] = native_list_empty()
    let pos: Int = 0
    let going: Bool = true
    while going {
        if pos >= total {
            let going = false
        } else {
            let k: String = tok_kind(tokens, pos)
            if str_eq(k, "Eof") {
                let going = false
            } else {
                // --- fn definition ---
                if str_eq(k, "Fn") {
                    let p: Int = pos + 1
                    let name: String = tok_value(tokens, p)
                    let p = p + 1
                    let r = scan_params_c(tokens, p)
                    let params_c: String = r["c"]
                    let p = r["pos"]
                    // r result map fully consumed — release to free peak heap.
                    el_release(r)
                    // skip return type
                    let k2: String = tok_kind(tokens, p)
                    if str_eq(k2, "Arrow") {
                        let p = p + 1
                        let p = skip_type(tokens, p)
                    }
                    // skip body
                    let k3: String = tok_kind(tokens, p)
                    if str_eq(k3, "LBrace") {
                        let p = skip_to_rbrace(tokens, p)
                    }
                    let is_main: Bool = str_eq(name, "main")
                    let sigs = native_list_append(sigs, {
                        "kind": "fn",
                        "name": name,
                        "params_c": params_c,
                        "is_main": is_main
                    })
                    let pos = p
                } else {
                // --- extern fn ---
                if str_eq(k, "Extern") {
                    let p: Int = pos + 1
                    let k2: String = tok_kind(tokens, p)
                    if str_eq(k2, "Fn") {
                        let p = p + 1
                        let name: String = tok_value(tokens, p)
                        let p = p + 1
                        let r = scan_params_c(tokens, p)
                        let params_c: String = r["c"]
                        let p = r["pos"]
                        // r result map fully consumed — release to free peak heap.
                        el_release(r)
                        let k3: String = tok_kind(tokens, p)
                        if str_eq(k3, "Arrow") {
                            let p = p + 1
                            let p = skip_type(tokens, p)
                        }
                        let sigs = native_list_append(sigs, {
                            "kind": "extern_fn",
                            "name": name,
                            "params_c": params_c,
                            "is_main": false
                        })
                        let pos = p
                    } else {
                        let pos = pos + 1
                    }
                } else {
                // --- top-level let ---
                if str_eq(k, "Let") {
                    let p: Int = pos + 1
                    let name: String = tok_value(tokens, p)
                    let p = p + 1
                    let ltype: String = ""
                    let k2: String = tok_kind(tokens, p)
                    if str_eq(k2, "Colon") {
                        let p = p + 1
                        let kt: String = tok_kind(tokens, p)
                        if str_eq(kt, "Ident") { let ltype = tok_value(tokens, p) }
                        let p = skip_type(tokens, p)
                    }
                    let p = expect(tokens, p, "Eq")
                    let p = skip_expr_to_stmt_boundary(tokens, p)
                    let sigs = native_list_append(sigs, {
                        "kind": "toplevel_let",
                        "name": name,
                        "ltype": ltype
                    })
                    let pos = p
                } else {
                // --- cgi block ---
                if str_eq(k, "Cgi") {
                    let p: Int = pos + 1
                    let name: String = tok_value(tokens, p)
                    let p = p + 1
                    let k2: String = tok_kind(tokens, p)
                    if str_eq(k2, "LBrace") {
                        let p = skip_to_rbrace(tokens, p)
                    }
                    let sigs = native_list_append(sigs, {
                        "kind": "cgi_block",
                        "name": name
                    })
                    let pos = p
                } else {
                // --- service block ---
                if str_eq(k, "Service") {
                    let p: Int = pos + 1
                    let name: String = tok_value(tokens, p)
                    let p = p + 1
                    let k2: String = tok_kind(tokens, p)
                    if str_eq(k2, "LBrace") {
                        let p = skip_to_rbrace(tokens, p)
                    }
                    let sigs = native_list_append(sigs, {
                        "kind": "service_block",
                        "name": name
                    })
                    let pos = p
                } else {
                // --- program block ---
                if str_eq(k, "Program") {
                    let p: Int = pos + 1
                    let name: String = tok_value(tokens, p)
                    let p = p + 1
                    let k2: String = tok_kind(tokens, p)
                    if str_eq(k2, "LBrace") {
                        let p = skip_to_rbrace(tokens, p)
                    }
                    let sigs = native_list_append(sigs, {
                        "kind": "program_block",
                        "name": name
                    })
                    let pos = p
                } else {
                    // Import, Type, Enum, From, or any other token.
                    // Skip ahead to the next statement boundary.
                    let p: Int = pos + 1
                    let p = skip_expr_to_stmt_boundary(tokens, p)
                    let pos = p
                }}}}}}
            }
        }
    }
    sigs
}
