compiler: ludic schema gains a "code" section (R7) - the code map from the compiler, so the studio's scan is only a fallback: modules (package, layer, uses and where, friend, files), states, actions, reducers, every dispatch, events (cancellable, net) with every emit and their @On listeners, ports (members with fn types, defaults, required) and binds (port, member, fn, value, at), handlers (phase, hook and target, @Public, @Server / @Predicted, @Queries with filters as written, scene and layer), models (@Owned, @Sync), prefabs, scenes (start, public, shows, lasts / then, loads, enter / exit, layers) and fn_refs - every fn name in code or a resource file with the slot it fills (registry field and row, port member or default, record field, emit field, call argument, assignment, let). Places are "file:line:col"; sorted by name, sites by place; schema_version stays 1 (additive). The parser records emits, fn refs, handlers and scenes' layers / lasts / loads for it (emit_schema_code.ludic, emit_schema_code_game.ludic); LANGUAGE.md and the changeset say so; schema_case lines added (not run); reseeded, bootstrap-cfree fixpoint holds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 00:19:24 +03:00
parent 4ccbc12b48
commit bfd99cdda1
12 changed files with 112243 additions and 92761 deletions

View file

@ -801,6 +801,43 @@ object with `"schema_version": 1`:
and is left out. and is left out.
- `units` - `@Unit`'s canonical spellings. - `units` - `@Unit`'s canonical spellings.
- `lang` - the text keys and the languages (see Text keys), or null without a `lang` line. - `lang` - the text keys and the languages (see Text keys), or null without a `lang` line.
- `code` - the program's code map, so an editor needs no scan of its own. Every place is
`"file:line:col"` (`"at"`); what the runtime declares and what the compiler writes itself are left
out. It holds:
- `modules` - `{"name", "package", "layer", "uses", "uses_at", "friend", "files"}`: `uses` null
without a `uses` line, `friend` null, or `{"of": null}` for a friend of every module and
`{"of": [...]}` for `friend module lab of a, b`; `files` its source files and the resource files
read into its registries;
- `states`, `actions` (`{"name", "module", "at"}`), `reducers` (`{"state", "action", "module",
"at"}`) and `dispatch` (every `dispatch`: `{"action", "module", "at"}`);
- `events` - `{"name", "module", "at", "cancellable", "net"}` (`net` `"toserver"` for
`@ToServer`, `"toclients"` for `@ToClients`, else null), its `emits` (every `emit`'s place) and
its `listeners` (the `@On` handlers: `{"handler", "module", "at"}`);
- `ports` - `{"name", "module", "at", "bound", "members"}`, each member `{"name", "type", "default",
"required"}` (its `fn` type as a field's is spelled, its default as written); and `binds` - one
row per member a `bind` gives: `{"port", "member", "fn", "value", "module", "at"}`, `fn` the
function a `fn name` names (null for a variable bound to a member that takes nothing) and
`value` as written;
- `handlers` - `{"name", "module", "at", "phase", "hook", "target", "public", "net", "queries",
"scene", "layer"}`: `phase` null for a hook, `hook` null or `"On"`, `"OnSpawn"`, `"OnDespawn"`,
`"OnAttach"`, `"OnDetach"`, `"OnEnable"`, `"OnDisable"`, `"OnStart"`, `"OnQuit"` with `target`
the event, model or property it names; `net` `"server"` (`@Server`), `"predicted"`
(`@Predicted`) or null; `queries` null or `{"these": [{"property", "filter"}], "on"}` (a
filter as written, or null); a scene's handler has its name as written and its `scene` and
`layer`;
- `models` (`{"name", "module", "at", "owned", "properties": [{"name", "sync"}]}`), `prefabs`
(`{"name", "model", "module", "at", "components": [{"property", "fields": [{"name",
"value"}]}]}`) and `scenes` (`{"name", "module", "at", "start", "public", "shows", "lasts",
"then", "loads", "enter", "exit", "layers"}`: `lasts` as written, `then` the scene it goes on to,
`loads` the scene a `loads then` one goes on to, `enter` / `exit` whether it has the block);
- `fn_refs` - every `fn name` written, in code or in a resource file: `{"fn", "module", "at",
"slot_kind", "slot", "entry"}`, the slot it fills: `"registry"` (`"Registry.field"`, `entry` the
row's key), `"port"` (a bind's `"Port.member"`), `"port_default"`, `"default"` (a record
field's default, `"Type.field"`), `"record"` (a `new` or a spawn's `"Type.field"`), `"event"`
(an `emit`'s `"Event.field"`), `"arg"` (`"callee(i)"`, the i-th argument written), `"assign"`
(`s.tick = fn f`: `"s.tick"`), `"let"` (the name), or `"value"` with a null slot.
Named lists are sorted by name (then file and line), sites by place.
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
@ -2323,7 +2360,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, consts, components and natives, for editors ludic schema -o build/schema.json # records, registries, entries, consts, components, natives, lang and the code map, 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)

12
changes/schema-code.md Normal file
View file

@ -0,0 +1,12 @@
bump: minor
type: feature
**`ludic schema` carries the code map: a `"code"` section.** Beside the records, registries and
`lang`, the schema now lists what an editor's code map needs, from the compiler's own reading:
`modules` (uses, layer, friend, files), `states`, `actions`, `reducers` and every `dispatch`, `events`
(cancellable, net) with every `emit` and their `@On` listeners, `ports` (members with their `fn`
types and defaults) and `binds` (port, member, the function named, where), `handlers` (phase, hook
and what it names, `@Public`, `@Server` / `@Predicted`, `@Queries` with their filters, the scene and
layer), `models` (`@Owned`, `@Sync`), `prefabs`, `scenes` (start, shows, lasts / loads and what
follows, layers) and `fn_refs` - every `fn name` in code or in a resource file, with the slot it
fills (a registry's field and row, a port's member, a record field, an argument, ...). Every place
is `"file:line:col"`; the rest of the schema is unchanged, so `schema_version` stays 1.

View file

@ -12,6 +12,9 @@
# 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) # 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 # natives every native tag registered with ui_native / ui_native_input by a literal
# units, lang @Unit's spellings; the text keys and the languages (emit_schema_lang)
# code the code map: modules, states, actions, reducers, dispatch, events, ports, binds,
# handlers, models, prefabs, scenes and fn_refs (emit_schema_code)
# #
# 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.
@ -47,6 +50,8 @@ function schema_buf() -> Buf {
sc_units(b) sc_units(b)
buf_puts(b, "],\n \"lang\": ") buf_puts(b, "],\n \"lang\": ")
sc_lang(b) # emit_schema_lang.ludic: the keys and the languages sc_lang(b) # emit_schema_lang.ludic: the keys and the languages
buf_puts(b, ",\n \"code\": ")
sc_code(b) # emit_schema_code.ludic: the code map
buf_puts(b, "\n}\n") buf_puts(b, "\n}\n")
return b return b
} }

View file

@ -0,0 +1,459 @@
# emit_schema_code.ludic — the schema's "code" (emit_schema.ludic): the program's code map as the
# compiler read it, so an editor needs no scan of its own. Every place is "file:line:col".
#
# modules name, package, layer, uses (null without a `uses` line) and where, friend, files
# states name, module, at actions name, module, at
# reducers state, action, module, at dispatch action, module, at
# events name, module, at, cancellable, net (@ToServer / @ToClients), emits (sites) and
# listeners (the @On handlers: handler, module, at)
# ports name, module, at, bound, members (name, type, default as written, required)
# binds port, member, fn (the function a `fn name` names, else null), value as written, at
# handlers (emit_schema_code_game.ludic) name, module, at, phase, hook, target, public, net,
# queries, scene, layer
# models, prefabs, scenes, fn_refs (emit_schema_code_game.ludic)
#
# Each list is sorted by name (then file and line) or, for sites, by place. What the runtime
# declares, and what the compiler writes itself, is left out.
# ---- what the parser records for it ------------------------------------------------------------
var g_cx_emits: []Node = new []Node # every `emit E(...)` read
var g_cx_fnrefs: []Node = new []Node # every `fn name` read, in code or data
var g_cx_h: []Node = new []Node # every handler written: its node (name, phase, net role)
var g_cx_hname: []pointer = new []pointer # its name as written (a scene's are qualified later)
var g_cx_hhook: []pointer = new []pointer # null, "On", "OnSpawn", "OnDespawn", ..., "OnStart", "OnQuit"
var g_cx_htarget: []pointer = new []pointer # the event, model or property the hook names
var g_cx_hq: []Node = new []Node # its @Queries (an S_QUERY), or null
var g_cx_hpub: []bool = new []bool # @Public
var g_cx_hscene: []pointer = new []pointer # the scene and layer it is written in, or null
var g_cx_hlayer: []pointer = new []pointer
var g_cx_hlisten: []Node = new []Node # an @On handler's listener (g_onlisten), whose event is resolved
var g_cx_slayers: []pointer = new []pointer # per g_scenes entry: ",a,b," its layers
var g_cx_sthen: []pointer = new []pointer # `lasts N then T`'s T, or null (N is the scene's sp0..sp1)
var g_cx_sloads: []pointer = new []pointer # `loads then T`'s T, or null
var g_cx_spub: []bool = new []bool
function cx_emit(n: Node) -> void { push(g_cx_emits, n) }
function cx_fnref(n: Node) -> void { push(g_cx_fnrefs, n) }
function cx_handler(h: Node, name: pointer, hook: pointer, target: pointer, q: Node, pub: bool, scene: pointer, layer: pointer) -> void {
push(g_cx_h, h)
push(g_cx_hname, name)
push(g_cx_hhook, hook)
push(g_cx_htarget, target)
push(g_cx_hq, q)
push(g_cx_hpub, pub)
push(g_cx_hscene, scene)
push(g_cx_hlayer, layer)
push(g_cx_hlisten, null)
}
# the handler just recorded is an @On listener, the one just registered
function cx_listened() -> void {
let k = len(g_cx_hlisten) - 1
if k >= 0 and len(g_onlisten) > 0 { g_cx_hlisten[k] = g_onlisten[len(g_onlisten) - 1] }
}
function cx_scene(layers: pointer, then_scene: pointer, loads: pointer, pub: bool) -> void {
push(g_cx_slayers, layers)
push(g_cx_sthen, then_scene)
push(g_cx_sloads, loads)
push(g_cx_spub, pub)
}
# ---- the section ----------------------------------------------------------------------------------
function sc_code(b: Buf) -> void {
buf_puts(b, "{\n \"modules\": [")
cx_modules(b)
buf_puts(b, "\n ],\n \"states\": [")
cx_decls(b, 2)
buf_puts(b, "\n ],\n \"actions\": [")
cx_decls(b, 3)
buf_puts(b, "\n ],\n \"reducers\": [")
cx_reducers(b)
buf_puts(b, "\n ],\n \"dispatch\": [")
cx_dispatch(b)
buf_puts(b, "\n ],\n \"events\": [")
cx_events(b)
buf_puts(b, "\n ],\n \"ports\": [")
cx_ports(b)
buf_puts(b, "\n ],\n \"binds\": [")
cx_binds(b)
buf_puts(b, "\n ],\n \"handlers\": [")
cx_handlers(b)
buf_puts(b, "\n ],\n \"models\": [")
cx_models(b)
buf_puts(b, "\n ],\n \"prefabs\": [")
cx_prefabs(b)
buf_puts(b, "\n ],\n \"scenes\": [")
cx_scenes(b)
buf_puts(b, "\n ],\n \"fn_refs\": [")
cx_fn_refs(b)
buf_puts(b, "\n ]\n }")
}
# ---- places ---------------------------------------------------------------------------------------
function cx_at_text(n: Node) -> pointer {
return `{n.file}:{itoa(n.line)}:{itoa(n.col)}`
}
function cx_at(b: Buf, n: Node) -> void {
buf_puts(b, "\"at\": ")
if n == null or n.file == null { buf_puts(b, "null") } else { jq_put(b, cx_at_text(n)) }
}
# a key that sorts by place
function cx_at_key(n: Node) -> pointer {
var f = n.file
if f == null { f = "" }
return `{f}\t{itoa(1000000 + n.line)}\t{itoa(1000000 + n.col)}`
}
# written by the program, in a file it read (not the runtime's, not the compiler's own)
function cx_written(n: Node) -> bool {
return n != null and n.file != null and n.pos >= 0 and not is_runtime_file(n.file)
}
# or read from a resource file, whose tokens carry no offset (lex_at)
function cx_written_or_data(n: Node) -> bool {
if cx_written(n) { return true }
if n == null or n.file == null { return false }
let k = len(n.file)
return k > 5 and (n.file[k - 5 .. k] == ".lres")
}
function cx_module(b: Buf, file: pointer) -> void {
buf_puts(b, "\"module\": ")
sc_str(b, sc_mod(file))
}
# ",a,b," as a JSON array, each name once
function cx_list(b: Buf, list: pointer) -> void {
buf_puts(b, "[")
let seen = ss_new(64)
var n = 0
var i = 1
while i < len(list) {
var j = i
while j < len(list) and list[j] != ',' { j += 1 }
if j > i {
let name = list[i .. j]
if ss_add(seen, name) {
if n > 0 { buf_puts(b, ", ") }
jq_put(b, name)
n += 1
}
}
i = j + 1
}
buf_puts(b, "]")
}
# the indices 0..n-1 sorted by keys
function cx_order(keys: []pointer) -> []int {
let xs = new []int
var i = 0
while i < len(keys) {
if keys[i] != null { push(xs, i) }
i += 1
}
sc_sort(xs, keys)
return xs
}
# ---- modules --------------------------------------------------------------------------------------
function cx_mod_names(names: []pointer, seen: StrSet, from: []pointer) -> void {
var i = 0
while i < len(from) {
if from[i] != null and len(from[i]) > 0 and ss_add(seen, from[i]) { push(names, from[i]) }
i += 1
}
}
function cx_modules(b: Buf) -> void {
let names = new []pointer
let seen = ss_new(256)
cx_mod_names(names, seen, g_mod_name)
cx_mod_names(names, seen, g_mu_mod)
cx_mod_names(names, seen, g_mod_friends)
let xs = cx_order(names)
var a = 0
while a < len(xs) {
let m = names[xs[a]]
sc_sep(b, a == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, m)
let files = new []pointer
var i = 0
while i < len(g_mod_file) {
if (g_mod_name[i] == m) and not is_runtime_file(g_mod_file[i]) { push(files, g_mod_file[i]) }
i += 1
}
var pkg: pointer = null
if len(files) > 0 and len(pkg_of_file(files[0])) > 0 { pkg = pkg_of_file(files[0]) }
buf_puts(b, ", \"package\": ")
sc_str(b, pkg)
let ly = mod_layer(m)
buf_puts(b, ", \"layer\": ")
if len(ly) == 0 { buf_puts(b, "null") } else { jq_put(b, ly) }
let u = mod_find_uses(m)
buf_puts(b, ", \"uses\": ")
if u < 0 { buf_puts(b, "null") } else { cx_list(b, g_mu_list[u]) }
buf_puts(b, ", \"uses_at\": ")
if u < 0 { buf_puts(b, "null") } else { jq_put(b, `{g_mu_file[u]}:{itoa(g_mu_line[u])}`) }
buf_puts(b, ", \"friend\": ")
cx_friend(b, m)
buf_puts(b, ", \"files\": [")
let fx = cx_order(files)
i = 0
while i < len(fx) {
if i > 0 { buf_puts(b, ", ") }
jq_put(b, files[fx[i]])
i += 1
}
buf_puts(b, "]}")
a += 1
}
}
# null, or {"of": null} for a friend of every module, else {"of": [the modules]}
function cx_friend(b: Buf, m: pointer) -> void {
var all = false
var list: pointer = null
var i = 0
while i < len(g_mod_friends) {
if (g_mod_friends[i] == m) {
if len(g_fr_scope[i]) == 0 { all = true } else if list == null { list = g_fr_scope[i] } else { list = list + g_fr_scope[i] }
}
i += 1
}
if not all and list == null {
buf_puts(b, "null")
return
}
buf_puts(b, "{\"of\": ")
if all { buf_puts(b, "null") } else { cx_list(b, list) }
buf_puts(b, "}")
}
# ---- states, actions, reducers, dispatch ----------------------------------------------------------
# the records of one kind (2 a state, 3 an action) in prog
function cx_decls(b: Buf, uns: int) -> void {
let keys = new []pointer
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_COMP and d.uns == uns and d.s != null and cx_written(d) { push(keys, sc_key(sc_name(d.s), d.file, d.line)) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let d = prog[xs[i]]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, sc_name(d.s))
buf_puts(b, ", ")
cx_module(b, d.file)
buf_puts(b, ", ")
cx_at(b, d)
buf_puts(b, "}")
i += 1
}
}
function cx_reducers(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(g_red_nodes) {
let f = g_red_nodes[i]
if cx_written(f) { push(keys, `{g_red_state[i]}\t{g_red_action[i]}\t{cx_at_key(f)}`) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let k = xs[i]
let f = g_red_nodes[k]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"state\": ")
jq_put(b, g_red_state[k])
buf_puts(b, ", \"action\": ")
jq_put(b, g_red_action[k])
buf_puts(b, ", ")
cx_module(b, f.file)
buf_puts(b, ", ")
cx_at(b, f)
buf_puts(b, "}")
i += 1
}
}
function cx_dispatch(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(g_dsp_at) {
let n = g_dsp_at[i]
if cx_written(n) { push(keys, cx_at_key(n)) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let n = g_dsp_at[xs[i]]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"action\": ")
jq_put(b, sc_name(g_dsp_names[xs[i]]))
buf_puts(b, ", ")
cx_module(b, n.file)
buf_puts(b, ", ")
cx_at(b, n)
buf_puts(b, "}")
i += 1
}
}
# ---- events ---------------------------------------------------------------------------------------
function cx_events(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(g_events) {
let e = g_events[i]
if e.s != null and cx_written(e) { push(keys, sc_key(sc_name(e.s), e.file, e.line)) } else { push(keys, null) }
i += 1
}
let ekeys = new []pointer # the emits, by place
i = 0
while i < len(g_cx_emits) {
if cx_written(g_cx_emits[i]) { push(ekeys, cx_at_key(g_cx_emits[i])) } else { push(ekeys, null) }
i += 1
}
let ex = cx_order(ekeys)
let hkeys = new []pointer # the listeners, by place
i = 0
while i < len(g_cx_h) {
if g_cx_hlisten[i] != null and cx_written(g_cx_h[i]) { push(hkeys, cx_at_key(g_cx_h[i])) } else { push(hkeys, null) }
i += 1
}
let hx = cx_order(hkeys)
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let e = g_events[xs[i]]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, sc_name(e.s))
buf_puts(b, ", ")
cx_module(b, e.file)
buf_puts(b, ", ")
cx_at(b, e)
buf_puts(b, ", \"cancellable\": ")
sc_bool(b, e.ival == 1)
buf_puts(b, ", \"net\": ")
sc_str(b, e.ty)
buf_puts(b, ", \"emits\": [")
var n = 0
var k = 0
while k < len(ex) {
let s = g_cx_emits[ex[k]]
if (s.s == e.s) {
if n > 0 { buf_puts(b, ", ") }
jq_put(b, cx_at_text(s))
n += 1
}
k += 1
}
buf_puts(b, "], \"listeners\": [")
n = 0
k = 0
while k < len(hx) {
let h = hx[k]
if (g_cx_hlisten[h].s == e.s) {
if n > 0 { buf_puts(b, ", ") }
buf_puts(b, "{\"handler\": ")
jq_put(b, g_cx_hname[h])
buf_puts(b, ", ")
cx_module(b, g_cx_h[h].file)
buf_puts(b, ", ")
cx_at(b, g_cx_h[h])
buf_puts(b, "}")
n += 1
}
k += 1
}
buf_puts(b, "]}")
i += 1
}
}
# ---- ports and binds ------------------------------------------------------------------------------
function cx_ports(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(g_pt_name) {
let c = g_pt_comp[i]
if cx_written(c) { push(keys, sc_key(g_pt_name[i], c.file, c.line)) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let k = xs[i]
let c = g_pt_comp[k]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, g_pt_name[k])
buf_puts(b, ", ")
cx_module(b, c.file)
buf_puts(b, ", ")
cx_at(b, c)
buf_puts(b, ", \"bound\": ")
sc_bool(b, g_pt_bind[k] != null)
buf_puts(b, ", \"members\": [")
var f = 0
while f < len(c.kids) {
let fi = c.kids[f]
if f > 0 { buf_puts(b, ", ") }
buf_puts(b, "{\"name\": ")
jq_put(b, fi.s)
buf_puts(b, ", \"type\": ")
jq_put(b, sc_ty(fi.ty))
buf_puts(b, ", \"default\": ")
sc_str(b, sc_text(fi.file, fi.sp0, fi.sp1))
buf_puts(b, ", \"required\": ")
sc_bool(b, fi.a == null)
buf_puts(b, "}")
f += 1
}
buf_puts(b, "]}")
i += 1
}
}
# one row per member a bind gives
function cx_binds(b: Buf) -> void {
let keys = new []pointer
let rows = new []Node
let ports = new []pointer
var i = 0
while i < len(g_bd) {
let bd = g_bd[i]
if cx_written(bd) and bd.a != null {
var k = 0
while k < len(bd.a.kids) {
let fi = bd.a.kids[k]
if fi.kind == E_FINIT {
push(keys, cx_at_key(fi))
push(rows, fi)
push(ports, bd.s)
}
k += 1
}
}
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let fi = rows[xs[i]]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"port\": ")
jq_put(b, ports[xs[i]])
buf_puts(b, ", \"member\": ")
jq_put(b, fi.s)
buf_puts(b, ", \"fn\": ")
if fi.a != null and fi.a.kind == E_FNREF and fi.a.pos >= 0 { jq_put(b, sc_name(fi.a.s)) } else { buf_puts(b, "null") }
buf_puts(b, ", \"value\": ")
sc_str(b, sc_text(fi.file, fi.sp0, fi.sp1))
buf_puts(b, ", ")
cx_module(b, fi.file)
buf_puts(b, ", ")
cx_at(b, fi)
buf_puts(b, "}")
i += 1
}
}

