# parse_game.ludic — the ECS front-end: component and system declarations, # `for (vars) in query [terms] where cond`, spawn and despawn. Mirrors the # game-construct parsing in compiler/front/parse.c. function 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 } var is_computed = false var is_sync = false # @Sync — this field replicates (NETWORKING N2) if is_op("@") { pi = pi + 1; let ann = eat_id(); if (ann == "Computed") { is_computed = true } else { if (ann == "Sync") { is_sync = true } }; skipnl() } let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype() if is_op("=") { pi = pi + 1; f.a = expr() } if is_sync { f.ival = 1 } # mark the field replicable (read by emit_net) if is_computed { register_computed(n.s, f.s, f.ty, f.a) } # derived: no storage else { push(n.kids, f) } if is_op(",") { pi = pi + 1 } } eat_op("}"); return n } # event Name { field: T = default, ... } — a public event's POD payload. Same # field grammar as a `property`, but stored in g_events, not prog: an event is a # signal shape, not per-entity storage. Zero fields is allowed (`event Ping {}`). function parse_event() -> Node { pi = pi + 1; let n = node(N_EVENT) if is_id("cancellable") { pi = pi + 1; n.ival = 1 } # a decision event: listeners may `cancel` it 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 } function parse_system() -> Node { pi = pi + 1; let n = node(N_SYS); n.s = eat_id(); n.ty = "Update" # postfix clauses on `handler Name …`: @anno(...) (parsed and reserved, e.g. # @deterministic / @Reads(...) / @Writes(...)) and `phase X`. The handler's # query lives in a prefix `@Queries(...)` annotation (see parse_one_decl), not # in a signature clause. while true { skipnl() # clauses may span several lines if is_op("@") { pi = pi + 1; let a = eat_id(); if is_op("(") { var 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 } break } skipnl() n.a = block() return n } # `[Term, ...]` with optional `where `, returning a node whose kids are # the terms (E_ID with ival=1 for {Tag} filters) and .a the where-expr or null. function 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 } } eat_op("]") if is_id("where") { pi = pi + 1; q.a = expr() } return q } # `for (a, b) in query [Pos, Vel] where ... { body }` function 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 } } eat_op(")") let inkw = eat_id() # 'in' if not is_id("query") { perr("expected 'query' in for-loop") } pi = pi + 1 # 'query' n.c = parse_query_tail() n.b = n.c.a # where n.a = block() return n } # ---- @Queries annotation ----------------------------------------------------- # `@Queries(these: [Prop{constraint}, ...], on: Model)` on a handler is an # annotation spelling of the `for (Prop, ...) in query [Prop, ..., {Model}] # where { body }` loop. It desugars to the same S_QUERY node, so # the whole query backend (iteration, filters, binding, break/continue) is reused. function mk_and(a: Node, b: Node) -> Node { if (a == null) { return b } let n = node(E_BIN); n.s = "and"; n.a = a; n.b = b; return n } # Rewrite each bare identifier in `e` as `base.field` — used both by # `Prop{constraint}` (base is the property binding) and by @Computed field # expansion (base is the accessed value). Non-destructive: builds a fresh tree, # so a stored computed expression can be expanded at many access sites. function qualify_fields(e: Node, base: Node) -> Node { if (e == null) { return e } if e.kind == E_ID { let m = node(E_MEMBER); m.a = base; m.s = e.s; return m } if e.kind == E_BIN { let n2 = node(E_BIN); n2.s = e.s; n2.a = qualify_fields(e.a, base); n2.b = qualify_fields(e.b, base); return n2 } if e.kind == E_UN { let n2 = node(E_UN); n2.s = e.s; n2.a = qualify_fields(e.a, base); return n2 } return e } # parse `(these: [...], on: Model)`, returning an S_QUERY with its vars/terms/where # filled in (the body `.a` is attached by the caller once the handler is parsed). function parse_queries_anno() -> Node { eat_op("(") let qn = node(S_QUERY) let terms = node(N_BLOCK) var wh: Node = null while not is_op(")") { skipnl() if is_op(")") { break } let key = eat_id(); eat_op(":") if (key == "these") { eat_op("["); skipnl() while not is_op("]") { let pname = eat_id() let v = node(E_ID); v.s = pname; push(qn.kids, v) # binding var = property name let t = node(E_ID); t.s = pname; t.ival = 0; push(terms.kids, t) if is_op("{") { pi = pi + 1; let ce = expr(); eat_op("}") let cb = node(E_ID); cb.s = pname; wh = mk_and(wh, qualify_fields(ce, cb)) } if is_op(",") { pi = pi + 1 } skipnl() } eat_op("]") } else { if (key == "on") { let mname = eat_id(); let t = node(E_ID); t.s = mname; t.ival = 1; push(terms.kids, t) # {Model} tag } else { expr() } } # unknown key: skip its value if is_op(",") { pi = pi + 1 } skipnl() } eat_op(")") qn.c = terms; qn.b = wh return qn } function parse_spawn() -> Node { 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() ci.a = record() # Comp { field: val, ... } — no `=` before the record push(n.kids, ci) if is_op(",") { pi = pi + 1 } } eat_op("}"); return n } # scene Name [start] { on enter {..} on exit {..} layer L { handler .. } .. } # A scene groups handlers behind an implicit active-scene register; only the # active scene's handlers run each phase. `on enter`/`on exit` are lifecycle # blocks (scene .a/.b); each layer's handlers are pushed straight into `prog` as # ordinary N_SYS nodes, tagged with the owning scene in `.c`, so the whole # system backend (functions, phases, enable/disable) is reused unchanged. function parse_scene() -> void { pi = pi + 1 # 'scene' let n = node(N_SCENE); n.s = eat_id() n.ival = g_scene_count # optional modifiers after the name, any order: `start` (the boot scene) and # `public` (promote its on-enter/on-exit to scene__enter / scene__exit). var is_pub = false while is_id("start") or is_id("public") { if is_id("start") { pi = pi + 1; g_start_scene = g_scene_count } else { pi = pi + 1; is_pub = true } } g_scene_count = g_scene_count + 1 if is_pub { ensure_event_empty(`scene_{n.s}_enter`); ensure_event_empty(`scene_{n.s}_exit`) } skipnl(); eat_op("{") while true { skipnl(); if is_op("}") { break } if is_id("on") { # on enter { .. } / on exit { .. } pi = pi + 1; let which = eat_id(); skipnl() if (which == "enter") { n.a = block() } else { if (which == "exit") { n.b = block() } else { perr("expected 'enter' or 'exit' after 'on'") } } continue } if is_id("layer") { # layer Name [public] { handler .. } pi = pi + 1; let lname = eat_id() if is_id("public") { pi = pi + 1; ensure_event_empty(`layer_{lname}_show`); ensure_event_empty(`layer_{lname}_hide`) } skipnl(); eat_op("{") let ltag = node(E_ID); ltag.s = lname # the layer name, tagged onto each handler while true { skipnl(); if is_op("}") { break } if not is_id("handler") { perr("expected 'handler' in layer") } let h = parse_system() # N_SYS: .s name, .ty phase, .a body h.c = n # tag the owning scene (null = global) h.b = ltag # tag the owning layer (for enable/disable layer) push(prog, h) skipnl() } eat_op("}") continue } perr("expected 'on', 'layer' or '}' in scene") } eat_op("}") push(g_scenes, n) } # enum Name { A, B, C } — named int constants; a variant's value is its index. # Accessed as `Name.A` (a compile-time int), so it names magic-int value spaces # (state ids, menu selections, mode registers) without a runtime cost. function parse_enum() -> Node { pi = pi + 1; let n = node(N_ENUM); n.s = eat_id(); skipnl(); eat_op("{") while true { skipnl(); if is_op("}") { break } let v = node(E_ID); v.s = eat_id(); push(n.kids, v) if is_op(",") { pi = pi + 1 }; skipnl() } eat_op("}"); return n } # archetype Name { CompA, @Sync CompB } — a named entity kind (bundle of # components). A member marked `@Sync` *participates* in replication (NETWORKING # N2): its @Sync-marked fields cross the wire for this model. Participation is # per model use-site — the same property syncs in one model, not another. The # per-member @Sync sets the member E_ID's ival=1 (read by emit_net). function 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) if is_op("@") { pi = pi + 1; let a = eat_id(); if (a == "Sync") { c.ival = 1 }; skipnl() } c.s = eat_id(); push(n.kids, c) if is_op(",") { pi = pi + 1 }; skipnl() } eat_op("}"); return n } # extern function name(params) -> T = "symbol" function 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) if is_op(",") { pi = pi + 1 } } eat_op(")") n.ty = "void" 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 return n } # ui Name { widget-tree } — parsed into a widget node tree (emitted later) function parse_widget() -> Node { 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 (toks[pi + 1].text == ":") { let pr = node(E_FINIT); pr.s = eat_id(); eat_op(":"); pr.a = expr(); push(w.b.kids, pr) # widget prop `key: value` (Rule A) } skipnl() if is_op("{") { pi = pi + 1 while true { skipnl(); if is_op("}") { break }; push(w.kids, parse_widget()) } eat_op("}") } return w } function 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() n.a = parse_widget() skipnl(); eat_op("}") return n }