// 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. // // The cursor (integer position into the token list) is threaded through every // parse function. Functions return { "node": , "pos": }. // // 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] // -- 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 { let kind: String = native_list_get(tokens, pos * 2) let value: String = native_list_get(tokens, pos * 2 + 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) / 2 if pos < 0 { return "Eof" } if pos >= n { return "Eof" } native_list_get(tokens, pos * 2) } fn tok_value(tokens: [Any], pos: Int) -> String { let n: Int = native_list_len(tokens) / 2 if pos < 0 { return "" } if pos >= n { return "" } native_list_get(tokens, pos * 2 + 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, pos: Int) -> Map { { "node": node, "pos": pos } } // -- Type annotation parser ---------------------------------------------------- // Skips over a type annotation, returning the new position. // Types can be: Ident, [Type], Map, 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 { let p = expect(tokens, pos, "LParen") let params: [Map] = 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

{title}

// // 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 // ). 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 is encountered. // Used for