schema: ludicc --emit-schema / ludic schema, --check --diagnostics=json, and editor attributes

--emit-schema FILE writes the compiler's resolved view once the program type-checks: every
record (fields, types, defaults as written, docs, places, attributes), every registry with its
entries in their final order after the open-registry merge (key, constant, index, file:line:col
of the entry and of each field value, and which file contributed which keys), every const, and
the zero-argument functions a fn value can name. Deterministic, schema_version 1; the runtime is
left out. `ludic schema [file] [-o FILE]` wraps it.

--check --diagnostics=json prints every error as one JSON array on stdout: the checker's and the
module rules' all, a parse or lowering error as the last. Tokens and nodes now carry a column.

Fields take several @attributes; @Ref(Registry), @OneOf(PREFIX_), @Range(lo, hi), @Unit("..."),
@Asset("..."), @Color on a field and @AppendOnly / @ByKey on a registry change nothing but go into
the schema, and @Ref naming no registry is an error (every one reported). Fixtures:
examples/lang/attributes.ludic, examples/rejected/ref_unknown.ludic, cases in ludic-dev test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 18:06:54 +03:00
parent f8d811aa97
commit 42deb76c28
27 changed files with 1220 additions and 23 deletions

View file

@ -11,14 +11,21 @@ function parse_component() -> Node {
var is_sync = false # @Sync — this field replicates (NETWORKING N2)
var is_frame = false # @frame — a fn stored here runs every frame (25.2)
var cap = 0 # @max(n) — this list never holds more than n (25.5a)
if is_op("@") {
var fat = -1 # its attributes, for the schema (attrs.ludic)
while is_op("@") { # one or more, on its line or the lines above
pi += 1
let ann = eat_id()
if ann == "Computed" { is_computed = true } else if ann == "Sync" { is_sync = true } else if ann == "frame" { is_frame = true }
if (ann == "Computed") or (ann == "Sync") or (ann == "frame") {
if ann == "Computed" { is_computed = true } else if ann == "Sync" { is_sync = true } else { is_frame = true }
if fat < 0 { fat = at_group() }
at_push(fat, ann, "[]", null, 0)
}
else if ann == "owns" { # @owns(PhysShape) — this field holds a handle its record owns (25.5e)
eat_op("(")
g_own_pending = eat_id()
eat_op(")")
if fat < 0 { fat = at_group() }
at_push(fat, ann, `["{g_own_pending}"]`, g_own_pending, 1)
}
else if ann == "max" {
eat_op("(")
@ -26,14 +33,30 @@ function parse_component() -> Node {
if ce.kind != E_INT or ce.ival <= 0 { perr("@max takes the most the list may hold: @max(64)") }
cap = ce.ival
eat_op(")")
if fat < 0 { fat = at_group() }
at_push(fat, ann, `[{itoa(cap)}]`, itoa(cap), 1)
}
else if at_field_known(ann) { # @Ref(Items), @Range(0, 1), @Unit("m/s"), ...
if fat < 0 { fat = at_group() }
at_field(fat, ann)
}
else if is_op("(") { # anything else: read past, as a declaration's is
var d = 0
while true { if is_op("(") { d += 1 }; if is_op(")") { d -= 1 }; pi += 1; if d == 0 { break } }
}
skipnl()
}
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
f.at = fat
if is_frame { push(g_frame_fields, `{n.s}.{f.s}`) }
if cap > 0 { push(g_cap_keys, `{n.s}.{f.s}`); push(g_cap_vals, cap) }
if g_own_pending != null { push(g_own_type, n.s); push(g_own_field, f.s); push(g_own_kind, g_own_pending); g_own_pending = null }
if is_op("=") { pi += 1; f.a = expr() }
if is_op("=") {
pi += 1
let v0 = pi
f.a = expr()
f.sp0 = toks[v0].off; f.sp1 = toks[pi - 1].oend # the schema's text of the default
}
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) }
@ -52,7 +75,12 @@ function parse_event() -> Node {
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() }
if is_op("=") {
pi += 1
let v0 = pi
f.a = expr()
f.sp0 = toks[v0].off; f.sp1 = toks[pi - 1].oend
}
push(n.kids, f)
if is_op(",") { pi += 1 } }
eat_op("}"); return n