5f9cad5908
The --emit-header path previously called parse() which builds the entire program AST in memory before writing the .elh file. For checkout.el (~491 lines with HTML template trees and deep BinOp string-concat chains), this exhausted memory before the header could be written. Fix: replace parse() + emit_header() with scan_fn_sigs_el() + emit_header_from_sigs(). The new path tokenises the source once, then walks the flat token list skipping over function bodies entirely — peak memory is O(tokens) instead of O(whole-program AST). New functions in parser.el: - scan_type_el: reads a type annotation and returns its El source string - scan_params_el: reads (name: Type, ...) and returns El params string - scan_fn_sigs_el: token-level scan that collects El-style fn signatures without building any expression AST nodes New function in compiler.el: - emit_header_from_sigs: writes .elh from scan_fn_sigs_el output Self-hosting check: elc compiled with new elc, diff of outputs is identical (zero difference). Smoke test: elc --emit-header checkout.el produces correct three-entry .elh (previously truncated at two entries due to mid-parse OOM).
2468 lines
97 KiB
EmacsLisp
2468 lines
97 KiB
EmacsLisp
// 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 * 2)
|
|
let value: String = native_list_get(tokens, pos * 2 + 1)
|
|
{ "kind": kind, "value": value }
|
|
}
|
|
|
|
fn tok_kind(tokens: [Any], pos: Int) -> String {
|
|
native_list_get(tokens, pos * 2)
|
|
}
|
|
|
|
fn tok_value(tokens: [Any], pos: Int) -> String {
|
|
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 just advance; error recovery is best-effort
|
|
pos + 1
|
|
}
|
|
|
|
// -- Result helpers ------------------------------------------------------------
|
|
|
|
fn make_result(node: Map<String, Any>, pos: Int) -> Map<String, Any> {
|
|
{ "node": node, "pos": pos }
|
|
}
|
|
|
|
// -- Type annotation parser ----------------------------------------------------
|
|
// Skips over a type annotation, returning the new position.
|
|
// Types can be: Ident, [Type], Map<K,V>, Type?, Type<Type,...>
|
|
|
|
fn skip_type(tokens: [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
|
|
while running {
|
|
let k = tok_kind(tokens, p)
|
|
if k == "RBrace" {
|
|
let running = false
|
|
} else {
|
|
if k == "Eof" {
|
|
let running = false
|
|
} else {
|
|
let r = parse_stmt(tokens, p)
|
|
let stmt = r["node"]
|
|
let new_p: Int = r["pos"]
|
|
// 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)
|
|
return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, 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 attach to following stmt
|
|
if k == "At" {
|
|
let p = pos + 1
|
|
let dec_name = tok_value(tokens, p)
|
|
let p = p + 1
|
|
let r = parse_stmt(tokens, p)
|
|
let inner = r["node"]
|
|
let p2 = r["pos"]
|
|
let inner_kind: String = inner["stmt"]
|
|
if str_eq(inner_kind, "FnDef") {
|
|
let with_dec = {
|
|
"stmt": "FnDef",
|
|
"name": inner["name"],
|
|
"params": inner["params"],
|
|
"body": inner["body"],
|
|
"ret_type": inner["ret_type"],
|
|
"decorator": dec_name
|
|
}
|
|
// 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)
|
|
}
|
|
|
|
// 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) / 2
|
|
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) / 2
|
|
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) / 2
|
|
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) / 2
|
|
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 }
|
|
//
|
|
// 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) / 2
|
|
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 {
|
|
// 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
|
|
}
|