ludic/selfhost/backend/emit_schema_code.ludic
Orkuncakilkaya e4265f5fdb feat(lang): 27.1 - a state machine as data: @Machine(Record.field) on a registry of transitions
A registry marked `@Machine(Deer.mood)` is the transitions of a machine over that enum field of the
records a state's Table<Deer> holds. Its record has from and to (the enum's variants), on: string (an
action's name, "" for a transition the tick asks), guard: fn(Row<Deer>, reads...) -> bool and
enter: fn(Row<Deer>, reads...) -> void; the states are the enum's variants and the start is the
field's default. The rows are data (an .lres or defs), the names the studio already edits.

Written by the compiler (machines.ludic, machines_write.ludic): for each action an `on` names, a row
reducer in the registry's file (named ..__machine__DeerSteps, so it sits beside the program's own
row reducer on the same action, after it): the row's state, the first transition from it on that
action whose guard passes, the field set, enter run - guards and enters called by name. When a row
leaves a state on a guard alone, `state DeerStepsMachine` (the kept row view) and
deer_steps_tick(m: mut DeerStepsMachine, s: mut Herd, reads...), one transition a row a tick.
Nothing allocates.

The table is the whole machine: the field written anywhere else - an assignment, or a `machine`
block's become over it - is a type error (check_stmt.ludic, ck_machine_write). Guards and enters take
the row first, are the record's module's, keep a row reducer's rules (and may be handed the row);
a guard writes nothing through it. The graph is checked, each error at its row (in the .lres when
the rows are there): a state never reached from the start, a state with no way out, an `on` naming
no action or an action with no @Target, a self-transition with no guard, two ways out of a state on
one trigger behind an unguarded first. Also refused: @Machine off a registry, a field that is not a
plain enum with a default, a @Column field, no table (or two) of the record, a transitions record of
another shape, a machine outside its table's state's module.

ludic schema's code section gains `machines` (registry, record, field, enum, table, start, states,
actions, tick, module, at); ludic deps names a machine's reducer `reducer Deer in Herd.deer on Spook
(machine DeerSteps)`. vocab @Machine; docs annot-machine, kw-machine; LANGUAGE.md "A machine as
data"; examples actions/machine (+ deer_steps.lres) and ten rejects; test.ludic feat, reject and
schema cases (not run); changes/machines.md. Reseeded; bootstrap-cfree fixpoint holds (317642
lines); Maroon Lake's `ludic build --check` is clean against this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:37:37 +03:00

586 lines
18 KiB
Text

# 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
# row_reducers record, table (State.path), state, action, target (its @Target field), predicted,
# net, module, at row_verbs name, record, module, at (27.3)
# actions also carry their target: the @Target field, or null
# machines registry, record, field, enum, table (State.path), start, states (the enum's
# variants), actions (those its rows' `on` name), tick (or null), module, at (27.1)
# 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 \"row_reducers\": [")
cx_row_reducers(b)
buf_puts(b, "\n ],\n \"row_verbs\": [")
cx_row_verbs(b)
buf_puts(b, "\n ],\n \"machines\": [")
cx_machines(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)
if uns == 3 { # an action: the field naming its row (27.3)
buf_puts(b, ", \"target\": ")
sc_str(b, rr_target(d.s))
}
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) and g_red_rec[i] == null { 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
}
}
# 27.3: the reducers on a table's rows, and the verbs that write a row
function cx_row_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) and g_red_rec[i] != null { push(keys, `{g_red_rec[i]}\t{g_red_state[i]}.{g_red_tbl[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, "{\"record\": ")
jq_put(b, g_red_rec[k])
buf_puts(b, ", \"table\": ")
jq_put(b, `{g_red_state[k]}.{g_red_tbl[k]}`)
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, ", \"target\": ")
sc_str(b, g_red_tgt[k])
buf_puts(b, ", \"predicted\": false, \"net\": null, ") # @Predicted and @Net come with 27.5
cx_module(b, f.file)
buf_puts(b, ", ")
cx_at(b, f)
buf_puts(b, "}")
i += 1
}
}
# 27.1: each @Machine registry: the field it drives, its table, and what the compiler wrote for it
function cx_machines(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(g_mc_reg) {
let v = g_rg_var[reg_find(g_mc_reg[i])]
if cx_written(v) { push(keys, sc_key(g_mc_reg[i], v.file, v.line)) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let m = xs[i]
let v = g_rg_var[reg_find(g_mc_reg[m])]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"registry\": ")
jq_put(b, g_mc_reg[m])
buf_puts(b, ", \"record\": ")
jq_put(b, g_mc_rec[m])
buf_puts(b, ", \"field\": ")
jq_put(b, g_mc_field[m])
buf_puts(b, ", \"enum\": ")
sc_str(b, g_mc_enum[m])
buf_puts(b, ", \"table\": ")
if g_mc_state[m] == null { buf_puts(b, "null") } else { jq_put(b, `{g_mc_state[m]}.{g_mc_path[m]}`) }
buf_puts(b, ", \"start\": ")
sc_str(b, g_mc_start[m])
buf_puts(b, ", \"states\": ")
cx_mc_states(b, g_mc_enum[m])
buf_puts(b, ", \"actions\": ")
cx_list(b, g_mc_acts[m])
buf_puts(b, ", \"tick\": ")
sc_str(b, g_mc_tick[m])
buf_puts(b, ", ")
cx_module(b, v.file)
buf_puts(b, ", ")
cx_at(b, v)
buf_puts(b, "}")
i += 1
}
}
function cx_mc_states(b: Buf, en: pointer) -> void {
buf_puts(b, "[")
if en != null {
let d = mc_enum_decl(en)
var i = 0
while d != null and i < len(d.kids) {
if i > 0 { buf_puts(b, ", ") }
jq_put(b, d.kids[i].s)
i += 1
}
}
buf_puts(b, "]")
}
function cx_row_verbs(b: Buf) -> void {
let keys = new []pointer
var i = 0
while i < len(g_rv_nodes) {
let f = g_rv_nodes[i]
if cx_written(f) { push(keys, sc_key(f.s, f.file, f.line)) } else { push(keys, null) }
i += 1
}
let xs = cx_order(keys)
i = 0
while i < len(xs) {
let f = g_rv_nodes[xs[i]]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, f.s)
buf_puts(b, ", \"record\": ")
sc_str(b, rv_record(f))
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
}
}