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 `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_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. (`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 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 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 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 # every check a build makes, nothing written
ludic build --check --diagnostics=json # the same, every error as a JSON array on stdout 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 # 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) 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 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 another kind is an error. `@OneOf` on a string field takes words, and every registry row's value is
checked against them. 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 # Counter.ludic — a component: what it takes (props), keeps (state), works out and does
component Counter { component Counter {
prop label: string prop label: string
prop step: int = 1 prop step: int = 1 # how much one press adds
state count: int = 0 state count: int = 0
doubled: int = count * 2 doubled: int = count * 2
function big() -> bool { return count > 3 } function big() -> bool { return count > 3 }

View file

@ -10,6 +10,8 @@
# consts every const: its type and its value as written # 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 # 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") # 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 # 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. # 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) sc_consts(b)
buf_puts(b, "\n ],\n \"functions\": [") buf_puts(b, "\n ],\n \"functions\": [")
sc_functions(b) 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") buf_puts(b, "\n ]\n}\n")
return b 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_head: []Node = new []Node
var g_cm_hdr: []int = new []int # 0.S: where state parameters go (after the name) 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 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)}_` } function cm_prefix(name: pointer) -> pointer { return `cmp_{reg_lower(name)}_` }
# component NAME { members } # component NAME { members }
function parse_ui_component() -> void { 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 pi += 1
let name = eat_id() let name = eat_id()
push(g_cm_hdr, toks[pi - 1].end) 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_fnames, new []pointer)
push(g_cm_calls, new []Node) push(g_cm_calls, new []Node)
push(g_cm_cnames, new []pointer) 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() skipnl()
eat_op("{") eat_op("{")
while true { while true {
@ -76,6 +96,11 @@ function parse_ui_component() -> void {
function cm_member(c: int, px: pointer) -> void { function cm_member(c: int, px: pointer) -> void {
if is_id("prop") or is_id("state") { if is_id("prop") or is_id("state") {
let is_prop = is_id("prop") 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 pi += 1
let fname = eat_id() let fname = eat_id()
eat_op(":") eat_op(":")
@ -86,8 +111,13 @@ function cm_member(c: int, px: pointer) -> void {
var def: Node = null var def: Node = null
if is_op("=") { if is_op("=") {
pi += 1 pi += 1
let v0 = pi
def = cm_getter(px + "def_" + fname, ty, expr()) 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 } if def != null { def.cm = c }
push(g_cm_defs[c], def) push(g_cm_defs[c], def)
return return
@ -98,11 +128,13 @@ function cm_member(c: int, px: pointer) -> void {
let f = parse_fn() let f = parse_fn()
f.cm = c f.cm = c
push(g_cm_cnames[c], f.s) 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 } if ev { f.s = px + "on_" + f.s } else { f.s = px + f.s }
push(g_cm_calls[c], f) push(g_cm_calls[c], f)
push(prog, f) push(prog, f)
return return
} }
push(g_cm_fline[c], toks[pi].line)
let fname = eat_id() let fname = eat_id()
var ty: pointer = null var ty: pointer = null
if is_op(":") { if is_op(":") {

View file

@ -14,6 +14,9 @@ function cm_class(c: int, px: pointer, head: Node) -> void {
lss = "" lss = ""
lss_at = "" lss_at = ""
} else { lss = cm_lss_inline(c, lss, dir, 0) } } 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` 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.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` 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_fnval.ludic")
push(f, "selfhost/backend/emit_vis.ludic") push(f, "selfhost/backend/emit_vis.ludic")
push(f, "selfhost/backend/emit_schema.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/backend/emit_schema_hash.ludic")
push(f, "selfhost/check/check_state.ludic") push(f, "selfhost/check/check_state.ludic")
push(f, "selfhost/check/check_index.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", "\"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\": \"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("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_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") 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)") 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)")