Phase 2: enforce statement separators (Rule B)
The parser now requires a newline or ';' between statements (block() in selfhost/parse.ludic); two statements may no longer sit adjacent with only spaces. Also fixed if-without-else swallowing its trailing separator. Migration: tools/ludic-tools/migrate_separators.c inserts ';' at statement boundaries corpus-wide (examples, runtime, 25 self-host fragments, ~1100 boundaries). Verified semantically identical — the migrated compiler compiles itself to IR byte-identical to the pre-migration seed, and every golden game renders identically. Reseeded to the strict compiler; C-free fixpoint holds; test.sh 14/14. Docs: Rule B documented in LANGUAGE.md; BOOTSTRAP.md R1 + stale fences updated; check-docs green across all docs. Fixed a multi-line string literal in emit_expr.ludic (byte-identical \n escape) that the C toolchain lexer mis-lexes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
985f9ad8f2
commit
eac8f8f335
35 changed files with 3861 additions and 3588 deletions
|
|
@ -3,25 +3,25 @@
|
|||
# game-construct parsing in compiler/front/parse.c.
|
||||
|
||||
fn parse_component() -> Node {
|
||||
pi = pi + 1 let n = node(N_COMP) n.s = eat_id() skipnl() eat_op("{")
|
||||
while true { skipnl() if is_op("}") { break }
|
||||
let f = node(N_FIELD) f.s = eat_id() eat_op(":") f.ty = ptype()
|
||||
if is_op("=") { pi = pi + 1 f.a = expr() }
|
||||
push(n.kids, f) if is_op(",") { pi = pi + 1 } }
|
||||
eat_op("}") return n
|
||||
pi = pi + 1; let n = node(N_COMP); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true { skipnl(); if is_op("}") { break }
|
||||
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
|
||||
if is_op("=") { pi = pi + 1; f.a = expr() }
|
||||
push(n.kids, f); if is_op(",") { pi = pi + 1 } }
|
||||
eat_op("}"); return n
|
||||
}
|
||||
|
||||
# skip an @annotation or a reads/writes/query/... clause we don't model yet
|
||||
fn skip_clause() -> void {
|
||||
if is_op("[") { let depth = 0
|
||||
while true { if is_op("[") { depth = depth + 1 } if is_op("]") { depth = depth - 1 }
|
||||
pi = pi + 1 if depth == 0 { break } }
|
||||
while true { if is_op("[") { depth = depth + 1 }; if is_op("]") { depth = depth - 1 }
|
||||
pi = pi + 1; if depth == 0 { break } }
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_system() -> Node {
|
||||
if is_id("edge") { pi = pi + 1 } # optional `edge` modifier before `system`
|
||||
pi = pi + 1 let n = node(N_SYS) n.s = eat_id() n.ty = "Update"
|
||||
pi = pi + 1; let n = node(N_SYS); n.s = eat_id(); n.ty = "Update"
|
||||
# clauses: @anno, phase X, reads/writes/needs/uses [..], query (..) [..]
|
||||
# A `query (vars) [terms] where c` clause desugars to a body wrapped in one
|
||||
# `for (vars) in query [terms] where c { ... }` — the same S_QUERY node the
|
||||
|
|
@ -30,15 +30,15 @@ fn parse_system() -> Node {
|
|||
let qn = node(S_QUERY)
|
||||
while true {
|
||||
skipnl() # clauses may span several lines
|
||||
if is_op("@") { pi = pi + 1 let a = eat_id() if is_op("(") { let d = 0 # @anno, one per turn so a
|
||||
while true { if is_op("(") { d = d + 1 } if is_op(")") { d = d - 1 } pi = pi + 1 if d == 0 { break } } }
|
||||
if is_op("@") { pi = pi + 1; let a = eat_id(); if is_op("(") { let d = 0 # @anno, one per turn so a
|
||||
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } }
|
||||
continue } # newline-separated @anno re-skips at the loop top
|
||||
if is_id("phase") { pi = pi + 1 n.ty = eat_id() continue }
|
||||
if is_id("reads") or is_id("writes") or is_id("needs") or is_id("uses") { pi = pi + 1 skip_clause() continue }
|
||||
if is_id("phase") { pi = pi + 1; n.ty = eat_id(); continue }
|
||||
if is_id("reads") or is_id("writes") or is_id("needs") or is_id("uses") { pi = pi + 1; skip_clause(); continue }
|
||||
if is_id("query") {
|
||||
pi = pi + 1 has_q = true
|
||||
pi = pi + 1; has_q = true
|
||||
if is_op("(") { eat_op("(") # optional (vars); omitted when nothing binds
|
||||
while not is_op(")") { let v = node(E_ID) v.s = eat_id() push(qn.kids, v) if is_op(",") { pi = pi + 1 } }
|
||||
while not is_op(")") { let v = node(E_ID); v.s = eat_id(); push(qn.kids, v); if is_op(",") { pi = pi + 1 } }
|
||||
eat_op(")") }
|
||||
qn.c = parse_query_tail()
|
||||
qn.b = qn.c.a # where-expr (or null)
|
||||
|
|
@ -50,7 +50,7 @@ fn parse_system() -> Node {
|
|||
let body = block()
|
||||
if has_q {
|
||||
qn.a = body
|
||||
let wrap = node(N_BLOCK) push(wrap.kids, qn)
|
||||
let wrap = node(N_BLOCK); push(wrap.kids, qn)
|
||||
n.a = wrap
|
||||
} else { n.a = body }
|
||||
return n
|
||||
|
|
@ -62,12 +62,12 @@ fn parse_query_tail() -> Node {
|
|||
eat_op("[")
|
||||
let q = node(N_BLOCK)
|
||||
while not is_op("]") {
|
||||
if is_op("{") { pi = pi + 1 let t = node(E_ID) t.s = eat_id() t.ival = 1 push(q.kids, t) eat_op("}") }
|
||||
else { let t = node(E_ID) t.s = eat_id() t.ival = 0 push(q.kids, t) }
|
||||
if is_op("{") { pi = pi + 1; let t = node(E_ID); t.s = eat_id(); t.ival = 1; push(q.kids, t); eat_op("}") }
|
||||
else { let t = node(E_ID); t.s = eat_id(); t.ival = 0; push(q.kids, t) }
|
||||
if is_op(",") { pi = pi + 1 }
|
||||
}
|
||||
eat_op("]")
|
||||
if is_id("where") { pi = pi + 1 q.a = expr() }
|
||||
if is_id("where") { pi = pi + 1; q.a = expr() }
|
||||
return q
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ fn parse_query_tail() -> Node {
|
|||
fn parse_query_for() -> Node {
|
||||
let n = node(S_QUERY)
|
||||
eat_op("(")
|
||||
while not is_op(")") { let v = node(E_ID) v.s = eat_id() push(n.kids, v) if is_op(",") { pi = pi + 1 } }
|
||||
while not is_op(")") { let v = node(E_ID); v.s = eat_id(); push(n.kids, v); if is_op(",") { pi = pi + 1 } }
|
||||
eat_op(")")
|
||||
let inkw = eat_id() # 'in'
|
||||
let qkw = eat_id() # 'query'
|
||||
|
|
@ -86,59 +86,59 @@ fn parse_query_for() -> Node {
|
|||
}
|
||||
|
||||
fn parse_spawn() -> Node {
|
||||
pi = pi + 1 let n = node(S_SPAWN) n.s = eat_id() skipnl() eat_op("{")
|
||||
pi = pi + 1; let n = node(S_SPAWN); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true {
|
||||
skipnl() if is_op("}") { break }
|
||||
let ci = node(E_FINIT) ci.s = eat_id() eat_op("=")
|
||||
skipnl(); if is_op("}") { break }
|
||||
let ci = node(E_FINIT); ci.s = eat_id(); eat_op("=")
|
||||
ci.a = record() # { field = val, ... }
|
||||
push(n.kids, ci)
|
||||
if is_op(",") { pi = pi + 1 }
|
||||
}
|
||||
eat_op("}") return n
|
||||
eat_op("}"); return n
|
||||
}
|
||||
|
||||
# archetype Name { CompA, CompB } — a named entity kind (bundle of components)
|
||||
fn parse_archetype() -> Node {
|
||||
pi = pi + 1 let n = node(N_ARCH) n.s = eat_id() skipnl() eat_op("{")
|
||||
while true { skipnl() if is_op("}") { break }
|
||||
let c = node(E_ID) c.s = eat_id() push(n.kids, c)
|
||||
if is_op(",") { pi = pi + 1 } skipnl() }
|
||||
eat_op("}") return n
|
||||
pi = pi + 1; let n = node(N_ARCH); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true { skipnl(); if is_op("}") { break }
|
||||
let c = node(E_ID); c.s = eat_id(); push(n.kids, c)
|
||||
if is_op(",") { pi = pi + 1 }; skipnl() }
|
||||
eat_op("}"); return n
|
||||
}
|
||||
|
||||
# extern fn name(params) -> T = "symbol"
|
||||
fn parse_extern() -> Node {
|
||||
pi = pi + 1 # 'extern'
|
||||
let fnkw = eat_id() # 'fn'
|
||||
let n = node(N_EXTERN) n.s = eat_id() eat_op("(")
|
||||
while not is_op(")") { let p = node(N_PARAM) p.s = eat_id() eat_op(":") p.ty = ptype() push(n.kids, p)
|
||||
let n = node(N_EXTERN); n.s = eat_id(); eat_op("(")
|
||||
while not is_op(")") { let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype(); push(n.kids, p)
|
||||
if is_op(",") { pi = pi + 1 } }
|
||||
eat_op(")")
|
||||
n.ty = "void"
|
||||
if is_op("->") { pi = pi + 1 n.ty = ptype() }
|
||||
if is_op("->") { pi = pi + 1; n.ty = ptype() }
|
||||
eat_op("=")
|
||||
let t = toks[pi] # "symbol"
|
||||
n.a = node(E_STR) n.a.s = t.text pi = pi + 1
|
||||
n.a = node(E_STR); n.a.s = t.text; pi = pi + 1
|
||||
return n
|
||||
}
|
||||
|
||||
# ui Name { widget-tree } — parsed into a widget node tree (emitted later)
|
||||
fn parse_widget() -> Node {
|
||||
let w = node(N_UI) w.s = eat_id() # widget type name
|
||||
let w = node(N_UI); w.s = eat_id() # widget type name
|
||||
w.b = node(N_BLOCK) # b.kids = props (E_FINIT)
|
||||
while toks[pi].kind == TK_ID and toks[pi + 1].kind == TK_OP and streq(toks[pi + 1].text, "=") {
|
||||
let pr = node(E_FINIT) pr.s = eat_id() eat_op("=") pr.a = expr() push(w.b.kids, pr)
|
||||
let pr = node(E_FINIT); pr.s = eat_id(); eat_op("="); pr.a = expr(); push(w.b.kids, pr)
|
||||
}
|
||||
skipnl()
|
||||
if is_op("{") { pi = pi + 1
|
||||
while true { skipnl() if is_op("}") { break } push(w.kids, parse_widget()) }
|
||||
while true { skipnl(); if is_op("}") { break }; push(w.kids, parse_widget()) }
|
||||
eat_op("}") }
|
||||
return w
|
||||
}
|
||||
fn parse_ui() -> Node {
|
||||
pi = pi + 1 let n = node(N_UI) n.s = eat_id() n.ival = 1 # ival=1 marks the top ui block
|
||||
skipnl() eat_op("{") skipnl()
|
||||
pi = pi + 1; let n = node(N_UI); n.s = eat_id(); n.ival = 1 # ival=1 marks the top ui block
|
||||
skipnl(); eat_op("{"); skipnl()
|
||||
n.a = parse_widget()
|
||||
skipnl() eat_op("}")
|
||||
skipnl(); eat_op("}")
|
||||
return n
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue