ludic/selfhost/frontend/machines_write.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

226 lines
9.2 KiB
Text

# machines_write.ludic — 27.1: what the compiler writes for a @Machine, and the checks on the
# functions its rows name. For each action an `on` names, a row reducer in the registry's file:
#
# reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, ludic_mc0: Herd, a: Spook) {
# let cur = r.rec.mood
# var took = false
# if not took and cur == Mood.Calm and deer_near(r, ludic_mc0) {
# took = true
# r.rec.mood = Mood.Wary
# deer_look(r, ludic_mc0)
# }
# ...
# }
#
# and, when a row leaves on a guard alone, a state keeping the tick's row view and the tick, which
# takes each row of the table through the same first match (one transition a row a tick):
#
# state DeerStepsMachine { row: Row<Deer> = new Row<Deer> }
# function deer_steps_tick(ludic_m: mut DeerStepsMachine, ludic_s: mut Herd, reads...) -> void
#
# A guard and an enter take the row first and the states they read after it (the record's field
# types say which); they are their record's module's, and they keep a row reducer's rules - the row
# reaches `r.rec` and `r.h`, is handed only to a @RowVerb or another of the machine's functions, and a
# @Column field is not written. A guard asks and writes nothing through its row.
# the states the guard and enter read, after the row, each once, in the order the record gives them
function mc_reads(comp: Node) -> []pointer {
let out = new []pointer
mc_reads_of(out, mc_field(comp, "guard"))
mc_reads_of(out, mc_field(comp, "enter"))
return out
}
function mc_reads_of(out: []pointer, f: Node) -> void {
if f == null { return }
let ps = fn_ty_params(f.ty)
var i = 1
while i < len(ps) {
if not mc_in(out, ps[i]) { push(out, ps[i]) }
i += 1
}
}
function mc_idx(xs: []pointer, s: pointer) -> int {
var i = 0
while i < len(xs) {
if (xs[i] == s) { return i }
i += 1
}
return -1
}
# a call's arguments: the row, then each state the field's type names
function mc_args(f: Node, reads: []pointer, table_st: pointer) -> pointer {
var out: pointer = "r"
if f == null { return out }
let ps = fn_ty_params(f.ty)
var i = 1
while i < len(ps) {
if table_st != null and (ps[i] == table_st) { out = out + ", ludic_s" } else { out = out + `, ludic_mc{itoa(mc_idx(reads, ps[i]))}` }
i += 1
}
return out
}
# the first match from the row's current state among the transitions on one trigger
function mc_steps_src(m: int, trig: pointer, reads: []pointer, table_st: pointer, ind: pointer) -> pointer {
let r = reg_find(g_mc_reg[m])
let comp = find_comp(g_rg_type[r])
let en = g_mc_enum[m]
let gargs = mc_args(mc_field(comp, "guard"), reads, table_st)
let eargs = mc_args(mc_field(comp, "enter"), reads, table_st)
var src = `{ind}let cur = r.rec.{g_mc_field[m]}\n{ind}var took = false\n`
var t = 0
while t < len(g_mt_key) {
if (g_mt_on[t] == trig) {
var cond = `not took and cur == {en}.{g_mt_from[t]}`
if g_mt_guard[t] != null { cond = cond + ` and {g_mt_guard[t]}({gargs})` }
src = src + `{ind}if {cond} {{\n{ind} took = true\n{ind} r.rec.{g_mc_field[m]} = {en}.{g_mt_to[t]}\n`
if g_mt_enter[t] != null { src = src + `{ind} {g_mt_enter[t]}({eargs})\n` }
src = src + `{ind}}}\n`
}
t += 1
}
return src
}
function mc_write(m: int) -> void {
let r = reg_find(g_mc_reg[m])
let v = g_rg_var[r]
let comp = find_comp(g_rg_type[r])
let reads = mc_reads(comp)
let rec = g_mc_rec[m]
let st = g_mc_state[m]
var params: pointer = ""
var j = 0
while j < len(reads) {
params = params + `, ludic_mc{itoa(j)}: {reads[j]}`
j += 1
}
# a row reducer per action, in the order the table first names them
var t = 0
var any_tick = false
while t < len(g_mt_key) {
let a = g_mt_on[t]
if len(a) == 0 { any_tick = true }
if len(a) > 0 and not has_sub(g_mc_acts[m], `,{a},`) {
g_mc_acts[m] = g_mc_acts[m] + a + ","
let src = `reducer {rec} in {st}.{g_mc_path[m]} on {a}(r: mut Row<{rec}>{params}, a: {a}) {{\n{mc_steps_src(m, a, reads, null, " ")}}}\n`
g_mc_gen_reg = g_mc_reg[m]
mc_parse_gen(src, v, 1)
g_mc_gen_reg = null
}
t += 1
}
if not any_tick { return }
# the tick: every row of the table through the guard-only transitions
let tick = `{pm_snake(g_mc_reg[m])}_tick`
g_mc_tick[m] = tick
var tparams: pointer = ""
j = 0
while j < len(reads) {
if not (reads[j] == st) { tparams = tparams + `, ludic_mc{itoa(j)}: {reads[j]}` }
j += 1
}
var src = `state {g_mc_reg[m]}Machine {{ row: Row<{rec}> = new Row<{rec}> }}\n`
src = src + `function {tick}(ludic_m: mut {g_mc_reg[m]}Machine, ludic_s: mut {st}{tparams}) -> void {{\n`
src = src + ` let tb = ludic_s.{g_mc_path[m]}\n if tb == null {{ return }}\n let r = ludic_m.row\n r.tb = tb\n var row = 0\n while row < tb_len(tb) {{\n`
src = src + " r.row = row\n r.h = tb_handle(tb, row)\n r.rec = tb_rec(tb, row)\n"
src = src + mc_steps_src(m, "", reads, st, " ")
src = src + " row += 1\n }\n}\n"
mc_parse_gen(src, v, v.vis)
}
# written into the registry's file, as its module's; every function it makes is the machine's
function mc_parse_gen(src: pointer, v: Node, vis: int) -> void {
let at = len(prog)
vw_parse(src, v.file, v.line, vis)
var k = at
while k < len(prog) {
if prog[k].kind == N_FN { push(g_mc_gen, prog[k]) }
k += 1
}
}
# ---- the functions a machine's rows name ------------------------------------------------------------
function mc_check_fns(m: int) -> void {
let r = reg_find(g_mc_reg[m])
let comp = find_comp(g_rg_type[r])
let rd = rr_decl(g_mc_rec[m])
let order = reg_order(r)
var o = 0
let seen = new []pointer
while o < len(order) {
let rec = g_df_rec[order[o]]
mc_check_fn(m, mc_value(rec, comp, "guard"), true, rd, seen)
mc_check_fn(m, mc_value(rec, comp, "enter"), false, rd, seen)
o += 1
}
}
function mc_check_fn(m: int, e: Node, guard: bool, rd: Node, seen: []pointer) -> void {
if e == null or e.kind != E_FNREF or mc_in(seen, e.s) { return }
push(seen, e.s)
let f = mc_fn_decl(e.s)
if f == null { return }
var what = "enter"
if guard { what = "guard" }
let who = `{g_mc_reg[m]}'s {what} {f.s}`
var first: Node = null
var k = 0
while k < len(f.kids) {
if f.kids[k].kind == N_PARAM and first == null { first = f.kids[k] }
k += 1
}
if first == null or not (first.ty == `Row${g_mc_rec[m]}`) {
perr_at(f.file, f.line, f.col, `{who} takes the row first - ({"r"}: Row<{g_mc_rec[m]}>, reads...)`)
return
}
if rd != null and not (rr_mod(rd.file) == rr_mod(f.file)) {
perr_at(f.file, f.line, f.col, `{who} is declared in {rr_mod_name(f.file)}, and {g_mc_rec[m]} is {rr_mod_name(rd.file)}'s: a row changes only through its record's own module's functions`)
}
mc_walk(f.a, m, who, first.s, guard, f)
}
function mc_walk_err(n: Node, at: Node, msg: pointer) -> void {
var w = n
if w == null or w.file == null or w.line == 0 { w = at }
perr_at(w.file, w.line, w.col, msg)
}
# a row reducer's rules, and a guard writes nothing through its row
function mc_walk(n: Node, m: int, who: pointer, r: pointer, guard: bool, f: Node) -> void {
if n == null { return }
let rec = g_mc_rec[m]
if n.kind == E_MEMBER and n.a != null and n.a.kind == E_ID and (n.a.s == r) {
if not ((n.s == "rec") or (n.s == "h")) { mc_walk_err(n, f, `{who}: {r}.{n.s} - a machine's function reaches its row's record and handle ({r}.rec, {r}.h) and no further; its table is written by {rec}'s @RowVerbs`) }
return
}
if n.kind == S_ASSIGN and n.a != null {
let root = ck_chain_root(n.a)
if guard and root != null and (root.s == r) { mc_walk_err(n, f, `{who} asks and changes nothing: it writes through {r} - a change belongs in the transition's enter`) }
let t = n.a
if t.kind == E_MEMBER and t.a != null and t.a.kind == E_MEMBER and (t.a.s == "rec") and t.a.a != null and t.a.a.kind == E_ID and (t.a.a.s == r) and rr_is_column(rec, t.s) {
mc_walk_err(n, f, `{who}: {rec}.{t.s} is mirrored by a column of the table (@Column) - write it through a @RowVerb, which keeps the column and its indexes with it`)
}
}
if n.kind == E_CALL {
mc_walk(n.a, m, who, r, guard, f)
var k = 0
while k < len(n.kids) {
var x = n.kids[k]
if x != null and x.kind == E_FINIT { x = x.a }
if x != null and x.kind == E_ID and (x.s == r) {
if guard and n.a != null and n.a.kind == E_ID and rr_is_verb(n.a.s) { mc_walk_err(n, f, `{who} asks and changes nothing: it hands {r} to the verb {n.a.s}`) }
if not (n.a != null and n.a.kind == E_ID and (rr_is_verb(n.a.s) or mc_is_fn(n.a.s))) {
var name: pointer = "a method"
if n.a != null and n.a.kind == E_ID { name = n.a.s }
mc_walk_err(n, f, `{who}: {name} is handed the row, and is not a @RowVerb - only the verbs of {rec}'s own module take a row`)
}
} else { mc_walk(n.kids[k], m, who, r, guard, f) }
k += 1
}
return
}
if n.kind == E_ID and (n.s == r) { mc_walk_err(n, f, `{who}: {r} goes no further than its record, its handle and {rec}'s @RowVerbs - it is the machine's view, not stored, copied or handed on`) }
mc_walk(n.a, m, who, r, guard, f)
mc_walk(n.b, m, who, r, guard, f)
mc_walk(n.c, m, who, r, guard, f)
var j = 0
while j < len(n.kids) {
mc_walk(n.kids[j], m, who, r, guard, f)
j += 1
}
}