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:
Orkun ÇAKILKAYA 2026-08-27 18:49:24 +03:00
parent 69fe39bff1
commit dfc17398cf
10 changed files with 3812 additions and 3113 deletions

View file

@ -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()