Files
el/lang/el-compiler/src/parser.el
T
bigmerge 45325f7391
El SDK CI - dev / build-and-test (pull_request) Failing after 4m6s
singleton: guard the state, not the program's name
The singleton lock protected a filename, not a store. It was keyed on
$EL_SINGLETON_DIR|$TMPDIR|/tmp + /el-singleton-<program>.lock — the
program's NAME and a temp directory — and never consulted the state it
claimed to protect, while its own refusal message read "Refusing to start
a second instance against the same state."

Measured, it failed in both directions. A second engram against a
DIFFERENT data dir was refused, naming the first's pid. And
TMPDIR=/tmp/other let a second engram start against the SAME data dir
with no complaint — the two-writer data-loss condition the guard exists
to prevent, defeated by one environment variable.

Both are one error: the identity of the resource had been replaced by a
label for it.

The lock now lives inside the state it guards —
<state>/.el-singleton-<id>.lock — and the program block says what that
state is. Same directory is the same file is the same inode, so it
contends and there is no TMPDIR left in the key to change. Different
directories are different files, so they don't. Different spellings of
one directory (trailing slash, x/../x, symlink) collapse in the kernel's
own path walk, so they contend without this code comparing strings;
canonicalisation is for the message, never the decision.

`guards:` is an expression so a program can point at the resolver that
already owns its path — guards: engram_resolve_data_dir() — instead of
restating that resolver's default, which is the two-owners defect spec
18.4 exists to prevent. A `singleton:` without `guards:` is now a compile
error; emitting a name-keyed lock instead would be emitting the defect.

Kept: the flock (the kernel drops it on crash and SIGKILL, so there is
still no "delete the lock file to get unstuck" ritual — a stale file
inside a copied data dir is inert), and the holder's pid in the message.
Changed: the message is true. It says "the same state" because the lock
it failed to take is in that state, and it names the state it checked.
An unguardable state (missing, read-only) now refuses rather than
starting unguarded.

Also corrects lang/AGENTS.md's compiler rebuild line, which had gone
stale: linking el_runtime.c alone no longer resolves.
2026-08-16 16:08:40 -05:00

2713 lines
108 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 {
// 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)
}
// 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 LBracemap 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) / 2
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)
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 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"],
"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) / 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 }
// 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) / 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 {
// --- 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
}