ludic schema: a components section and a natives list (R4)

Each UI component: module, place, doc, xml and lss paths, props and state
(type, default as written, place, doc), states_read (the header's states),
derived fields with their types, functions and events as the template calls
them (states and instance stripped), and the native tags its template uses.
natives: every ui_native / ui_native_input call with a literal tag - tag, via,
handler, place. schema_version stays 1; the lists are additions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 19:36:04 +03:00
parent 28f7b4666b
commit 281ebc23e1
11 changed files with 80436 additions and 71227 deletions

View file

@ -680,6 +680,19 @@ object with `"schema_version": 1`:
`params` (the arguments a caller passes), `states` (what the runtime supplies), `signature`, and
`fn_type` - the type a `fn` field sees, the states stripped, spelled as a field's type is
(`fn(NpcPerson,float)->bool`), so matching a function to a field is comparing two strings.
- `components` - every UI component (see Components): its module, place, doc, `xml` and `lss` (the
template's and the stylesheet's paths, `lss` null when it has none); `props` and `state`, the
instance's own fields in the order written, each with its type, its default as written (or null),
its place and doc (a prop's `attributes` are `[]`: a component's members take none); `states_read`,
the program's states its header names, which the runtime supplies and the template never sees;
`derived`, the fields worked out each frame, with their types (an inferred one resolved);
`functions` (`{"name", "params": [["i", "int"]], "result"}`) and `events` (its `on` handlers,
`{"name", "params"}`) as the template calls them, the header's states and the instance stripped;
and `natives`, the registered native tags its template uses as elements;
- `natives` - every `ui_native` / `ui_native_input` call whose tag is a string literal:
`{"tag", "via", "handler", "module", "file", "line", "col"}`, `via` the function called and
`handler` the `fn` value it passes last (else null). A tag worked out at run time cannot be known
and is left out.
Each list is sorted by name (then file and line), a registry's entries are in index order, and the
paths are the ones the compiler read, so two runs over the same source write the same file. The
@ -2121,7 +2134,7 @@ ludic test -j 4 # four tests at once (default: one per
ludic test packages/ludic.base # the test programs under a directory (a package's)
ludic build --check # every check a build makes, nothing written
ludic build --check --diagnostics=json # the same, every error as a JSON array on stdout
ludic schema -o build/schema.json # records, registries, entries and consts, for editors
ludic schema -o build/schema.json # records, registries, entries, consts, components and natives, for editors
ludic deps # how tangled the modules are, as the compiler resolved them
ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them)

View file

