Phase 6b: annotation DSL (@Queries, @Handles) + docs prose pass
@Queries(these: [Prop{constraint}, ...], on: Model) on a handler desugars to the
existing S_QUERY loop: each property binds by its own name, a Prop{...} block
qualifies its bare fields to that property, and on: adds a {Model} tag filter.
Implemented via parse_queries_anno + qualify_fields (parse_game.ludic), wired
into parse_one_decl; @Handles parses on the program (documentation).
examples/annotations.ludic demonstrates it (output 3 1 0 0); test.sh 15/15.
Docs: added the Annotations section to LANGUAGE.md and did the vocabulary prose
pass (component->property, archetype->model, system->handler, game->program)
across the docs. check-docs + test-tools green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
69fe39bff1
commit
dfc17398cf
10 changed files with 3812 additions and 3113 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -281,11 +281,13 @@ fn already_loaded(full: ptr) -> bool {
|
|||
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
|
||||
fn parse_one_decl() -> void {
|
||||
let is_export = false
|
||||
let qspec: Node = ptr_null()
|
||||
while is_op("@") {
|
||||
pi = pi + 1; let a = eat_id() # collect a leading @annotation
|
||||
if streq(a, "export") { is_export = true }
|
||||
if is_op("(") { let d = 0 # optional @anno(args) — skipped
|
||||
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } }
|
||||
if streq(a, "Queries") { qspec = parse_queries_anno() } # @Queries(these: [...], on: ...)
|
||||
else { if is_op("(") { let d = 0 # any other @anno(args) — parsed and skipped
|
||||
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } } }
|
||||
skipnl()
|
||||
}
|
||||
if is_id("import") { pi = pi + 1
|
||||
|
|
@ -299,7 +301,14 @@ fn parse_one_decl() -> void {
|
|||
if is_id("enum") { push(prog, parse_enum()); return }
|
||||
if is_id("property") { push(prog, parse_component()); return }
|
||||
if is_id("model") { push(prog, parse_archetype()); return }
|
||||
if is_id("handler") { push(prog, parse_system()); return }
|
||||
if is_id("handler") {
|
||||
let h = parse_system()
|
||||
if not ptr_is_null(qspec) { # @Queries wraps the body in its S_QUERY
|
||||
qspec.a = h.a
|
||||
let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap
|
||||
}
|
||||
push(prog, h); return
|
||||
}
|
||||
if is_id("ui") { push(prog, parse_ui()); return }
|
||||
if is_id("var") { push(prog, parse_var()); return }
|
||||
if is_id("const") { push(prog, parse_const()); return }
|
||||
|
|
@ -340,8 +349,15 @@ fn parse_program() -> void {
|
|||
loaded_paths = new []ptr
|
||||
skipnl()
|
||||
g_game_name = "Ludic"
|
||||
# imports may precede the game block
|
||||
# imports may precede the program block
|
||||
while is_id("import") { pi = pi + 1; let t = toks[pi]; let rel = t.text; pi = pi + 1; do_import(rel); skipnl() }
|
||||
# @annotations on the program itself (e.g. @Handles(Movement)) — parsed, skipped
|
||||
while is_op("@") {
|
||||
pi = pi + 1; let a = eat_id()
|
||||
if is_op("(") { let d = 0
|
||||
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } }
|
||||
skipnl()
|
||||
}
|
||||
if is_id("program") { pi = pi + 1; g_game_name = eat_id(); skipnl(); eat_op("{") }
|
||||
while true {
|
||||
skipnl()
|
||||
|
|
|
|||
|
|
@ -84,6 +84,62 @@ fn parse_query_for() -> Node {
|
|||
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.
|
||||
|
||||
fn mk_and(a: Node, b: Node) -> Node {
|
||||
if ptr_is_null(a) { return b }
|
||||
let n = node(E_BIN); n.s = "and"; n.a = a; n.b = b; return n
|
||||
}
|
||||
|
||||
# In `Prop{scale > 0}` the bare names are fields of Prop; qualify each to
|
||||
# `Prop.field` (the binding var is the property name) for the desugared where.
|
||||
fn qualify_fields(e: Node, prop: ptr) -> Node {
|
||||
if ptr_is_null(e) { return e }
|
||||
if e.kind == E_ID {
|
||||
let m = node(E_MEMBER); let base = node(E_ID); base.s = prop; m.a = base; m.s = e.s; return m
|
||||
}
|
||||
if e.kind == E_BIN { e.a = qualify_fields(e.a, prop); e.b = qualify_fields(e.b, prop); return e }
|
||||
if e.kind == E_UN { e.a = qualify_fields(e.a, prop); return e }
|
||||
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).
|
||||
fn parse_queries_anno() -> Node {
|
||||
eat_op("(")
|
||||
let qn = node(S_QUERY)
|
||||
let terms = node(N_BLOCK)
|
||||
let wh: Node = ptr_null()
|
||||
while not is_op(")") {
|
||||
skipnl()
|
||||
if is_op(")") { break }
|
||||
let key = eat_id(); eat_op(":")
|
||||
if streq(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("}"); wh = mk_and(wh, qualify_fields(ce, pname)) }
|
||||
if is_op(",") { pi = pi + 1 }
|
||||
skipnl()
|
||||
}
|
||||
eat_op("]")
|
||||
} else { if streq(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
|
||||
}
|
||||
|
||||
fn parse_spawn() -> Node {
|
||||
pi = pi + 1; let n = node(S_SPAWN); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue