45325f7391
El SDK CI - dev / build-and-test (pull_request) Failing after 4m6s
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.
2713 lines
108 KiB
EmacsLisp
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 LBrace→map path inside parse_primary
|
|
let prev_no_block: String = state_get("__no_block_expr")
|
|
state_set("__no_block_expr", "1")
|
|
let r = parse_expr(tokens, p + 1)
|
|
state_set("__no_block_expr", prev_no_block)
|
|
let interp_val = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let p = expect(tokens, p, "RBrace")
|
|
// Check if the expr is a call to raw()
|
|
let is_raw_call = false
|
|
let interp_kind: String = interp_val["expr"]
|
|
if str_eq(interp_kind, "Call") {
|
|
let fn_node = interp_val["func"]
|
|
let fn_kind: String = fn_node["expr"]
|
|
if str_eq(fn_kind, "Ident") {
|
|
let fn_name_v: String = fn_node["name"]
|
|
if str_eq(fn_name_v, "raw") {
|
|
let is_raw_call = true
|
|
}
|
|
}
|
|
}
|
|
if is_raw_call {
|
|
let raw_args = interp_val["args"]
|
|
let raw_inner = native_list_get(raw_args, 0)
|
|
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
|
|
} else {
|
|
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Text tokens — collect run of non-special tokens
|
|
let r_text = parse_html_text_tokens(tokens, p)
|
|
let text_str: String = r_text["text"]
|
|
let p = r_text["pos"]
|
|
// r_text result map fully consumed — release to free peak heap.
|
|
el_release(r_text)
|
|
let text_trimmed: String = str_trim(text_str)
|
|
if !str_eq(text_trimmed, "") {
|
|
let children = native_list_append(children, { "html": "Text", "text": text_trimmed })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
{ "children": children, "pos": p }
|
|
}
|
|
|
|
// Parse body of {#each} until {/each}. Mirrors parse_html_children but
|
|
// stops at the {/each} sentinel rather than a closing element tag.
|
|
fn parse_html_each_body(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|
parse_html_children(tokens, pos, "__each__")
|
|
}
|
|
|
|
// Parse a single HTML element: <tag attrs> children </tag>
|
|
// or self-closing: <tag attrs/>
|
|
// Pos points to the Lt token.
|
|
fn parse_html_element(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|
let p = pos
|
|
// consume <
|
|
let p = expect(tokens, p, "Lt")
|
|
// tag name
|
|
let tag_name = tok_value(tokens, p)
|
|
let p = p + 1
|
|
// parse attributes
|
|
let r_attrs = parse_html_attrs(tokens, p)
|
|
let attrs = r_attrs["attrs"]
|
|
let p = r_attrs["pos"]
|
|
// r_attrs result map fully consumed — release to free peak heap.
|
|
el_release(r_attrs)
|
|
// check for self-closing /> or void element
|
|
let k = tok_kind(tokens, p)
|
|
let self_closing = false
|
|
if str_eq(k, "Slash") {
|
|
// />
|
|
let p = p + 1
|
|
let p = expect(tokens, p, "Gt")
|
|
let self_closing = true
|
|
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
|
|
}
|
|
// consume >
|
|
let p = expect(tokens, p, "Gt")
|
|
// void elements have no children, no closing tag
|
|
if is_void_element(tag_name) {
|
|
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
|
|
}
|
|
// raw-text mode for style/script — collect content as plain text without parsing CSS/JS as El
|
|
if str_eq(tag_name, "style") {
|
|
let r_raw = parse_raw_text_content(tokens, p, "style")
|
|
let raw_text: String = r_raw["text"]
|
|
let p = r_raw["pos"]
|
|
el_release(r_raw)
|
|
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
|
|
let raw_children: [Map<String, Any>] = native_list_empty()
|
|
let raw_children = native_list_append(raw_children, raw_child)
|
|
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
|
|
}
|
|
if str_eq(tag_name, "script") {
|
|
let r_raw = parse_raw_text_content(tokens, p, "script")
|
|
let raw_text: String = r_raw["text"]
|
|
let p = r_raw["pos"]
|
|
el_release(r_raw)
|
|
let raw_child: Map<String, Any> = { "html": "Text", "text": raw_text }
|
|
let raw_children: [Map<String, Any>] = native_list_empty()
|
|
let raw_children = native_list_append(raw_children, raw_child)
|
|
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": raw_children, "self_closing": false }, p)
|
|
}
|
|
// parse children
|
|
let r_children = parse_html_children(tokens, p, tag_name)
|
|
let children = r_children["children"]
|
|
let p = r_children["pos"]
|
|
// r_children result map fully consumed — release to free peak heap.
|
|
el_release(r_children)
|
|
make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": children, "self_closing": false }, p)
|
|
}
|
|
|
|
// Entry point for HTML template parsing.
|
|
// Pos points to Lt (or Lt Not for <!doctype>).
|
|
// May parse an optional <!doctype html> prefix followed by the root element.
|
|
fn parse_html_template(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|
let p = pos
|
|
// Check for <!doctype html>
|
|
let doctype = false
|
|
let k = tok_kind(tokens, p)
|
|
let k2 = tok_kind(tokens, p + 1)
|
|
if str_eq(k, "Lt") {
|
|
if str_eq(k2, "Not") {
|
|
let k3_v = tok_value(tokens, p + 2)
|
|
if str_eq(k3_v, "doctype") {
|
|
let doctype = true
|
|
// consume <!doctype html>
|
|
let p = p + 2
|
|
let scanning = true
|
|
while scanning {
|
|
let ck = tok_kind(tokens, p)
|
|
if str_eq(ck, "Eof") { let scanning = false }
|
|
if str_eq(ck, "Gt") {
|
|
let p = p + 1
|
|
let scanning = false
|
|
} else {
|
|
let p = p + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Parse root element
|
|
let r = parse_html_element(tokens, p)
|
|
let root = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let root_with_doctype = root
|
|
if doctype {
|
|
let root_with_doctype = { "html": root["html"], "tag": root["tag"], "attrs": root["attrs"], "children": root["children"], "self_closing": root["self_closing"], "doctype": true }
|
|
}
|
|
make_result({ "expr": "HtmlTemplate", "root": root_with_doctype }, p)
|
|
}
|
|
|
|
fn parse_primary(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|
let k = tok_kind(tokens, pos)
|
|
let v = tok_value(tokens, pos)
|
|
|
|
// Integer literal
|
|
if k == "Int" {
|
|
return make_result({ "expr": "Int", "value": v }, pos + 1)
|
|
}
|
|
|
|
// Float literal
|
|
if k == "Float" {
|
|
return make_result({ "expr": "Float", "value": v }, pos + 1)
|
|
}
|
|
|
|
// String literal
|
|
if k == "Str" {
|
|
return make_result({ "expr": "Str", "value": v }, pos + 1)
|
|
}
|
|
|
|
// Bool literal
|
|
if k == "Bool" {
|
|
return make_result({ "expr": "Bool", "value": v }, pos + 1)
|
|
}
|
|
|
|
// HTML template literal: <tagname ...>...</tagname> or <!doctype html>...
|
|
// Detected in value position only; `<` in comparison position is already
|
|
// consumed by parse_binop before parse_primary is reached.
|
|
if k == "Lt" {
|
|
let k2 = tok_kind(tokens, pos + 1)
|
|
if str_eq(k2, "Not") {
|
|
return parse_html_template(tokens, pos)
|
|
}
|
|
if str_eq(k2, "Ident") {
|
|
let tag_candidate = tok_value(tokens, pos + 1)
|
|
if is_html_tag_name(tag_candidate) {
|
|
return parse_html_template(tokens, pos)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Identifier
|
|
if k == "Ident" {
|
|
return make_result({ "expr": "Ident", "name": v }, pos + 1)
|
|
}
|
|
|
|
// Grouped expression
|
|
if k == "LParen" {
|
|
let r = parse_expr(tokens, pos + 1)
|
|
let node = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let p = expect(tokens, p, "RParen")
|
|
return make_result(node, p)
|
|
}
|
|
|
|
// Array literal: [e1, e2, ...]
|
|
if k == "LBracket" {
|
|
let p = pos + 1
|
|
let elems: [Map<String, Any>] = native_list_empty()
|
|
let running = true
|
|
while running {
|
|
let k2 = tok_kind(tokens, p)
|
|
if k2 == "RBracket" {
|
|
let running = false
|
|
} else {
|
|
if k2 == "Eof" {
|
|
let running = false
|
|
} else {
|
|
let r = parse_expr(tokens, p)
|
|
let elem = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let elems = native_list_append(elems, elem)
|
|
let k3 = tok_kind(tokens, p)
|
|
if k3 == "Comma" {
|
|
let p = p + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let p = expect(tokens, p, "RBracket")
|
|
return make_result({ "expr": "Array", "elems": elems }, p)
|
|
}
|
|
|
|
// Map literal: { "key": val, ... }
|
|
//
|
|
// Suppression: when parse_if / parse_while / parse_for / parse_match
|
|
// are parsing a head expression, they set __no_block_expr=1 so a stray
|
|
// `{` here doesn't get gobbled as a Map literal - it belongs to the
|
|
// following block. Without this, `if a || b { ... }` could mis-parse
|
|
// (the `||` recursion lands at `{` and tries to read the if-body as a
|
|
// Map, then loops forever when keys don't match `Str: expr`).
|
|
if k == "LBrace" {
|
|
let no_block: String = state_get("__no_block_expr")
|
|
if str_eq(no_block, "1") {
|
|
// Fall through to fallback - caller will see `{` and treat it
|
|
// as the start of the block they're expecting.
|
|
return make_result({ "expr": "Nil" }, pos)
|
|
}
|
|
// Distinguish map literal from interpolation chain.
|
|
// A map literal requires { key: value } — the second token inside { must be Colon.
|
|
// An empty {} is a map literal. Everything else is an interpolation chain.
|
|
let first_k: String = tok_kind(tokens, pos + 1)
|
|
let second_k: String = tok_kind(tokens, pos + 2)
|
|
if str_eq(first_k, "RBrace") {
|
|
// Empty map literal {}
|
|
return make_result({ "expr": "Map", "pairs": native_list_empty() }, pos + 2)
|
|
}
|
|
if str_eq(second_k, "Colon") {
|
|
// MAP LITERAL: { key: value, ... }
|
|
let p = pos + 1
|
|
let pairs: [Map<String, Any>] = native_list_empty()
|
|
let running = true
|
|
while running {
|
|
let k2 = tok_kind(tokens, p)
|
|
if k2 == "RBrace" {
|
|
let running = false
|
|
} else {
|
|
if k2 == "Eof" {
|
|
let running = false
|
|
} else {
|
|
// key: Str or Ident token
|
|
let key = tok_value(tokens, p)
|
|
let new_p: Int = p + 1
|
|
let new_p = expect(tokens, new_p, "Colon")
|
|
let r = parse_expr(tokens, new_p)
|
|
let val_node = r["node"]
|
|
let new_p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let pair = { "key": key, "value": val_node }
|
|
let pairs = native_list_append(pairs, pair)
|
|
let k3 = tok_kind(tokens, new_p)
|
|
if k3 == "Comma" {
|
|
let new_p = new_p + 1
|
|
}
|
|
// Non-progress guard: malformed map content can leave
|
|
// parse_expr returning the same pos. Force advance.
|
|
if new_p <= p {
|
|
let p = p + 1
|
|
} else {
|
|
let p = new_p
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let p = expect(tokens, p, "RBrace")
|
|
return make_result({ "expr": "Map", "pairs": pairs }, p)
|
|
}
|
|
// INTERPOLATION CHAIN: {expr}, {expr}{expr}, {expr}<html>, etc.
|
|
// Build a BinOp(Plus, ...) concatenation chain.
|
|
let p = pos
|
|
let chain_node: Map<String, Any> = { "expr": "Nil" }
|
|
let chain_started = false
|
|
let chain_running = true
|
|
while chain_running {
|
|
let ck: String = tok_kind(tokens, p)
|
|
if str_eq(ck, "LBrace") {
|
|
let prev_no_block: String = state_get("__no_block_expr")
|
|
state_set("__no_block_expr", "1")
|
|
let r = parse_expr(tokens, p + 1)
|
|
state_set("__no_block_expr", prev_no_block)
|
|
let part = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let p = expect(tokens, p, "RBrace")
|
|
if !chain_started {
|
|
let chain_node = part
|
|
let chain_started = true
|
|
} else {
|
|
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
|
}
|
|
} else {
|
|
if str_eq(ck, "Lt") {
|
|
let ck2: String = tok_kind(tokens, p + 1)
|
|
if str_eq(ck2, "Not") {
|
|
let r = parse_html_template(tokens, p)
|
|
let part = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
if !chain_started {
|
|
let chain_node = part
|
|
let chain_started = true
|
|
} else {
|
|
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
|
}
|
|
} else {
|
|
if str_eq(ck2, "Ident") {
|
|
let tag_candidate: String = tok_value(tokens, p + 1)
|
|
if is_html_tag_name(tag_candidate) {
|
|
let r = parse_html_template(tokens, p)
|
|
let part = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
if !chain_started {
|
|
let chain_node = part
|
|
let chain_started = true
|
|
} else {
|
|
let chain_node: Map<String, Any> = { "expr": "BinOp", "op": "Plus", "left": chain_node, "right": part }
|
|
}
|
|
} else {
|
|
let chain_running = false
|
|
}
|
|
} else {
|
|
let chain_running = false
|
|
}
|
|
}
|
|
} else {
|
|
let chain_running = false
|
|
}
|
|
}
|
|
}
|
|
return make_result(chain_node, p)
|
|
}
|
|
|
|
// if expression
|
|
if k == "If" {
|
|
let r = parse_if(tokens, pos)
|
|
return r
|
|
}
|
|
|
|
// match expression
|
|
if k == "Match" {
|
|
let r = parse_match(tokens, pos)
|
|
return r
|
|
}
|
|
|
|
// for expression (used as statement)
|
|
if k == "For" {
|
|
let r = parse_for_expr(tokens, pos)
|
|
return r
|
|
}
|
|
|
|
// Anonymous function literal (lambda): fn(params) -> RetType { body }
|
|
// Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... })
|
|
// Produces a Lambda expression node (distinct from a named FnDef statement).
|
|
if k == "Fn" {
|
|
let p = pos + 1
|
|
let r = parse_params(tokens, p)
|
|
let params = r["params"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let ret_type = ""
|
|
let k2 = tok_kind(tokens, p)
|
|
if k2 == "Arrow" {
|
|
let p = p + 1
|
|
let kt = tok_kind(tokens, p)
|
|
if kt == "Ident" {
|
|
let ret_type = tok_value(tokens, p)
|
|
}
|
|
let p = skip_type(tokens, p)
|
|
}
|
|
let r2 = parse_block(tokens, p)
|
|
let body = r2["stmts"]
|
|
let p = r2["pos"]
|
|
// r2 result map fully consumed — release to free peak heap.
|
|
el_release(r2)
|
|
return make_result({ "expr": "Lambda", "params": params, "body": body, "ret_type": ret_type }, p)
|
|
}
|
|
|
|
// Unary not
|
|
if k == "Not" {
|
|
let r = parse_primary(tokens, pos + 1)
|
|
let inner = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
return make_result({ "expr": "Not", "inner": inner }, p)
|
|
}
|
|
|
|
// Unary minus
|
|
if k == "Minus" {
|
|
let r = parse_primary(tokens, pos + 1)
|
|
let inner = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
return make_result({ "expr": "Neg", "inner": inner }, p)
|
|
}
|
|
|
|
// Soft keywords usable as identifiers in expression position. The lexer
|
|
// turns words like `target`, `to`, `via`, `deploy`, etc. into dedicated
|
|
// token kinds for the deploy/retry DSLs, but they're also valid as
|
|
// parameter names and local variables. When one of these appears in
|
|
// expression position (where only an Ident makes sense), treat it as
|
|
// an Ident carrying the original text - otherwise references to a
|
|
// parameter named `target` compile to EL_NULL.
|
|
if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Via" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Deploy" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Reason" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Times" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Fallback" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Retry" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Parallel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Trace" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Requires" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Where" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "As" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "With" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Manager" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Engine" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Accessor" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
if k == "Vessel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
|
|
|
// Fallback: skip unknown token
|
|
make_result({ "expr": "Nil" }, pos + 1)
|
|
}
|
|
|
|
fn parse_if(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|
let p = expect(tokens, pos, "If")
|
|
// Suppress Map-literal parsing in the cond so a stray `{` (the start
|
|
// of the then-block) isn't gobbled as a Map.
|
|
let prev_no_block: String = state_get("__no_block_expr")
|
|
state_set("__no_block_expr", "1")
|
|
let r = parse_expr(tokens, p)
|
|
state_set("__no_block_expr", prev_no_block)
|
|
let cond = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let r2 = parse_block(tokens, p)
|
|
let then_stmts = r2["stmts"]
|
|
let p = r2["pos"]
|
|
// r2 result map fully consumed — release to free peak heap.
|
|
el_release(r2)
|
|
let has_else = false
|
|
let else_stmts: [Map<String, Any>] = native_list_empty()
|
|
let k2 = tok_kind(tokens, p)
|
|
if k2 == "Else" {
|
|
let p = p + 1
|
|
let k3 = tok_kind(tokens, p)
|
|
if k3 == "If" {
|
|
// else-if chain: parse as nested if
|
|
let r3 = parse_if(tokens, p)
|
|
let nested = r3["node"]
|
|
let p = r3["pos"]
|
|
// r3 result map fully consumed — release to free peak heap.
|
|
el_release(r3)
|
|
let else_stmts = native_list_append(else_stmts, { "stmt": "Expr", "value": nested })
|
|
let has_else = true
|
|
} else {
|
|
let r3 = parse_block(tokens, p)
|
|
let else_stmts = r3["stmts"]
|
|
let p = r3["pos"]
|
|
// r3 result map fully consumed — release to free peak heap.
|
|
el_release(r3)
|
|
let has_else = true
|
|
}
|
|
}
|
|
make_result({ "expr": "If", "cond": cond, "then": then_stmts, "else": else_stmts, "has_else": has_else }, p)
|
|
}
|
|
|
|
fn parse_match(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|
let p = expect(tokens, pos, "Match")
|
|
let prev_no_block: String = state_get("__no_block_expr")
|
|
state_set("__no_block_expr", "1")
|
|
let r = parse_expr(tokens, p)
|
|
state_set("__no_block_expr", prev_no_block)
|
|
let subject = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let p = expect(tokens, p, "LBrace")
|
|
let arms: [Map<String, Any>] = native_list_empty()
|
|
let running = true
|
|
while running {
|
|
let k = tok_kind(tokens, p)
|
|
if k == "RBrace" {
|
|
let running = false
|
|
} else {
|
|
if k == "Eof" {
|
|
let running = false
|
|
} else {
|
|
// parse pattern => body
|
|
let r2 = parse_pattern(tokens, p)
|
|
let pattern = r2["node"]
|
|
let p = r2["pos"]
|
|
// r2 result map fully consumed — release to free peak heap.
|
|
el_release(r2)
|
|
let p = expect(tokens, p, "FatArrow")
|
|
let r3 = parse_expr(tokens, p)
|
|
let body = r3["node"]
|
|
let p = r3["pos"]
|
|
// r3 result map fully consumed — release to free peak heap.
|
|
el_release(r3)
|
|
let arm = { "pattern": pattern, "body": body }
|
|
let arms = native_list_append(arms, arm)
|
|
let k2 = tok_kind(tokens, p)
|
|
if k2 == "Comma" {
|
|
let p = p + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let p = expect(tokens, p, "RBrace")
|
|
make_result({ "expr": "Match", "subject": subject, "arms": arms }, p)
|
|
}
|
|
|
|
fn parse_pattern(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|
let k = tok_kind(tokens, pos)
|
|
if k == "Ident" {
|
|
let v = tok_value(tokens, pos)
|
|
if v == "_" {
|
|
return make_result({ "pattern": "Wildcard" }, pos + 1)
|
|
}
|
|
// Check for Enum::Variant pattern (Color::Red, Status::Ok, etc.)
|
|
// Lexed as: Ident ColonColon Ident
|
|
let next_k = tok_kind(tokens, pos + 1)
|
|
if next_k == "ColonColon" {
|
|
let variant_name = tok_value(tokens, pos + 2)
|
|
return make_result({ "pattern": "Variant", "enum_name": v, "variant": variant_name }, pos + 3)
|
|
}
|
|
return make_result({ "pattern": "Binding", "name": v }, pos + 1)
|
|
}
|
|
if k == "Int" {
|
|
return make_result({ "pattern": "LitInt", "value": tok_value(tokens, pos) }, pos + 1)
|
|
}
|
|
if k == "Str" {
|
|
return make_result({ "pattern": "LitStr", "value": tok_value(tokens, pos) }, pos + 1)
|
|
}
|
|
if k == "Bool" {
|
|
return make_result({ "pattern": "LitBool", "value": tok_value(tokens, pos) }, pos + 1)
|
|
}
|
|
// Wildcard _
|
|
make_result({ "pattern": "Wildcard" }, pos + 1)
|
|
}
|
|
|
|
fn parse_for_expr(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|
let p = expect(tokens, pos, "For")
|
|
let item_name = tok_value(tokens, p)
|
|
let p = p + 1
|
|
let p = expect(tokens, p, "In")
|
|
let prev_no_block: String = state_get("__no_block_expr")
|
|
state_set("__no_block_expr", "1")
|
|
let r = parse_expr(tokens, p)
|
|
state_set("__no_block_expr", prev_no_block)
|
|
let list_expr = r["node"]
|
|
let p = r["pos"]
|
|
// r result map fully consumed — release to free peak heap.
|
|
el_release(r)
|
|
let r2 = parse_block(tokens, p)
|
|
let body = r2["stmts"]
|
|
let p = r2["pos"]
|
|
// r2 result map fully consumed — release to free peak heap.
|
|
el_release(r2)
|
|
make_result({ "expr": "For", "item": item_name, "list": list_expr, "body": body }, p)
|
|
}
|
|
|
|
fn parse_block(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|
let p = expect(tokens, pos, "LBrace")
|
|
let stmts: [Map<String, Any>] = native_list_empty()
|
|
let running = true
|
|
// Runaway backstop: a block can hold at most (token count) statements, since
|
|
// every iteration consumes >= 1 token. If we exceed that, the cursor has run
|
|
// off the end without terminating (malformed input) -> fail fast, don't hang.
|
|
let blk_total: Int = native_list_len(tokens) / 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
|
|
}
|