@ -22,3 +22,10 @@ A target no part of the program declares (an `@Ref` registry, an `@Tint` or list
a warning and `"unresolved": true` in the schema, so a package can name the game's registry; a name of
another kind is an error. `@OneOf` on a string field takes words, and every registry row's value is
checked against them.
The schema has a `components` list: each UI component's module, place, doc, template and stylesheet
paths, its `props` and `state` (type, default as written, place, doc), `states_read` (the states its
header names, apart from its model), `derived` fields with their types, and the `functions` and
`events` its template calls with their parameters (states and instance stripped), and the
registered native tags its template uses. A `natives` list gives every `ui_native` /
`ui_native_input` call with a literal tag: the tag, the function called, its handler and its place.

View file

@ -1,7 +1,7 @@
# Counter.ludic — a component: what it takes (props), keeps (state), works out and does
component Counter {
prop label: string
prop step: int = 1
prop step: int = 1 # how much one press adds
state count: int = 0
doubled: int = count * 2
function big() -> bool { return count > 3 }

View file

@ -10,6 +10,8 @@
# consts every const: its type and its value as written
# functions every function a `fn` value can name: fn_type is the type a field sees, the states
# stripped (the runtime supplies them), written as a field's type is ("fn(A,B)->R")
# components every UI component: props, state, derived fields, functions, events (emit_schema_ui)
# natives every native tag registered with ui_native / ui_native_input by a literal
#
# Deterministic: each list sorted by name (then file and line), an entry list in index order, the
# paths as the compiler read them, and nothing about the run. The engine's runtime is left out.
@ -36,6 +38,11 @@ function schema_buf() -> Buf {
sc_consts(b)
buf_puts(b, "\n ],\n \"functions\": [")
sc_functions(b)
sc_natives_find()
buf_puts(b, "\n ],\n \"components\": [")
sc_components(b)
buf_puts(b, "\n ],\n \"natives\": [")
sc_natives(b)
buf_puts(b, "\n ]\n}\n")
return b
}

View file

@ -0,0 +1,296 @@
# emit_schema_ui.ludic — the schema's UI side (emit_schema.ludic): every component, as a UI editor
# needs to know it, and every native tag the program registers with ludic.ui.
#
# components each `component` (component.ludic): its module, place, doc, template and stylesheet
# paths; its props and its state (both fields of the instance, a prop given by the
# parent's attribute, a state kept by the instance and set by the template's `set:`),
# each with its type, its default as written, its doc and place; `states_read`, the
# program's states its header names (supplied by the runtime, never seen by the
# template); `derived`, the fields worked out each frame; `functions` and `events`,
# what the template calls, with the header's states and the instance stripped; and
# `natives`, the registered native tags its template uses
# natives each call of ui_native / ui_native_input whose tag is a string literal: the tag, the
# function called, the handler it names (a `fn` value, else null) and the place. A tag
# worked out at run time cannot be known here and is left out.
# ---- natives ----------------------------------------------------------------------------------
var g_sn_tag: []pointer = new []pointer
var g_sn_via: []pointer = new []pointer
var g_sn_fn: []pointer = new []pointer # the handler's name, or null
var g_sn_call: []Node = new []Node
function sc_natives_find() -> void {
g_sn_tag = new []pointer
g_sn_via = new []pointer
g_sn_fn = new []pointer
g_sn_call = new []Node
var i = 0
while i < len(prog) {
sc_nt_walk(prog[i])
i += 1
}
}
function sc_nt_walk(n: Node) -> void {
if n == null { return }
if n.kind == E_CALL and n.a != null and n.a.kind == E_ID { sc_nt_call(n) }
sc_nt_walk(n.a)
sc_nt_walk(n.b)
sc_nt_walk(n.c)
var i = 0
while i < len(n.kids) {
sc_nt_walk(n.kids[i])
i += 1
}
}
# ui_native(ui_st, tag, measure, draw) / ui_native_input(ui_st, tag, f): the tag counted from the end,
# since the leading state may be written or supplied
function sc_nt_call(n: Node) -> void {
let via = vis_plain(n.a.s)
if not (via == "ui_native") and not (via == "ui_native_input") { return }
if n.file == null or n.pos < 0 or is_runtime_file(n.file) { return }
let f = find_fn(n.a.s)
if f == null { return }
var np = 0
var ti = -1
var k = 0
while k < len(f.kids) {
if f.kids[k].kind == N_PARAM {
if (f.kids[k].s == "tag") { ti = np }
np += 1
}
k += 1
}
if ti < 0 { return }
let ai = len(n.kids) - (np - ti)
if ai < 0 or ai >= len(n.kids) { return }
let t = n.kids[ai]
if t.kind != E_STR or t.s == null { return }
var j = 0
while j < len(g_sn_call) {
let o = g_sn_call[j]
if (g_sn_tag[j] == t.s) and (g_sn_via[j] == via) and (o.file == n.file) and o.a.line == n.a.line and o.a.col == n.a.col { return }
j += 1
}
var h: pointer = null
let last = n.kids[len(n.kids) - 1]
if last.kind == E_FNREF and last.s != null { h = sc_name(last.s) }
push(g_sn_tag, t.s)
push(g_sn_via, via)
push(g_sn_fn, h)
push(g_sn_call, n)
}
function sc_natives(b: Buf) -> void {
let xs = new []int
let keys = new []pointer
var i = 0
while i < len(g_sn_tag) {
let n = g_sn_call[i]
push(xs, i)
push(keys, sc_key(g_sn_tag[i], n.file, n.a.line) + "\t" + g_sn_via[i])
i += 1
}
sc_sort(xs, keys)
i = 0
while i < len(xs) {
let j = xs[i]
let n = g_sn_call[j]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"tag\": ")
jq_put(b, g_sn_tag[j])
buf_puts(b, ", \"via\": ")
jq_put(b, g_sn_via[j])
buf_puts(b, ", \"handler\": ")
sc_str(b, g_sn_fn[j])
buf_puts(b, ", \"module\": ")
sc_str(b, sc_mod(n.file))
buf_puts(b, ", ")
sc_at(b, n.file, n.a.line, n.a.col) # where the function's name is written
buf_puts(b, "}")
i += 1
}
}
# ---- components -------------------------------------------------------------------------------
function sc_components(b: Buf) -> void {
let xs = new []int
let keys = new []pointer
var c = 0
while c < len(g_cm_name) {
if not is_runtime_file(g_cm_file[c]) {
push(xs, c)
push(keys, sc_key(g_cm_name[c], g_cm_file[c], g_cm_at[c][0]))
} else {
push(keys, "")
}
c += 1
}
sc_sort(xs, keys)
var i = 0
while i < len(xs) {
sc_component(b, xs[i], i == 0)
i += 1
}
}
function sc_component(b: Buf, c: int, first: bool) -> void {
let file = g_cm_file[c]
sc_sep(b, first, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, g_cm_name[c])
buf_puts(b, ", \"module\": ")
sc_str(b, sc_mod(file))
buf_puts(b, ", \"exported\": ")
sc_bool(b, g_cm_head[c].vis == 1)
buf_puts(b, ", ")
sc_at(b, file, g_cm_at[c][0], g_cm_at[c][1])
buf_puts(b, ",\n \"xml\": ")
jq_put(b, g_cm_xml_path[c])
buf_puts(b, ", \"lss\": ")
var lss: pointer = g_cm_lss_path[c]
if len(lss) == 0 { lss = null }
sc_str(b, lss)
buf_puts(b, ",\n \"doc\": ")
sc_str(b, sc_doc(file, g_cm_at[c][2]))
buf_puts(b, ",\n \"props\": [")
sc_cm_keeps(b, c, true)
buf_puts(b, "],\n \"state\": [")
sc_cm_keeps(b, c, false)
buf_puts(b, "],\n \"states_read\": [")
var k = 0
while k < len(g_cm_ps[c]) {
if k > 0 { buf_puts(b, ", ") }
jq_put(b, sc_ty(g_cm_ps[c][k].ty))
k += 1
}
buf_puts(b, "],\n \"derived\": [")
k = 0
while k < len(g_cm_fields[c]) {
sc_sep(b, k == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, g_cm_fnames[c][k])
buf_puts(b, ", \"type\": ")
jq_put(b, sc_ty(g_cm_fields[c][k].ty))
buf_puts(b, ", \"line\": ")
buf_puti(b, g_cm_fline[c][k])
buf_puts(b, "}")
k += 1
}
if len(g_cm_fields[c]) > 0 { buf_puts(b, "\n ") }
buf_puts(b, "],\n \"functions\": [")
sc_cm_calls(b, c, false)
buf_puts(b, "],\n \"events\": [")
sc_cm_calls(b, c, true)
buf_puts(b, "],\n \"natives\": [")
sc_cm_natives(b, c)
buf_puts(b, "]}")
}
# its props (or its state), in the order written: the instance's fields
function sc_cm_keeps(b: Buf, c: int, props: bool) -> void {
let file = g_cm_file[c]
var n = 0
var i = 0
while i < len(g_cm_keep[c]) {
if g_cm_prop[c][i] == props {
let a = i * 5
sc_sep(b, n == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, g_cm_keep[c][i])
buf_puts(b, ", \"type\": ")
jq_put(b, sc_ty(g_cm_keep_ty[c][i]))
buf_puts(b, ", \"default\": ")
sc_str(b, sc_text(file, g_cm_kat[c][a + 3], g_cm_kat[c][a + 4]))
buf_puts(b, ", ")
sc_at(b, file, g_cm_kat[c][a], g_cm_kat[c][a + 1])
if props { buf_puts(b, ", \"attributes\": []") }
buf_puts(b, ", \"doc\": ")
sc_str(b, sc_doc(file, g_cm_kat[c][a + 2]))
buf_puts(b, "}")
n += 1
}
i += 1
}
if n > 0 { buf_puts(b, "\n ") }
}
# its functions (or its `on` events) as the template calls them: past the header's states and the
# instance, which the runtime supplies
function sc_cm_calls(b: Buf, c: int, events: bool) -> void {
var n = 0
var i = 0
while i < len(g_cm_calls[c]) {
if g_cm_isev[c][i] == events {
let f = g_cm_calls[c][i]
sc_sep(b, n == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, g_cm_cnames[c][i])
buf_puts(b, ", \"params\": [")
var k = 0
var np = 0
while k < len(f.kids) {
let p = f.kids[k]
if p.kind == N_PARAM and not (p.s == "self") and not sc_is_state_param(p) {
if np > 0 { buf_puts(b, ", ") }
buf_puts(b, "[")
jq_put(b, p.s)
buf_puts(b, ", ")
jq_put(b, sc_ty(p.ty))
buf_puts(b, "]")
np += 1
}
k += 1
}
buf_puts(b, "]")
if not events {
buf_puts(b, ", \"result\": ")
jq_put(b, sc_ty(f.ty))
}
buf_puts(b, ", \"line\": ")
buf_puti(b, f.line)
buf_puts(b, "}")
n += 1
}
i += 1
}
if n > 0 { buf_puts(b, "\n ") }
}
# the registered native tags its template uses as elements, sorted, each once
function sc_cm_natives(b: Buf, c: int) -> void {
let xml = g_cm_xml[c]
let tags = new []pointer
let xs = new []int
var i = 0
while i < len(g_sn_tag) {
let t = g_sn_tag[i]
var seen = false
var j = 0
while j < len(tags) {
if (tags[j] == t) { seen = true }
j += 1
}
if not seen and sc_xml_uses(xml, t) {
push(xs, len(tags))
push(tags, t)
}
i += 1
}
sc_sort(xs, tags)
i = 0
while i < len(xs) {
if i > 0 { buf_puts(b, ", ") }
jq_put(b, tags[xs[i]])
i += 1
}
}
# `<tag` followed by a space, a '/' or a '>' somewhere in the template
function sc_xml_uses(xml: pointer, tag: pointer) -> bool {
if xml == null { return false }
let n = len(xml)
let m = len(tag)
var i = 0
while i + m + 1 < n {
if xml[i] == '<' and xml[i + 1] == tag[0] and (xml[i + 1 .. i + 1 + m] == tag) {
let e = xml[i + 1 + m]
if e == ' ' or e == '/' or e == '>' or e == '\n' or e == '\t' or e == '\r' { return true }
}
i += 1
}
return false
}

View file

@ -34,11 +34,24 @@ var g_cm_cnames: [][]pointer = new [][]pointer
var g_cm_head: []Node = new []Node
var g_cm_hdr: []int = new []int # 0.S: where state parameters go (after the name)
var g_cm_ps: [][]Node = new [][]Node # the states it declares
# for the schema (emit_schema_ui.ludic): where the declaration and each prop or state are written,
# a prop's or state's default as text offsets, a field's line, which calls are events, and the files
var g_cm_at: [][]int = new [][]int # the `component` word: line, col, pos
var g_cm_kat: [][]int = new [][]int # per prop or state: line, col, pos, sp0, sp1 (5 each)
var g_cm_fline: [][]int = new [][]int
var g_cm_isev: [][]bool = new [][]bool
var g_cm_xml_path: []pointer = new []pointer
var g_cm_lss_path: []pointer = new []pointer # "" when it has no stylesheet
var g_cm_xml: []pointer = new []pointer
function cm_prefix(name: pointer) -> pointer { return `cmp_{reg_lower(name)}_` }
# component NAME { members }
function parse_ui_component() -> void {
let at = new []int
push(at, toks[pi].line)
push(at, toks[pi].col)
push(at, toks[pi].pos)
pi += 1
let name = eat_id()
push(g_cm_hdr, toks[pi - 1].end)
@ -58,6 +71,13 @@ function parse_ui_component() -> void {
push(g_cm_fnames, new []pointer)
push(g_cm_calls, new []Node)
push(g_cm_cnames, new []pointer)
push(g_cm_at, at)
push(g_cm_kat, new []int)
push(g_cm_fline, new []int)
push(g_cm_isev, new []bool)
push(g_cm_xml_path, "")
push(g_cm_lss_path, "")
push(g_cm_xml, "")
skipnl()
eat_op("{")
while true {
@ -76,6 +96,11 @@ function parse_ui_component() -> void {
function cm_member(c: int, px: pointer) -> void {
if is_id("prop") or is_id("state") {
let is_prop = is_id("prop")
push(g_cm_kat[c], toks[pi].line)
push(g_cm_kat[c], toks[pi].col)
push(g_cm_kat[c], toks[pi].pos)
var sp0 = -1
var sp1 = -1
pi += 1
let fname = eat_id()
eat_op(":")
@ -86,8 +111,13 @@ function cm_member(c: int, px: pointer) -> void {
var def: Node = null
if is_op("=") {
pi += 1
let v0 = pi
def = cm_getter(px + "def_" + fname, ty, expr())
sp0 = toks[v0].off
sp1 = toks[pi - 1].oend
}
push(g_cm_kat[c], sp0)
push(g_cm_kat[c], sp1)
if def != null { def.cm = c }
push(g_cm_defs[c], def)
return
@ -98,11 +128,13 @@ function cm_member(c: int, px: pointer) -> void {
let f = parse_fn()
f.cm = c
push(g_cm_cnames[c], f.s)
push(g_cm_isev[c], ev)
if ev { f.s = px + "on_" + f.s } else { f.s = px + f.s }
push(g_cm_calls[c], f)
push(prog, f)
return
}
push(g_cm_fline[c], toks[pi].line)
let fname = eat_id()
var ty: pointer = null
if is_op(":") {

View file

@ -14,6 +14,9 @@ function cm_class(c: int, px: pointer, head: Node) -> void {
lss = ""
lss_at = ""
} else { lss = cm_lss_inline(c, lss, dir, 0) }
g_cm_xml_path[c] = xml_path
g_cm_lss_path[c] = lss_at
g_cm_xml[c] = xml
var src = `function {px}class() -> UiClass {{\n let k = new UiClass\n k.name = "{name}"\n`
src = src + ` k.xml_path = "{xml_path}"\n k.xml = {px}xml_text()\n k.lss_path = "{lss_at}"\n k.lss = {px}lss_text()\n`
src = src + ` k.make = fn {px}new\n k.renew = fn {px}renew\n k.props = fn {px}props\n k.model = fn {px}model\n k.call = fn {px}call\n return k\n}}\n`

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -90,6 +90,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/backend/emit_fnval.ludic")
push(f, "selfhost/backend/emit_vis.ludic")
push(f, "selfhost/backend/emit_schema.ludic")
push(f, "selfhost/backend/emit_schema_ui.ludic")
push(f, "selfhost/backend/emit_schema_hash.ludic")
push(f, "selfhost/check/check_state.ludic")
push(f, "selfhost/check/check_index.ludic")

View file

@ -1240,6 +1240,12 @@ function cmd_dev_test() -> int {
schema_case("lang/attributes", "\"fn_type\": \"fn(Tool,int)->float\", \"params\": [\"Tool\", \"int\"]", "--emit-schema: a function with arguments, its fn_type spelled as a field's")
schema_case("lang/attributes", "{\"name\": \"Node\", \"args\": [\"tool\"]}", "--emit-schema: @Node and the field it reads its model from")
schema_case("lang/attributes", "{\"name\": \"Ref\", \"args\": [\"Items\"], \"unresolved\": true}", "--emit-schema: an attribute whose target the program lacks is marked unresolved, not refused")
schema_case("library/ui_component", "{\"name\": \"step\", \"type\": \"int\", \"default\": \"1\", \"file\": \"examples/library/ui_component_parts/Counter.ludic\", \"line\": 4, \"col\": 3, \"attributes\": [], \"doc\": \"how much one press adds\"}", "--emit-schema: a component's prop, with its default, place and doc")
schema_case("library/ui_component", "\"xml\": \"examples/library/ui_component_parts/Counter.xml\", \"lss\": \"examples/library/ui_component_parts/Counter.lss\"", "--emit-schema: a component's template and stylesheet")
schema_case("state/component", "\"states_read\": [\"Score\", \"Look\"]", "--emit-schema: the states a component's header names, apart from its model")
schema_case("state/component", "{\"name\": \"add\", \"params\": [[\"n\", \"int\"]], \"line\": 8}", "--emit-schema: a component's event as the template calls it, its states and instance stripped")
schema_case("library/ui_pointer", "{\"tag\": \"chartmap\", \"via\": \"ui_native_input\", \"handler\": \"map_input\", \"module\": null, \"file\": \"examples/library/ui_pointer.ludic\", \"line\": 35, \"col\": 5}", "--emit-schema: a native tag registered by a literal, and its handler")
schema_case("library/ui_pointer", "\"natives\": [\"chartmap\"]}", "--emit-schema: the native tags a component's template uses")
reject_case("rejected/registry_twice", "def Furnishings crate is declared twice", "a registry's key is declared once")
reject_case("rejected/registry_field", "Furnishing has no field colour", "a def's fields are the record's")
feat_case("modules/open_registry", "", "clock animals weather / 2 2 3", "open_registry.ludic (L8+L3: an open registry takes other modules' defs; its own entries first, then by module name)")