View file

@ -0,0 +1,415 @@
# emit_schema_code_game.ludic — the "code" section's handlers, models, prefabs, scenes and fn_refs
# (emit_schema_code.ludic says the shape of the rest).
#
# handlers name (as written: a scene's without its scene), module, at, phase (null for a hook),
# hook (null, "On", "OnSpawn", "OnDespawn", "OnAttach", "OnDetach", "OnEnable",
# "OnDisable", "OnStart", "OnQuit"), target (the hook's event, model or property),
# public (@Public), net ("server" for @Server, "predicted" for @Predicted, else null),
# queries (null, or {"these": [{"property", "filter"}], "on": model or null}), scene, layer
# models name, module, at, owned (@Owned), properties [{"name", "sync"}]
# prefabs name, model, module, at, components [{"property", "fields": [{"name", "value"}]}]
# scenes name, module, at, start, public, shows, lasts (as written) and then, loads (the scene it
# goes on to), enter, exit (has one), layers
# fn_refs every `fn name` written, in code or data: fn, module, at, and the slot it fills -
# slot_kind "registry" (slot "Registry.field", entry its key), "port" (a bind's
# "Port.member"), "port_default", "default" (a record field's), "record" (a `new` or a
# spawn's "Type.field"), "event" (an emit's "Event.field"), "arg" ("callee(i)", the i-th
# argument written), "assign" (`s.tick = fn f`: "s.tick"), "let" (the name) or "value"
# (anywhere else, slot null)
# ---- handlers -------------------------------------------------------------------------------------
function cx_handlers(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(g_cx_h) {
let h = g_cx_h[i]
if cx_written(h) { push(keys, sc_key(g_cx_hname[i], h.file, h.line)) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let k = xs[i]
let h = g_cx_h[k]
let hook = g_cx_hhook[k]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, g_cx_hname[k])
buf_puts(b, ", ")
cx_module(b, h.file)
buf_puts(b, ", ")
cx_at(b, h)
buf_puts(b, ", \"phase\": ")
if hook == null or (hook == "OnStart") or (hook == "OnQuit") { sc_str(b, h.ty) } else { buf_puts(b, "null") }
buf_puts(b, ", \"hook\": ")
sc_str(b, hook)
buf_puts(b, ", \"target\": ")
sc_str(b, g_cx_htarget[k])
buf_puts(b, ", \"public\": ")
sc_bool(b, g_cx_hpub[k])
buf_puts(b, ", \"net\": ")
if h.ival == 1 { jq_put(b, "server") } else if h.ival == 2 { jq_put(b, "predicted") } else { buf_puts(b, "null") }
buf_puts(b, ", \"queries\": ")
cx_queries(b, g_cx_hq[k])
buf_puts(b, ", \"scene\": ")
sc_str(b, g_cx_hscene[k])
buf_puts(b, ", \"layer\": ")
sc_str(b, g_cx_hlayer[k])
buf_puts(b, "}")
i += 1
}
}
# @Queries(these: [P {filter}, ...], on: M): its terms, a filter as written (parse_queries_anno)
function cx_queries(b: Buf, q: Node) -> void {
if q == null or q.c == null {
buf_puts(b, "null")
return
}
buf_puts(b, "{\"these\": [")
var on: pointer = null
var n = 0
var i = 0
while i < len(q.c.kids) {
let t = q.c.kids[i]
if t.ival == 1 {
on = t.s
} else {
if n > 0 { buf_puts(b, ", ") }
buf_puts(b, "{\"property\": ")
jq_put(b, t.s)
buf_puts(b, ", \"filter\": ")
sc_str(b, sc_text(t.file, t.sp0, t.sp1))
buf_puts(b, "}")
n += 1
}
i += 1
}
buf_puts(b, "], \"on\": ")
sc_str(b, on)
buf_puts(b, "}")
}
# ---- models and prefabs ---------------------------------------------------------------------------
function cx_models(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_ARCH and d.s != null and cx_written(d) { push(keys, sc_key(sc_name(d.s), d.file, d.line)) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let d = prog[xs[i]]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, sc_name(d.s))
buf_puts(b, ", ")
cx_module(b, d.file)
buf_puts(b, ", ")
cx_at(b, d)
buf_puts(b, ", \"owned\": ")
sc_bool(b, d.ival == 1)
buf_puts(b, ", \"properties\": [")
var k = 0
while k < len(d.kids) {
if k > 0 { buf_puts(b, ", ") }
buf_puts(b, "{\"name\": ")
jq_put(b, sc_name(d.kids[k].s))
buf_puts(b, ", \"sync\": ")
sc_bool(b, d.kids[k].ival == 1)
buf_puts(b, "}")
k += 1
}
buf_puts(b, "]}")
i += 1
}
}
function cx_prefabs(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_PREFAB and d.s != null and cx_written(d) { push(keys, sc_key(sc_name(d.s), d.file, d.line)) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let d = prog[xs[i]]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, sc_name(d.s))
buf_puts(b, ", \"model\": ")
sc_str(b, sc_name(d.ty))
buf_puts(b, ", ")
cx_module(b, d.file)
buf_puts(b, ", ")
cx_at(b, d)
buf_puts(b, ", \"components\": [")
var k = 0
while k < len(d.kids) {
let ci = d.kids[k]
if k > 0 { buf_puts(b, ", ") }
buf_puts(b, "{\"property\": ")
jq_put(b, sc_name(ci.s))
buf_puts(b, ", \"fields\": [")
var f = 0
while ci.a != null and f < len(ci.a.kids) {
let fi = ci.a.kids[f]
if f > 0 { buf_puts(b, ", ") }
buf_puts(b, "{\"name\": ")
jq_put(b, fi.s)
buf_puts(b, ", \"value\": ")
sc_str(b, sc_text(fi.file, fi.sp0, fi.sp1))
buf_puts(b, "}")
f += 1
}
buf_puts(b, "]}")
k += 1
}
buf_puts(b, "]}")
i += 1
}
}
# ---- scenes ---------------------------------------------------------------------------------------
function cx_scenes(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(g_scenes) {
let n = g_scenes[i]
if cx_written(n) and i < len(g_cx_slayers) { push(keys, sc_key(sc_name(n.s), n.file, n.line)) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let k = xs[i]
let n = g_scenes[k]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, sc_name(n.s))
buf_puts(b, ", ")
cx_module(b, n.file)
buf_puts(b, ", ")
cx_at(b, n)
buf_puts(b, ", \"start\": ")
sc_bool(b, n.ival == g_start_scene)
buf_puts(b, ", \"public\": ")
sc_bool(b, g_cx_spub[k])
buf_puts(b, ", \"shows\": ")
sc_str(b, n.ty)
buf_puts(b, ", \"lasts\": ")
sc_str(b, sc_text(n.file, n.sp0, n.sp1))
buf_puts(b, ", \"then\": ")
sc_str(b, g_cx_sthen[k])
buf_puts(b, ", \"loads\": ")
sc_str(b, g_cx_sloads[k])
buf_puts(b, ", \"enter\": ")
sc_bool(b, n.a != null)
buf_puts(b, ", \"exit\": ")
sc_bool(b, n.b != null)
buf_puts(b, ", \"layers\": ")
cx_list(b, g_cx_slayers[k])
buf_puts(b, "}")
i += 1
}
}
# ---- fn refs --------------------------------------------------------------------------------------
# the walk's findings: each `fn name` node met, with the slot it was met in (the first meeting wins)
var g_cx_wn: []Node = new []Node
var g_cx_wkind: []pointer = new []pointer
var g_cx_wslot: []pointer = new []pointer
var g_cx_wentry: []pointer = new []pointer
function cx_fn_refs(b: Buf) -> void {
cx_walk_all()
let keys = new []pointer
var i = 0
while i < len(g_cx_fnrefs) {
let n = g_cx_fnrefs[i]
if cx_written_or_data(n) and n.s != null { push(keys, `{cx_at_key(n)}\t{n.s}`) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
var first = true
var last: pointer = null
while i < len(xs) {
let n = g_cx_fnrefs[xs[i]]
let key = keys[xs[i]]
if last == null or not (key == last) { # a node read twice is one site
last = key
var w = 0
while w < len(g_cx_wn) and g_cx_wn[w] != n { w += 1 }
sc_sep(b, first, " ")
first = false
buf_puts(b, "{\"fn\": ")
jq_put(b, sc_name(n.s))
buf_puts(b, ", ")
cx_module(b, n.file)
buf_puts(b, ", ")
cx_at(b, n)
buf_puts(b, ", \"slot_kind\": ")
if w < len(g_cx_wn) { jq_put(b, g_cx_wkind[w]) } else { jq_put(b, "value") }
buf_puts(b, ", \"slot\": ")
if w < len(g_cx_wn) { sc_str(b, g_cx_wslot[w]) } else { buf_puts(b, "null") }
buf_puts(b, ", \"entry\": ")
if w < len(g_cx_wn) { sc_str(b, g_cx_wentry[w]) } else { buf_puts(b, "null") }
buf_puts(b, "}")
}
i += 1
}
}
# every place a `fn name` can be written, the narrowest slots first: a registry's rows, the binds, the
# ports' defaults, then the program's declarations and the bodies kept outside them
function cx_walk_all() -> void {
var i = 0
while i < len(g_df_rec) {
cx_walk_rec(g_df_rec[i], "registry", g_df_reg[i], g_df_key[i], 0)
i += 1
}
i = 0
while i < len(g_bd) {
if g_bd[i].a != null { cx_walk_rec(g_bd[i].a, "port", g_bd[i].s, null, 0) }
i += 1
}
i = 0
while i < len(g_pt_comp) {
let c = g_pt_comp[i]
var f = 0
while f < len(c.kids) {
cx_walk(c.kids[f].a, "port_default", `{g_pt_name[i]}.{c.kids[f].s}`, null, 0)
f += 1
}
i += 1
}
i = 0
while i < len(prog) {
if prog[i].file == null or not is_runtime_file(prog[i].file) { cx_walk(prog[i], "value", null, null, 0) }
i += 1
}
cx_walk_list(g_tests)
cx_walk_list(g_onlisten)
cx_walk_list(g_onspawn)
cx_walk_list(g_ondespawn)
cx_walk_list(g_onattach)
cx_walk_list(g_ondetach)
cx_walk_list(g_onenable)
cx_walk_list(g_ondisable)
cx_walk_list(g_scenes)
}
function cx_walk_list(xs: []Node) -> void {
var i = 0
while i < len(xs) {
cx_walk(xs[i], "value", null, null, 0)
i += 1
}
}
# a record literal's values, each in the slot owner.field
function cx_walk_rec(rec: Node, kind: pointer, owner: pointer, entry: pointer, depth: int) -> void {
if rec == null or depth > 400 { return }
var i = 0
while i < len(rec.kids) {
let fi = rec.kids[i]
if fi.kind == E_FINIT { cx_walk(fi.a, kind, `{owner}.{fi.s}`, entry, depth + 1) } else { cx_walk(fi, "value", null, null, depth + 1) }
i += 1
}
}
function cx_callee(c: Node) -> pointer {
if c == null { return null }
if c.kind == E_ID and c.s != null { return sc_name(c.s) }
if c.kind == E_MEMBER and c.a != null and c.a.kind == E_ID and c.a.s != null and c.s != null { return `{c.a.s}.{c.s}` }
return null
}
function cx_walk(n: Node, kind: pointer, slot: pointer, entry: pointer, depth: int) -> void {
if n == null or depth > 400 { return }
let k = n.kind
let d = depth + 1
if k == E_FNREF {
push(g_cx_wn, n)
push(g_cx_wkind, kind)
push(g_cx_wslot, slot)
push(g_cx_wentry, entry)
return
}
if k == E_NEW and n.a != null and n.a.kind == E_REC and n.s != null {
cx_walk_rec(n.a, "record", sc_name(n.s), null, d)
cx_walk(n.b, "value", null, null, d)
return
}
if k == S_EMIT and n.a != null and n.s != null {
cx_walk_rec(n.a, "event", sc_name(n.s), null, d)
return
}
if (k == S_SPAWN or k == N_PREFAB) {
var i = 0
while i < len(n.kids) {
let ci = n.kids[i]
if ci.kind == E_FINIT and ci.a != null and ci.a.kind == E_REC { cx_walk_rec(ci.a, "record", sc_name(ci.s), null, d) } else { cx_walk(ci, "value", null, null, d) }
i += 1
}
return
}
if k == E_CALL {
let callee = cx_callee(n.a)
cx_walk(n.a, "value", null, null, d)
var i = 0
var arg = 0
while i < len(n.kids) {
let a = n.kids[i]
if a != null and a.pos >= 0 {
if callee != null { cx_walk(a, "arg", `{callee}({itoa(arg)})`, null, d) } else { cx_walk(a, "value", null, null, d) }
arg += 1
} else {
cx_walk(a, "value", null, null, d)
}
i += 1
}
return
}
if k == N_COMP {
var i = 0
while i < len(n.kids) {
if n.s != null and n.kids[i].s != null { cx_walk(n.kids[i].a, "default", `{sc_name(n.s)}.{n.kids[i].s}`, null, d) }
i += 1
}
return
}
if k == S_ASSIGN and n.b != null and n.b.kind == E_FNREF { # `s.tick = fn f`: the slot as written
cx_walk(n.b, "assign", cx_callee(n.a), null, d)
return
}
if k == S_LET and n.a != null and n.a.kind == E_FNREF and n.s != null {
cx_walk(n.a, "let", n.s, null, d)
return
}
if k == E_LIST { # a list of fn values fills the slot it is in
var i = 0
while i < len(n.kids) {
cx_walk(n.kids[i], kind, slot, entry, d)
i += 1
}
return
}
if k == N_SYS { # its .b and .c are its layer's and scene's tags
cx_walk(n.a, "value", null, null, d)
return
}
if k == N_SCENE {
cx_walk(n.a, "value", null, null, d)
cx_walk(n.b, "value", null, null, d)
return
}
cx_walk(n.a, "value", null, null, d)
cx_walk(n.b, "value", null, null, d)
cx_walk(n.c, "value", null, null, d)
var i = 0
while i < len(n.kids) {
cx_walk(n.kids[i], "value", null, null, d)
i += 1
}
}

View file

@ -304,6 +304,7 @@ const TPL_HOLE: int = 92 # on the callee of e
# non-cancellable event yields 0. # non-cancellable event yields 0.
function parse_emit() -> Node { function parse_emit() -> Node {
pi += 1; let n = node(S_EMIT); n.s = eat_id() pi += 1; let n = node(S_EMIT); n.s = eat_id()
cx_emit(n) # the schema's code map (emit_schema_code.ludic)
let r = node(E_REC) let r = node(E_REC)
eat_op("("); skipnl() eat_op("("); skipnl()
while not is_op(")") { while not is_op(")") {
@ -344,7 +345,7 @@ function p_primary() -> Node {
if (t.text == "spawn") { return parse_spawn() } # spawn as an expression: the new entity if (t.text == "spawn") { return parse_spawn() } # spawn as an expression: the new entity
# fn name — a top-level function as a value, for a worker entry point (Job.parallel_for) # fn name — a top-level function as a value, for a worker entry point (Job.parallel_for)
# `fn` stays an ordinary name before an operator word (`p < fn and ...`) or at the end of a line # `fn` stays an ordinary name before an operator word (`p < fn and ...`) or at the end of a line
if (t.text == "fn") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 1].line == t.line) and (toks[pi + 1].text != "and") and (toks[pi + 1].text != "or") and (toks[pi + 1].text != "not") and (toks[pi + 1].text != "in") and (toks[pi + 1].text != "is") { pi += 1; let n = node(E_FNREF); n.line = t.line; n.s = eat_id(); return n } if (t.text == "fn") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 1].line == t.line) and (toks[pi + 1].text != "and") and (toks[pi + 1].text != "or") and (toks[pi + 1].text != "not") and (toks[pi + 1].text != "in") and (toks[pi + 1].text != "is") { pi += 1; let n = node(E_FNREF); n.line = t.line; n.s = eat_id(); cx_fnref(n); return n }
# try EXPR else { ... } — evaluate a fallible (result-typed) expression; on # try EXPR else { ... } — evaluate a fallible (result-typed) expression; on
# `ok` the whole expression is its payload, on `err` the else block runs (with # `ok` the whole expression is its payload, on `err` the else block runs (with
# the message bound to `error`) and its trailing expression is the fallback. # the message bound to `error`) and its trailing expression is the fallback.
@ -954,6 +955,30 @@ function already_loaded(full: pointer) -> bool {
return false return false
} }
# a handler's hook, for the schema's code map: the attribute that made it one, and what it names
function decl_hook(ev: pointer, sp: pointer, dsp: pointer, att: pointer, det: pointer, en: pointer, dis: pointer, ph: pointer) -> pointer {
if ev != null { return "On" }
if sp != null { return "OnSpawn" }
if dsp != null { return "OnDespawn" }
if att != null { return "OnAttach" }
if det != null { return "OnDetach" }
if en != null { return "OnEnable" }
if dis != null { return "OnDisable" }
if ph != null {
if (ph == "Start") { return "OnStart" }
return "OnQuit"
}
return null
}
function decl_hook_target(ev: pointer, sp: pointer, dsp: pointer, att: pointer, det: pointer, en: pointer, dis: pointer) -> pointer {
if ev != null { return ev }
if sp != null { return sp }
if dsp != null { return dsp }
if att != null { return att }
if det != null { return det }
if en != null { return en }
return dis
}
# parse one top-level declaration (or resolve an import) into `prog`. # parse one top-level declaration (or resolve an import) into `prog`.
# Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`, # Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`,
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords. # `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
@ -1108,7 +1133,9 @@ function parse_one_decl() -> void {
if is_det { push(g_det_names, h.s) } if is_det { push(g_det_names, h.s) }
if g_aok_pending { push(g_alloc_ok_names, h.s); g_aok_pending = false } if g_aok_pending { push(g_alloc_ok_names, h.s); g_aok_pending = false }
if (role != null) { if (role == "server") { h.ival = 1 } else { h.ival = 2 } } # N5: @Server=1 / @Predicted=2 if (role != null) { if (role == "server") { h.ival = 1 } else { h.ival = 2 } } # N5: @Server=1 / @Predicted=2
if (on_event != null) { register_onlisten(on_event, h.a); return } # @On(Event) listener cx_handler(h, h.s, decl_hook(on_event, onspawn_model, ondespawn_model, onattach_prop, ondetach_prop, onenable_prop, ondisable_prop, hook_phase),
decl_hook_target(on_event, onspawn_model, ondespawn_model, onattach_prop, ondetach_prop, onenable_prop, ondisable_prop), qspec, is_public, null, null) # the schema's code map
if (on_event != null) { register_onlisten(on_event, h.a); cx_listened(); return } # @On(Event) listener
if (onspawn_model != null) { if (onspawn_model != null) {
register_onspawn(onspawn_model, h.a) # spawn hook register_onspawn(onspawn_model, h.a) # spawn hook
if is_public { ensure_event(`model_{onspawn_model}_spawn`, false) } # @Public -> model_<M>_spawn if is_public { ensure_event(`model_{onspawn_model}_spawn`, false) } # @Public -> model_<M>_spawn

View file

@ -189,7 +189,7 @@ function parse_queries_anno() -> Node {
let pname = eat_id() let pname = eat_id()
let v = node(E_ID); v.s = pname; push(qn.kids, v) # binding var = property name 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) let t = node(E_ID); t.s = pname; t.ival = 0; push(terms.kids, t)
if is_op("{") { pi += 1; let ce = expr(); eat_op("}") if is_op("{") { pi += 1; let f0 = pi; let ce = expr(); t.sp0 = toks[f0].off; t.sp1 = toks[pi - 1].oend; eat_op("}")
let cb = node(E_ID); cb.s = pname; wh = mk_and(wh, qualify_fields(ce, cb)) } let cb = node(E_ID); cb.s = pname; wh = mk_and(wh, qualify_fields(ce, cb)) }
if is_op(",") { pi += 1 } if is_op(",") { pi += 1 }
skipnl() skipnl()
@ -249,12 +249,13 @@ function parse_scene() -> void {
while is_id("start") or is_id("public") or is_id("shows") or is_id("lasts") or is_id("loads") { while is_id("start") or is_id("public") or is_id("shows") or is_id("lasts") or is_id("loads") {
if is_id("start") { pi += 1; g_start_scene = g_scene_count } if is_id("start") { pi += 1; g_start_scene = g_scene_count }
else if is_id("shows") { pi += 1; n.ty = eat_id() } # shows Menu: the engine opens, renders and closes it else if is_id("shows") { pi += 1; n.ty = eat_id() } # shows Menu: the engine opens, renders and closes it
else if is_id("lasts") { pi += 1; lasts = expr(); if not is_id("then") { perr("lasts N needs `then Scene`") }; pi += 1; then_scene = eat_id() } else if is_id("lasts") { pi += 1; let l0 = pi; lasts = expr(); n.sp0 = toks[l0].off; n.sp1 = toks[pi - 1].oend; if not is_id("then") { perr("lasts N needs `then Scene`") }; pi += 1; then_scene = eat_id() }
else if is_id("loads") { pi += 1; if not is_id("then") { perr("loads needs `then Scene`") }; pi += 1; loads_then = eat_id(); g_uses_atlas = true } else if is_id("loads") { pi += 1; if not is_id("then") { perr("loads needs `then Scene`") }; pi += 1; loads_then = eat_id(); g_uses_atlas = true }
else { pi += 1; is_pub = true } else { pi += 1; is_pub = true }
} }
g_scene_count += 1 g_scene_count += 1
if is_pub { ensure_event_empty(`scene_{n.s}_enter`); ensure_event_empty(`scene_{n.s}_exit`) } if is_pub { ensure_event_empty(`scene_{n.s}_enter`); ensure_event_empty(`scene_{n.s}_exit`) }
var layers = "," # the schema's code map (emit_schema_code.ludic)
skipnl(); eat_op("{") skipnl(); eat_op("{")
while true { while true {
skipnl(); if is_op("}") { break } skipnl(); if is_op("}") { break }
@ -273,6 +274,7 @@ function parse_scene() -> void {
} }
if is_id("layer") { # layer Name [public] { handler .. } if is_id("layer") { # layer Name [public] { handler .. }
pi += 1; let lname = eat_id() pi += 1; let lname = eat_id()
layers = layers + lname + ","
if is_id("public") { pi += 1; ensure_event_empty(`layer_{lname}_show`); ensure_event_empty(`layer_{lname}_hide`) } if is_id("public") { pi += 1; ensure_event_empty(`layer_{lname}_show`); ensure_event_empty(`layer_{lname}_hide`) }
skipnl(); eat_op("{") skipnl(); eat_op("{")
let ltag = node(E_ID); ltag.s = lname # the layer name, tagged onto each handler let ltag = node(E_ID); ltag.s = lname # the layer name, tagged onto each handler
@ -287,6 +289,7 @@ function parse_scene() -> void {
} }
if not is_id("handler") { perr("expected 'handler' in layer") } if not is_id("handler") { perr("expected 'handler' in layer") }
let h = parse_system() # N_SYS: .s name, .ty phase, .a body let h = parse_system() # N_SYS: .s name, .ty phase, .a body
cx_handler(h, h.s, null, null, qspec, false, n.s, lname)
if (qspec != null) { qspec.a = h.a; let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap } if (qspec != null) { qspec.a = h.a; let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap }
h.s = `{n.s}_{h.s}` # scene-qualified: two scenes may both have a `Draw` handler h.s = `{n.s}_{h.s}` # scene-qualified: two scenes may both have a `Draw` handler
h.c = n # tag the owning scene (null = global) h.c = n # tag the owning scene (null = global)
@ -302,6 +305,7 @@ function parse_scene() -> void {
eat_op("}") eat_op("}")
if (lasts != null) { scene_add_timer(n, lasts, then_scene) } if (lasts != null) { scene_add_timer(n, lasts, then_scene) }
if (loads_then != null) { scene_add_loader(n, loads_then) } if (loads_then != null) { scene_add_loader(n, loads_then) }
cx_scene(layers, then_scene, loads_then, is_pub)
push(g_scenes, n) push(g_scenes, n)
} }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,8 @@
# What an editor of a program's data needs, from the compiler's own view: every record with its # What an editor of a program's data needs, from the compiler's own view: every record with its
# fields, types, defaults, docs and attributes (@Ref, @Range, ...), every registry with its entries # fields, types, defaults, docs and attributes (@Ref, @Range, ...), every registry with its entries
# in their final order and where each is written, every const, and the functions a `fn` value can # in their final order and where each is written, every const, and the functions a `fn` value can
# name. The compiler writes it (`ludicc --emit-schema`, selfhost/backend/emit_schema.ludic) once the # name, the text keys, and the code map (modules, actions, events, ports, handlers, scenes, fn refs).
# The compiler writes it (`ludicc --emit-schema`, selfhost/backend/emit_schema.ludic) once the
# program has type-checked. # program has type-checked.
# #
# ludic schema [file] the JSON on stdout # ludic schema [file] the JSON on stdout

View file

@ -100,6 +100,8 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/backend/emit_schema_ui.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/backend/emit_schema_lang.ludic") push(f, "selfhost/backend/emit_schema_lang.ludic")
push(f, "selfhost/backend/emit_schema_code.ludic")
push(f, "selfhost/backend/emit_schema_code_game.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")
push(f, "selfhost/check/check_types.ludic") push(f, "selfhost/check/check_types.ludic")

View file

@ -1303,6 +1303,21 @@ function cmd_dev_test() -> int {
schema_case("lang/i18n/i18n", "\"unused\": [\"map.close\"]", "--emit-schema: lang.unused - en.po's keys the program uses nowhere") schema_case("lang/i18n/i18n", "\"unused\": [\"map.close\"]", "--emit-schema: lang.unused - en.po's keys the program uses nowhere")
schema_case("lang/i18n/i18n", "\"missing\": [\"hud.day\"], \"fuzzy\": [\"pause.close\"], \"extra\": [\"old.title\"]", "--emit-schema: lang.languages - another .po's missing, fuzzy and extra keys against en.po") schema_case("lang/i18n/i18n", "\"missing\": [\"hud.day\"], \"fuzzy\": [\"pause.close\"], \"extra\": [\"old.title\"]", "--emit-schema: lang.languages - another .po's missing, fuzzy and extra keys against en.po")
schema_case("lang/i18n/i18n", "{\"english\": \"Close\", \"keys\": [\"map.close\", \"pause.close\"]}", "--emit-schema: lang.split - one English under several keys") schema_case("lang/i18n/i18n", "{\"english\": \"Close\", \"keys\": [\"map.close\", \"pause.close\"]}", "--emit-schema: lang.split - one English under several keys")
schema_case("actions/modules", "{\"state\": \"Bag\", \"action\": \"PickUp\", \"module\": \"bag\", \"at\": \"examples/actions/mods/bag/index.ludic:4:16\"}", "--emit-schema: code.reducers - a reducer's state, action, module and place")
schema_case("actions/modules", "{\"action\": \"PickUp\", \"module\": null, \"at\": \"examples/actions/modules.ludic:8:21\"}", "--emit-schema: code.dispatch - every dispatch site")
schema_case("modules/ports", "{\"name\": \"Clock\", \"module\": \"clock\", \"at\": \"examples/modules/station/clock/index.ludic:4:13\", \"bound\": true, \"members\": [{\"name\": \"now\", \"type\": \"fn()->int\", \"default\": null, \"required\": true}, {\"name\": \"day\", \"type\": \"fn()->int\", \"default\": \"fn first_day\", \"required\": false}]}", "--emit-schema: code.ports - a port's members, their fn types and defaults")
schema_case("modules/ports", "{\"port\": \"Clock\", \"member\": \"now\", \"fn\": \"game_hours\", \"value\": \"fn game_hours\", \"module\": \"app\", \"at\": \"examples/modules/station/app/index.ludic:3:14\"}", "--emit-schema: code.binds - a bind's member, the function it names and where")
schema_case("modules/ports", "{\"name\": \"clock\", \"package\": null, \"layer\": null, \"uses\": [\"units\"], \"uses_at\": \"examples/modules/station/clock/index.ludic:3\", \"friend\": null, \"files\": [\"examples/modules/station/clock/index.ludic\"]}", "--emit-schema: code.modules - a module's uses and files")
schema_case("modules/friend_of", "{\"name\": \"lab\", \"package\": null, \"layer\": null, \"uses\": null, \"uses_at\": null, \"friend\": {\"of\": [\"bank\"]}", "--emit-schema: code.modules - a friend module and whose names it sees")
schema_case("networking/net_demo", "{\"name\": \"Move\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:24:19\", \"cancellable\": false, \"net\": \"toserver\", \"emits\": [\"examples/networking/net_demo.ludic:32:10\"], \"listeners\": [{\"handler\": \"DoMove\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:25:21\"}]}", "--emit-schema: code.events - a remote event, its emit sites and @On listeners")
schema_case("networking/net_demo", "{\"name\": \"Ship\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:22:16\", \"owned\": true, \"properties\": [{\"name\": \"Pos\", \"sync\": true}]}", "--emit-schema: code.models - an @Owned model and its @Sync property")
schema_case("networking/net_roles", "\"phase\": \"Update\", \"hook\": null, \"target\": null, \"public\": false, \"net\": \"server\"", "--emit-schema: code.handlers - an @Server handler's phase and net role")
schema_case("lang/annotations", "\"queries\": {\"these\": [{\"property\": \"Transform\", \"filter\": \"scale > 0\"}, {\"property\": \"Velocity\", \"filter\": \"dx > 0 or dy > 0\"}], \"on\": \"Actor\"}", "--emit-schema: code.handlers - @Queries' terms, filters and model")
schema_case("lang/annotations", "\"phase\": null, \"hook\": \"OnSpawn\", \"target\": \"Actor\"", "--emit-schema: code.handlers - a lifecycle hook and the model it names")
schema_case("lang/scenes", "{\"name\": \"Title\", \"module\": null, \"at\": \"examples/lang/scenes.ludic:24:9\", \"start\": true, \"public\": false, \"shows\": null, \"lasts\": null, \"then\": null, \"loads\": null, \"enter\": true, \"exit\": true, \"layers\": [\"Main\"]}", "--emit-schema: code.scenes - the start scene, its blocks and layers")
schema_case("lang/scenes", "\"scene\": \"Play\", \"layer\": \"Hud\"", "--emit-schema: code.handlers - a scene's handler, its scene and layer")
schema_case("state/write_reach", "{\"fn\": \"log_note\", \"module\": null, \"at\": \"examples/state/write_reach.ludic:21:37\", \"slot_kind\": \"registry\", \"slot\": \"Kinds.note\", \"entry\": \"box\"}", "--emit-schema: code.fn_refs - a fn in a registry row, the field and row it fills")
schema_case("modules/ports", "\"slot_kind\": \"port_default\", \"slot\": \"Clock.day\"", "--emit-schema: code.fn_refs - a port member's default")
diag_json_case("rejected/i18n_keys/key_missing", 2, "the key pause.gone is not in examples/rejected/i18n_keys/lang/en.po", "a key literal en.po does not have is refused, at its line and column") diag_json_case("rejected/i18n_keys/key_missing", 2, "the key pause.gone is not in examples/rejected/i18n_keys/lang/en.po", "a key literal en.po does not have is refused, at its line and column")
diag_json_case("rejected/i18n_keys/key_missing", 2, "is a plural key, and examples/rejected/i18n_keys/lang/en.po:7 gives pause.resume no msgid_plural", "kn\"...\" is a plural key: en.po's entry has a msgid_plural") diag_json_case("rejected/i18n_keys/key_missing", 2, "is a plural key, and examples/rejected/i18n_keys/lang/en.po:7 gives pause.resume no msgid_plural", "kn\"...\" is a plural key: en.po's entry has a msgid_plural")
diag_json_case("rejected/i18n_keys/key_missing", 2, "trf: hud.day is given 2 value(s), and its English in examples/rejected/i18n_keys/lang/en.po has 1 hole(s)", "trf given more values than the key's English has holes: a warning") diag_json_case("rejected/i18n_keys/key_missing", 2, "trf: hud.day is given 2 value(s), and its English in examples/rejected/i18n_keys/lang/en.po has 1 hole(s)", "trf given more values than the key's English has holes: a warning")