404 lines
18 KiB
Text
404 lines
18 KiB
Text
# parse_game.ludic — the ECS front-end: property, model, handler, scene, ui and
|
|
# enum declarations, `for (vars) in query [terms] where cond`, spawn and prefab.
|
|
|
|
function parse_component() -> Node {
|
|
pi += 1; let n = node(N_COMP); n.s = eat_id()
|
|
if is_op("<") { n.tps = gen_params() } # L5: property Pool<T> { ... }
|
|
gen_enter(n.tps)
|
|
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 += 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 += 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 += 1 } }
|
|
eat_op("}")
|
|
gen_enter(null)
|
|
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 += 1; let n = node(N_EVENT)
|
|
if is_id("cancellable") { 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 += 1; f.a = expr() }
|
|
push(n.kids, f)
|
|
if is_op(",") { pi += 1 } }
|
|
eat_op("}"); return n
|
|
}
|
|
|
|
function parse_system() -> Node {
|
|
pi += 1; let n = node(N_SYS); n.s = eat_id(); n.ty = "Update"
|
|
n.pos2 = toks[pi - 1].end # 0.S: where state parameters would go
|
|
let ps = parse_entry_params() # 0.S: handler Draw(h: Hiker) phase Render
|
|
# 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 += 1; let a = eat_id(); if is_op("(") { var d = 0 # @anno, one per turn so a
|
|
while true { if is_op("(") { d += 1 }; if is_op(")") { d -= 1 }; pi += 1; if d == 0 { break } } }
|
|
continue } # newline-separated @anno re-skips at the loop top
|
|
if is_id("phase") {
|
|
pi += 1; n.ty = eat_id()
|
|
if not is_phase_name(n.ty) { perr(`unknown phase '{n.ty}' (expected Start, Input, FixedUpdate, Update, LateUpdate, Render or Overlay)`) }
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
skipnl()
|
|
n.a = block()
|
|
n.a.pos2 = n.pos2 # 0.S2: a listener is known by its body
|
|
entry_bind(n.a, ps)
|
|
return n
|
|
}
|
|
|
|
# `[Term, ...]` with optional `where <expr>`, 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 += 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 += 1 }
|
|
}
|
|
eat_op("]")
|
|
if is_id("where") { 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 += 1 } }
|
|
eat_op(")")
|
|
let inkw = eat_id() # 'in'
|
|
if not is_id("query") { perr("expected 'query' in for-loop") }
|
|
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 <constraints> { 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 += 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 += 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 += 1 }
|
|
skipnl()
|
|
}
|
|
eat_op(")")
|
|
qn.c = terms; qn.b = wh
|
|
return qn
|
|
}
|
|
|
|
function parse_spawn() -> Node {
|
|
pi += 1; let n = node(S_SPAWN); n.s = eat_id(); skipnl(); eat_op("{")
|
|
parse_component_records(n)
|
|
eat_op("}"); return n
|
|
}
|
|
# `Comp { field: val, ... }, Comp { ... }` — the body shared by spawn and prefab
|
|
function parse_component_records(n: Node) -> void {
|
|
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 += 1 }
|
|
}
|
|
}
|
|
# prefab Name: Model { Comp { field: val }, ... } — a model with preset fields.
|
|
# `spawn Name { ... }` spawns the model with the prefab's fields, then the spawn's
|
|
# own overrides on top; `Prefab.spawn(name: "Name")` does it by name at runtime.
|
|
function parse_prefab() -> Node {
|
|
pi += 1; let n = node(N_PREFAB); n.s = eat_id(); eat_op(":"); n.ty = eat_id(); skipnl(); eat_op("{")
|
|
parse_component_records(n)
|
|
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 += 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_<S>_enter / scene_<S>_exit).
|
|
var is_pub = false
|
|
var lasts: Node = null # lasts N then Next: a timed scene (a banner, a splash)
|
|
var then_scene: pointer = null
|
|
var loads_then: pointer = null # loads then Next: pump the Assets queue, draw a bar, go on when ready
|
|
while is_id("start") or is_id("public") or is_id("shows") or is_id("lasts") or is_id("loads") {
|
|
if is_id("start") { pi += 1; g_start_scene = g_scene_count }
|
|
else if is_id("shows") { pi += 1; n.ty = eat_id() } # shows Menu: the engine opens, renders and closes it
|
|
else if is_id("lasts") { pi += 1; lasts = expr(); if not is_id("then") { perr("lasts N needs `then Scene`") }; pi += 1; then_scene = eat_id() }
|
|
else if is_id("loads") { pi += 1; if not is_id("then") { perr("loads needs `then Scene`") }; pi += 1; loads_then = eat_id(); g_uses_atlas = true }
|
|
else { pi += 1; is_pub = true }
|
|
}
|
|
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 += 1; let which = eat_id()
|
|
let at = toks[pi - 1].end # 0.S: where its states would go
|
|
let ps = parse_entry_params() # on enter (h: mut Hiker) { .. }
|
|
skipnl()
|
|
var body: Node = null
|
|
if which == "enter" { n.a = block(); body = n.a }
|
|
else if which == "exit" { n.b = block(); body = n.b }
|
|
else { perr("expected 'enter' or 'exit' after 'on'") }
|
|
body.pos2 = at
|
|
entry_bind(body, ps)
|
|
continue
|
|
}
|
|
if is_id("layer") { # layer Name [public] { handler .. }
|
|
pi += 1; let lname = eat_id()
|
|
if is_id("public") { 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 }
|
|
var qspec: Node = null # `@Queries(...)` makes the handler run once per matching entity
|
|
while is_op("@") {
|
|
pi += 1; let a = eat_id()
|
|
if (a == "Queries") { qspec = parse_queries_anno() }
|
|
else { perr(`a handler inside a layer may only carry @Queries (got @{a})`) }
|
|
skipnl()
|
|
}
|
|
if not is_id("handler") { perr("expected 'handler' in layer") }
|
|
let h = parse_system() # N_SYS: .s name, .ty phase, .a body
|
|
if (qspec != null) { qspec.a = h.a; let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap }
|
|
h.s = `{n.s}_{h.s}` # scene-qualified: two scenes may both have a `Draw` handler
|
|
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("}")
|
|
if (lasts != null) { scene_add_timer(n, lasts, then_scene) }
|
|
if (loads_then != null) { scene_add_loader(n, loads_then) }
|
|
push(g_scenes, n)
|
|
}
|
|
|
|
# `scene S loads then T`: an Update handler that pumps the Assets queue and
|
|
# `become`s T once it is ready, and a Render handler that draws the default bar.
|
|
function scene_add_loader(sc: Node, next: pointer) -> void {
|
|
let h = node(N_SYS); h.s = `{sc.s}_Loader`; h.ty = "Update"; h.c = sc
|
|
let body = node(N_BLOCK)
|
|
let pump = node(S_EXPR); let pc = node(E_CALL); let pm = node(E_MEMBER); let pid = node(E_ID); pid.s = "Assets"; pm.a = pid; pm.s = "pump"; pc.a = pm
|
|
let n4 = node(E_INT); n4.ival = 4; push(pc.kids, n4); pump.a = pc; push(body.kids, pump)
|
|
let go = node(S_IF); let rc = node(E_CALL); let rm = node(E_MEMBER); let rid = node(E_ID); rid.s = "Assets"; rm.a = rid; rm.s = "ready"; rc.a = rm; go.a = rc
|
|
go.b = node(N_BLOCK); let bec = node(S_BECOME); bec.s = next; push(go.b.kids, bec); push(body.kids, go)
|
|
h.a = body; push(prog, h)
|
|
let d = node(N_SYS); d.s = `{sc.s}_LoaderBar`; d.ty = "Render"; d.c = sc
|
|
let dbody = node(N_BLOCK); let dc = node(E_CALL); let did = node(E_ID); did.s = "assets_draw_progress"; dc.a = did
|
|
let ds = node(S_EXPR); ds.a = dc; push(dbody.kids, ds); d.a = dbody; push(prog, d)
|
|
}
|
|
|
|
# `scene S lasts N then T`: a counter var, set on enter, counted down by a
|
|
# generated Update handler that `become`s T at zero — the splash / banner scene.
|
|
function scene_add_timer(sc: Node, frames: Node, next: pointer) -> void {
|
|
let counter = `scene_{sc.s}_left`
|
|
let v = node(N_VAR); v.s = counter; v.ty = "int"; v.pos = -1; push(prog, v) # the compiler's own
|
|
if (sc.a == null) { sc.a = node(N_BLOCK) }
|
|
let set = node(S_ASSIGN); set.s = "="; let lhs = node(E_ID); lhs.s = counter; set.a = lhs; set.b = frames
|
|
let ins = new []Node; push(ins, set)
|
|
var i = 0
|
|
while i < len(sc.a.kids) { push(ins, sc.a.kids[i]); i += 1 }
|
|
sc.a.kids = ins
|
|
let h = node(N_SYS); h.s = `{sc.s}_Timer`; h.ty = "Update"; h.c = sc
|
|
let body = node(N_BLOCK)
|
|
let dec = node(S_ASSIGN); dec.s = "-="; let l2 = node(E_ID); l2.s = counter; dec.a = l2; let one = node(E_INT); one.ival = 1; dec.b = one
|
|
push(body.kids, dec)
|
|
let test = node(E_BIN); test.s = "<="; let l3 = node(E_ID); l3.s = counter; test.a = l3; let zero = node(E_INT); zero.ival = 0; test.b = zero
|
|
let go = node(S_IF); go.a = test; go.b = node(N_BLOCK)
|
|
let bec = node(S_BECOME); bec.s = next; push(go.b.kids, bec)
|
|
push(body.kids, go)
|
|
h.a = body
|
|
push(prog, h)
|
|
}
|
|
|
|
# `button id: Play goto: Scene`: a click changes scene. Every such button becomes a
|
|
# generated UiClicked listener — `if id == UI_Name { become Scene }` — the same
|
|
# code a game would write by hand.
|
|
function ui_register_gotos() -> void {
|
|
var i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_UI and prog[i].ival == 1 { ui_gotos_in(prog[i].a) }
|
|
i += 1
|
|
}
|
|
}
|
|
function ui_gotos_in(w: Node) -> void {
|
|
var idn: pointer = null
|
|
var goto: pointer = null
|
|
var p = 0
|
|
while p < len(w.b.kids) {
|
|
let pr = w.b.kids[p]
|
|
if (pr.s == "id") and (pr.a.kind == E_ID) { idn = pr.a.s }
|
|
if (pr.s == "goto") and (pr.a.kind == E_ID) { goto = pr.a.s }
|
|
p += 1
|
|
}
|
|
if (idn != null) and (goto != null) {
|
|
let body = node(N_BLOCK)
|
|
let test = node(E_BIN); test.s = "=="; let l = node(E_ID); l.s = "id"; test.a = l; let r = node(E_ID); r.s = "UI_" + idn; test.b = r
|
|
let go = node(S_IF); go.a = test; go.b = node(N_BLOCK)
|
|
let bec = node(S_BECOME); bec.s = goto; push(go.b.kids, bec)
|
|
push(body.kids, go)
|
|
register_onlisten("UiClicked", body)
|
|
}
|
|
var k = 0
|
|
while k < len(w.kids) { ui_gotos_in(w.kids[k]); k += 1 }
|
|
}
|
|
|
|
# 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.
|
|
#
|
|
# A variant may also carry a payload — `enum Tile { Empty, Wall, Door(int),
|
|
# Portal(int, int) }` (issue #56). A payload turns the whole enum into a tagged
|
|
# union: variants are constructed by name (`Door(3)`, bare `Empty`) and boxed
|
|
# (a tag + payload slots), and `match` destructures them (`Door(n) => …`). An
|
|
# all-payload-less enum keeps the zero-cost compile-time-ordinal representation.
|
|
# Each variant node is an E_ID (s=variant name); its payload types are stored as
|
|
# N_PARAM kids, one per slot, carrying only `.ty`.
|
|
function parse_enum() -> Node {
|
|
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()
|
|
if is_op("(") { # variant payload: Door(int), Portal(int, int)
|
|
pi += 1; skipnl()
|
|
while not is_op(")") {
|
|
let p = node(N_PARAM); p.ty = ptype(); push(v.kids, p)
|
|
if is_op(",") { pi += 1 }; skipnl() }
|
|
eat_op(")") }
|
|
push(n.kids, v)
|
|
if is_op(",") { 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 += 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 += 1; let a = eat_id(); if (a == "Sync") { c.ival = 1 }; skipnl() }
|
|
c.s = eat_id(); push(n.kids, c)
|
|
if is_op(",") { pi += 1 }; skipnl() }
|
|
eat_op("}"); return n
|
|
}
|
|
|
|
# extern function name(params) -> T = "symbol"
|
|
function parse_extern() -> Node {
|
|
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 += 1 } }
|
|
eat_op(")")
|
|
n.ty = "void"
|
|
if is_op("->") { pi += 1; n.ty = ptype() }
|
|
eat_op("=")
|
|
let t = toks[pi] # "symbol"
|
|
n.a = node(E_STR); n.a.s = t.text; 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 += 1
|
|
while true { skipnl(); if is_op("}") { break }; push(w.kids, parse_widget()) }
|
|
eat_op("}") }
|
|
return w
|
|
}
|
|
function parse_ui() -> Node {
|
|
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
|
|
}
|