7376349124
type User = { name: String } was silently broken: the parser consumed
the type name then called expect(LBrace) while sitting on the = token.
expect() advances unconditionally on mismatch, so it consumed = and
treated { as the first field name, producing a corrupt TypeDef node.
The FnDef following the broken TypeDef was then parsed incorrectly or
lost entirely -- causing greet() and similar functions to vanish from
JS/C output with no error.
Fix: detect and skip the optional Eq token before expecting LBrace.
Both targets benefit; rebuild elc to pick up the fix.
1084 lines
38 KiB
EmacsLisp
1084 lines
38 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: [Map<String, Any>]) -> [Map<String, Any>]
|
|
|
|
// ── Token access helpers ──────────────────────────────────────────────────────
|
|
|
|
fn tok_at(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|
native_list_get(tokens, pos)
|
|
}
|
|
|
|
fn tok_kind(tokens: [Map<String, Any>], pos: Int) -> String {
|
|
let t = native_list_get(tokens, pos)
|
|
t["kind"]
|
|
}
|
|
|
|
fn tok_value(tokens: [Map<String, Any>], pos: Int) -> String {
|
|
let t = native_list_get(tokens, pos)
|
|
t["value"]
|
|
}
|
|
|
|
fn expect(tokens: [Map<String, Any>], pos: Int, kind: String) -> Int {
|
|
let k = tok_kind(tokens, pos)
|
|
if k == kind {
|
|
return pos + 1
|
|
}
|
|
// On mismatch just advance; error recovery is best-effort
|
|
pos + 1
|
|
}
|
|
|
|
// ── Result helpers ────────────────────────────────────────────────────────────
|
|
|
|
fn make_result(node: Map<String, Any>, pos: Int) -> Map<String, Any> {
|
|
{ "node": node, "pos": pos }
|
|
}
|
|
|
|
// ── Type annotation parser ────────────────────────────────────────────────────
|
|
// Skips over a type annotation, returning the new position.
|
|
// Types can be: Ident, [Type], Map<K,V>, Type?, Type<Type,...>
|
|
|
|
fn skip_type(tokens: [Map<String, 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: [Map<String, 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 ────────────────────────────────────────────────────────
|
|
|
|
fn parse_primary(tokens: [Map<String, 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)
|
|
}
|
|
|
|
// 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"]
|
|
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"]
|
|
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)
|
|
}
|
|
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 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"]
|
|
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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Unary not
|
|
if k == "Not" {
|
|
let r = parse_primary(tokens, pos + 1)
|
|
let inner = r["node"]
|
|
let p = r["pos"]
|
|
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"]
|
|
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: [Map<String, 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"]
|
|
let r2 = parse_block(tokens, p)
|
|
let then_stmts = r2["stmts"]
|
|
let p = r2["pos"]
|
|
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"]
|
|
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"]
|
|
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: [Map<String, 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"]
|
|
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"]
|
|
let p = expect(tokens, p, "FatArrow")
|
|
let r3 = parse_expr(tokens, p)
|
|
let body = r3["node"]
|
|
let p = r3["pos"]
|
|
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: [Map<String, 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: [Map<String, 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"]
|
|
let r2 = parse_block(tokens, p)
|
|
let body = r2["stmts"]
|
|
let p = r2["pos"]
|
|
make_result({ "expr": "For", "item": item_name, "list": list_expr, "body": body }, p)
|
|
}
|
|
|
|
fn parse_block(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|
let p = expect(tokens, pos, "LBrace")
|
|
let stmts: [Map<String, Any>] = native_list_empty()
|
|
let running = true
|
|
while running {
|
|
let k = tok_kind(tokens, p)
|
|
if k == "RBrace" {
|
|
let running = false
|
|
} else {
|
|
if k == "Eof" {
|
|
let running = false
|
|
} else {
|
|
let r = parse_stmt(tokens, p)
|
|
let stmt = r["node"]
|
|
let new_p: Int = r["pos"]
|
|
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: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|
let r = parse_primary(tokens, pos)
|
|
let node = r["node"]
|
|
let p = r["pos"]
|
|
|
|
// 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"]
|
|
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"]
|
|
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 }
|
|
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 }
|
|
false
|
|
}
|
|
|
|
fn parse_binop(tokens: [Map<String, Any>], pos: Int, min_prec: Int) -> Map<String, Any> {
|
|
let r = parse_postfix(tokens, pos)
|
|
let left = r["node"]
|
|
let p = r["pos"]
|
|
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"]
|
|
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: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
|
parse_binop(tokens, pos, 1)
|
|
}
|
|
|
|
// ── Statement parsing ─────────────────────────────────────────────────────────
|
|
|
|
fn parse_stmt(tokens: [Map<String, 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"]
|
|
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"]
|
|
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"]
|
|
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"]
|
|
// 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"]
|
|
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"]
|
|
let r2 = parse_block(tokens, p)
|
|
let body = r2["stmts"]
|
|
let p = r2["pos"]
|
|
return make_result({ "stmt": "While", "cond": cond, "body": body }, p)
|
|
}
|
|
|
|
// for loop
|
|
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 list_expr = r["node"]
|
|
let p = r["pos"]
|
|
let r2 = parse_block(tokens, p)
|
|
let body = r2["stmts"]
|
|
let p = r2["pos"]
|
|
return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
|
|
}
|
|
|
|
// @decorator — capture decorator name and attach to following stmt
|
|
if k == "At" {
|
|
let p = pos + 1
|
|
let dec_name = tok_value(tokens, p)
|
|
let p = p + 1
|
|
let r = parse_stmt(tokens, p)
|
|
let inner = r["node"]
|
|
let p2 = r["pos"]
|
|
let inner_kind: String = inner["stmt"]
|
|
if str_eq(inner_kind, "FnDef") {
|
|
let with_dec = {
|
|
"stmt": "FnDef",
|
|
"name": inner["name"],
|
|
"params": inner["params"],
|
|
"body": inner["body"],
|
|
"ret_type": inner["ret_type"],
|
|
"decorator": dec_name
|
|
}
|
|
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)
|
|
}
|
|
|
|
// 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"]
|
|
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"]
|
|
make_result({ "stmt": "Expr", "value": val }, p)
|
|
}
|
|
|
|
// ── Top-level parse ────────────────────────────────────────────────────────────
|
|
|
|
fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
|
let total: Int = native_list_len(tokens)
|
|
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"]
|
|
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
|
|
}
|