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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 16:37:37 +03:00
parent a637aec63e
commit e4265f5fdb
33 changed files with 131217 additions and 109412 deletions

View file

@ -47,6 +47,7 @@ function parse_reducer() -> void {
push(g_red_state, st)
push(g_red_action, act)
push(g_red_rec, null) # a state's reducer: no row (actions_rows.ludic)
push(g_red_mc, null) # ... and no machine wrote it (machines.ludic)
push(g_red_tbl, null)
push(g_red_tgt, null)
push(prog, f)
@ -102,6 +103,7 @@ function act_err(n: Node, msg: pointer) -> void {
# after the program is read: the reducers checked, each dispatch numbered, and the queue, its push
# and its drain written
function actions_finish() -> void {
machines_finish() # 27.1: each @Machine checked and its reducers written
rows_finish_checks() # 27.3: @Target, @RowVerb (actions_rows.ludic)
var i = 0
while i < len(g_red_nodes) {

View file

@ -16,6 +16,7 @@
var g_red_rec: []pointer = new []pointer # per reducer: the row's record, or null (a state's reducer)
var g_red_tbl: []pointer = new []pointer # ... the table's path in its state ("deer", "w.tab")
var g_red_tgt: []pointer = new []pointer # ... the action's @Target field, once checked
var g_red_mc: []pointer = new []pointer # ... the @Machine registry that wrote it, or null (27.1)
var g_tgt_comp: []pointer = new []pointer # every @Target field: its record, name, type, node
var g_tgt_field: []pointer = new []pointer
var g_tgt_node: []Node = new []Node
@ -42,7 +43,7 @@ function parse_row_reducer() -> void {
pi = k + 1
toks[pi].text = "function" # read the rest as a function named for the action
let f = parse_fn()
f.s = `ludic_rowred__{act}__{rr_suffix(rec, st, path)}`
f.s = `ludic_rowred__{act}__{rr_suffix(rec, st, path)}{rr_mc_suffix(g_mc_gen_reg)}`
f.vis = 1
push(g_red_nodes, f)
push(g_red_state, st)
@ -50,8 +51,19 @@ function parse_row_reducer() -> void {
push(g_red_rec, rec)
push(g_red_tbl, path)
push(g_red_tgt, null)
push(g_red_mc, g_mc_gen_reg)
push(prog, f)
}
# a machine's reducer beside the program's own on the same row and action: Deer__in__Herd__deer__machine__DeerSteps
function rr_mc_suffix(reg: pointer) -> pointer {
if reg == null { return "" }
return `__machine__{reg}`
}
# two names, either of which may be none
function rr_same(a: pointer, b: pointer) -> bool {
if a == null or b == null { return a == null and b == null }
return (a == b)
}
function rr_shape() -> void {
perr("a row reducer is `reducer Record in State.table on Action(r: mut Row<Record>, a: Action) { ... }`")
}
@ -67,7 +79,7 @@ function rr_suffix(rec: pointer, st: pointer, path: pointer) -> pointer {
return `{rec}__in__{st}__{p}`
}
function rr_wrap_name(i: int) -> pointer {
return `ludic_reduce__{g_red_action[i]}__{rr_suffix(g_red_rec[i], g_red_state[i], g_red_tbl[i])}`
return `ludic_reduce__{g_red_action[i]}__{rr_suffix(g_red_rec[i], g_red_state[i], g_red_tbl[i])}{rr_mc_suffix(g_red_mc[i])}`
}
# a field's @Target / @Column, read by parse_component
function rr_field_attr(comp: pointer, f: Node, is_target: bool, is_column: bool) -> void {
@ -154,7 +166,7 @@ function rows_finish_checks() -> void {
i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_FN and d.pos >= 0 and d.tps == null and not rr_is_verb(d.s) and not str_starts(d.s, "ludic_rowred__") { rr_no_row_param(d) }
if d.kind == N_FN and d.pos >= 0 and d.tps == null and not rr_is_verb(d.s) and not mc_is_fn(d.s) and not str_starts(d.s, "ludic_rowred__") { rr_no_row_param(d) }
i += 1
}
}
@ -215,7 +227,7 @@ function rr_check(i: int) -> void {
rr_params(i, f, who)
var j = 0
while j < i {
if g_red_rec[j] != null and (g_red_state[j] == st) and (g_red_action[j] == act) and (g_red_rec[j] == rec) and (g_red_tbl[j] == path) { act_err(f, `{who} is declared twice (first at {g_red_nodes[j].file}:{itoa(g_red_nodes[j].line)})`) }
if g_red_rec[j] != null and (g_red_state[j] == st) and (g_red_action[j] == act) and (g_red_rec[j] == rec) and (g_red_tbl[j] == path) and rr_same(g_red_mc[j], g_red_mc[i]) { act_err(f, `{who} is declared twice (first at {g_red_nodes[j].file}:{itoa(g_red_nodes[j].line)})`) }
j += 1
}
}
@ -327,7 +339,7 @@ function rr_walk(n: Node, i: int, r: pointer) -> void {
}
}
function rr_call_verb(i: int, n: Node, r: pointer) -> void {
if n.a != null and n.a.kind == E_ID and rr_is_verb(n.a.s) { return }
if n.a != null and n.a.kind == E_ID and (rr_is_verb(n.a.s) or mc_is_fn(n.a.s)) { return } # a verb, or a machine's guard or enter
var name: pointer = "a method"
if n.a != null and n.a.kind == E_ID { name = n.a.s }
rr_err(i, n, `{name} is handed the row, and is not a @RowVerb: only the verbs of {g_red_rec[i]}'s own module take a row`)

View file

@ -0,0 +1,431 @@
# machines.ludic — 27.1: a state machine as data. A record's enum field is the state, and a registry
# marked @Machine(Record.field) is its transitions table, one row a transition:
#
# enum Mood { Calm, Wary, Fled }
# property Deer { mood: Mood = Mood.Calm, fear: int = 0 } # the start is the default
# state Herd { deer: Table<Deer> = null }
# property DeerStep {
# from: Mood = Mood.Calm
# to: Mood = Mood.Calm
# on: string = "" # an action, or "" for the tick
# guard: fn(Row<Deer>, Herd) -> bool = null
# enter: fn(Row<Deer>, Herd) -> void = null
# }
# @Machine(Deer.mood) registry DeerSteps of DeerStep from "deer_steps.lres"
#
# The compiler writes the machine from the table: a row reducer on each action an `on` names (the
# row's current state, the first transition from it whose guard passes, the field set, `enter` run)
# and, when a row leaves on a guard alone, `<registry>_tick(m: mut <Registry>Machine, s: mut State,
# reads...)`, which asks those guards of every row of the table. The table is the whole machine: the
# field is written by nothing else (check_stmt.ludic), and the graph is checked here - every state
# reached from the start, every state with a way out, every `on` an action that names a row, no
# self-transition without a guard, no second way out on one trigger behind an unguarded first.
var g_mc_reg: []pointer = new []pointer # per machine: its registry, the record and field
var g_mc_rec: []pointer = new []pointer
var g_mc_field: []pointer = new []pointer
var g_mc_file: []pointer = new []pointer # where @Machine was written
var g_mc_line: []int = new []int
var g_mc_enum: []pointer = new []pointer # ... worked out when the program is read
var g_mc_state: []pointer = new []pointer # the state holding Table<Record>, and the field
var g_mc_path: []pointer = new []pointer
var g_mc_start: []pointer = new []pointer # the start: the field's default
var g_mc_tick: []pointer = new []pointer # the tick's name, or null (no guard-only transition)
var g_mc_acts: []pointer = new []pointer # the actions its `on`s name, ",A,B,"
var g_mc_gen: []Node = new []Node # every function the machines wrote: the field is theirs
var g_mc_fns: []pointer = new []pointer # every guard and enter a machine names
var g_mc_pend_rec: pointer = null # @Machine(Rec.field) read, for the registry after it
var g_mc_pend_field: pointer = null
var g_mc_pend_line: int = 0
var g_mc_gen_reg: pointer = null # the machine whose reducers are being written
# one machine's transitions, while it is checked and written
var g_mt_key: []pointer = new []pointer
var g_mt_from: []pointer = new []pointer
var g_mt_to: []pointer = new []pointer
var g_mt_on: []pointer = new []pointer
var g_mt_guard: []pointer = new []pointer
var g_mt_enter: []pointer = new []pointer
var g_mt_file: []pointer = new []pointer
var g_mt_line: []int = new []int
var g_mt_col: []int = new []int
# @Machine(Record.field), before a registry
function mc_parse_attr() -> void {
eat_op("(")
let rec = eat_id()
if not is_op(".") { perr("@Machine names the record's field that holds the state: @Machine(WildAnimal.state)") }
pi += 1
let field = eat_id()
eat_op(")")
g_mc_pend_rec = rec
g_mc_pend_field = field
g_mc_pend_line = toks[pi - 1].line
at_decl_with("Machine", `["{rec}.{field}"]`)
}
# after a declaration's attributes: @Machine is a registry's
function mc_attr_placed() -> void {
if g_mc_pend_rec == null { return }
if is_id("registry") or (is_id("open") and (toks[pi + 1].text == "registry")) { return }
perr(`@Machine({g_mc_pend_rec}.{g_mc_pend_field}) marks the registry of a machine's transitions: @Machine({g_mc_pend_rec}.{g_mc_pend_field}) registry Steps of Step from "steps.lres"`)
}
# the registry just declared is a machine's
function mc_take(v: Node, pm: bool) -> void {
if g_mc_pend_rec == null { return }
if pm { perr(`registry {v.s}: a machine's transitions are the program's, compiled in - a @PerMap registry is read when a map loads`) }
push(g_mc_reg, v.s)
push(g_mc_rec, g_mc_pend_rec)
push(g_mc_field, g_mc_pend_field)
push(g_mc_file, g_parse_file)
push(g_mc_line, g_mc_pend_line)
push(g_mc_enum, null)
push(g_mc_state, null)
push(g_mc_path, null)
push(g_mc_start, null)
push(g_mc_tick, null)
push(g_mc_acts, ",")
g_mc_pend_rec = null
g_mc_pend_field = null
}
function mc_find_reg(name: pointer) -> int {
var i = 0
while i < len(g_mc_reg) {
if (g_mc_reg[i] == name) { return i }
i += 1
}
return -1
}
# the machine whose state is Record.field, or -1
function mc_of_field(rec: pointer, field: pointer) -> int {
var i = 0
while i < len(g_mc_rec) {
if (g_mc_rec[i] == rec) and (g_mc_field[i] == field) { return i }
i += 1
}
return -1
}
function mc_is_fn(name: pointer) -> bool {
var i = 0
while i < len(g_mc_fns) {
if (g_mc_fns[i] == name) { return true }
i += 1
}
return false
}
function mc_is_gen(d: Node) -> bool {
if d == null { return false }
var i = 0
while i < len(g_mc_gen) {
if g_mc_gen[i] == d { return true }
i += 1
}
return false
}
function mc_err(m: int, msg: pointer) -> void {
perr_at(g_mc_file[m], g_mc_line[m], 0, `@Machine({g_mc_rec[m]}.{g_mc_field[m]}) registry {g_mc_reg[m]}: {msg}`)
}
function mc_row_err(m: int, t: int, msg: pointer) -> void {
perr_at(g_mt_file[t], g_mt_line[t], g_mt_col[t], `{g_mc_reg[m]} {g_mt_key[t]}: {msg}`)
}
function mc_enum_decl(name: pointer) -> Node {
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_ENUM and (d.s == name) { return d }
i += 1
}
return null
}
function mc_fn_decl(name: pointer) -> Node {
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_FN and (d.s == name) { return d }
i += 1
}
return null
}
function mc_field(comp: Node, name: pointer) -> Node {
if comp == null { return null }
var i = 0
while i < len(comp.kids) {
if comp.kids[i].kind == N_FIELD and (comp.kids[i].s == name) { return comp.kids[i] }
i += 1
}
return null
}
function mc_has_variant(en: Node, v: pointer) -> bool {
var i = 0
while i < len(en.kids) {
if (en.kids[i].s == v) { return true }
i += 1
}
return false
}
# `Mood.Calm` -> "Calm", when it names a variant of the enum; else null
function mc_variant(e: Node, en: Node) -> pointer {
if e == null or e.kind != E_MEMBER or e.a == null or e.a.kind != E_ID or not (e.a.s == en.s) { return null }
if not mc_has_variant(en, e.s) { return null }
return e.s
}
# an entry's value for a field: what it writes, else the record's default
function mc_value(rec: Node, comp: Node, name: pointer) -> Node {
var i = 0
while i < len(rec.kids) {
let k = rec.kids[i]
if k.kind == E_FINIT and (k.s == name) { return k.a }
i += 1
}
let f = mc_field(comp, name)
if f == null { return null }
return f.a
}
# ---- when the program is read ----------------------------------------------------------------------
# each machine resolved, its table checked, and its reducers and tick written
function machines_finish() -> void {
var m = 0
while m < len(g_mc_reg) {
mc_resolve(m)
m += 1
}
diag_stop_if_errors()
m = 0
while m < len(g_mc_reg) {
mc_rows(m)
mc_graph(m)
diag_stop_if_errors()
mc_write(m)
m += 1
}
m = 0
while m < len(g_mc_reg) {
mc_check_fns(m)
m += 1
}
diag_stop_if_errors()
}
# the record, its enum field and start, the transitions' record, and the one table of the record
function mc_resolve(m: int) -> void {
let rec = g_mc_rec[m]
let field = g_mc_field[m]
let r = reg_find(g_mc_reg[m])
let d = rr_decl(rec)
if d == null {
mc_err(m, `{rec} is not a record - @Machine names a property's enum field: @Machine(Deer.mood)`)
return
}
if d.uns == 2 or d.uns == 3 {
mc_err(m, `{rec} is a state or an action - a machine's state is a field of the records held in a Table<{rec}>, one machine per row`)
return
}
let f = mc_field(d, field)
if f == null {
mc_err(m, `{rec} has no field {field}`)
return
}
let en = mc_enum_decl(f.ty)
if en == null or enum_is_tagged(en) {
mc_err(m, `{rec}.{field} is {rr_ty_text(f.ty)} - a machine's state is a plain enum, its variants the states ({field}: Mood = Mood.Calm)`)
return
}
g_mc_enum[m] = en.s
let st = mc_variant(f.a, en)
if st == null {
mc_err(m, `{rec}.{field} has no start - its default is the state a row starts in ({field}: {en.s} = {en.s}.{en.kids[0].s})`)
return
}
g_mc_start[m] = st
if rr_is_column(rec, field) { mc_err(m, `{rec}.{field} is @Column - a machine writes its state on the row's record, and a column mirroring it would go stale`) }
mc_resolve_table(m, rec)
mc_resolve_shape(m, find_comp(g_rg_type[r]), en.s, rec)
if g_mc_state[m] != null {
let sd = rr_decl(g_mc_state[m])
if sd != null and not (rr_mod(sd.file) == rr_mod(g_mc_file[m])) {
mc_err(m, `{g_mc_state[m]} is {rr_mod_name(sd.file)}'s, and this machine is in {rr_mod_name(g_mc_file[m])} - a table's rows are reduced only by the module that owns its state`)
}
}
}
# the one state field of type Table<Record>
function mc_resolve_table(m: int, rec: pointer) -> void {
var n = 0
var where: pointer = ""
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_COMP and d.uns == 2 {
var k = 0
while k < len(d.kids) {
let f = d.kids[k]
if f.kind == N_FIELD and (f.ty == `Table${rec}`) {
if n == 0 {
g_mc_state[m] = d.s
g_mc_path[m] = f.s
}
if n > 0 { where = where + ", " }
where = where + `{d.s}.{f.s}`
n += 1
}
k += 1
}
}
i += 1
}
if n == 0 { mc_err(m, `no state holds a Table<{rec}> - a machine runs on the rows of one (state Herd {{ deer: Table<{rec}> = null }})`) }
if n > 1 { mc_err(m, `{n} states' fields hold a Table<{rec}> ({where}) - a machine runs on one table`) }
}
# the transitions' record: from and to of the enum, on a string, guard and enter on the row
function mc_resolve_shape(m: int, comp: Node, en: pointer, rec: pointer) -> void {
let shape = `from: {en}, to: {en}, on: string, guard: fn(Row<{rec}>, ...) -> bool, enter: fn(Row<{rec}>, ...) -> void`
let r = reg_find(g_mc_reg[m])
if comp == null {
mc_err(m, `its record {g_rg_type[r]} is not a property - a transition is ({shape})`)
return
}
let fr = mc_field(comp, "from")
let to = mc_field(comp, "to")
let on = mc_field(comp, "on")
if fr == null or to == null or on == null or not (fr.ty == en) or not (to.ty == en) or not (on.ty == "string") {
mc_err(m, `{comp.s} is not a transition - its fields are ({shape})`)
return
}
mc_shape_fn(m, comp, "guard", "bool", rec, shape)
mc_shape_fn(m, comp, "enter", "void", rec, shape)
}
function mc_shape_fn(m: int, comp: Node, name: pointer, ret: pointer, rec: pointer, shape: pointer) -> void {
let f = mc_field(comp, name)
if f == null { return } # no guards (or no enters) in this machine
if not str_starts(f.ty, "fn(") or not (fn_ty_ret(f.ty) == ret) {
mc_err(m, `{comp.s}.{name} is {f.ty} - a transition is ({shape})`)
return
}
let ps = fn_ty_params(f.ty)
if len(ps) == 0 or not (ps[0] == `Row${rec}`) {
mc_err(m, `{comp.s}.{name} takes the row first: fn(Row<{rec}>, ...) -> {ret}`)
return
}
var i = 1
while i < len(ps) {
if not is_state_ty(ps[i]) { mc_err(m, `{comp.s}.{name}: after the row it takes the states it reads, and {rr_ty_text(ps[i])} is not a state`) }
i += 1
}
}
# ---- the table's rows --------------------------------------------------------------------------------
function mc_rows(m: int) -> void {
g_mt_key = new []pointer
g_mt_from = new []pointer
g_mt_to = new []pointer
g_mt_on = new []pointer
g_mt_guard = new []pointer
g_mt_enter = new []pointer
g_mt_file = new []pointer
g_mt_line = new []int
g_mt_col = new []int
let r = reg_find(g_mc_reg[m])
let comp = find_comp(g_rg_type[r])
let en = mc_enum_decl(g_mc_enum[m])
let order = reg_order(r)
var o = 0
while o < len(order) {
let d = order[o]
let rec = g_df_rec[d]
push(g_mt_key, g_df_key[d])
push(g_mt_file, rec.file)
push(g_mt_line, g_df_kline[d])
push(g_mt_col, g_df_kcol[d])
let t = len(g_mt_key) - 1
push(g_mt_from, mc_variant(mc_value(rec, comp, "from"), en))
push(g_mt_to, mc_variant(mc_value(rec, comp, "to"), en))
push(g_mt_on, mc_on(m, t, mc_value(rec, comp, "on")))
push(g_mt_guard, mc_fn_of(m, t, mc_value(rec, comp, "guard"), "guard"))
push(g_mt_enter, mc_fn_of(m, t, mc_value(rec, comp, "enter"), "enter"))
if g_mt_from[t] == null { mc_row_err(m, t, `from is not a state of {en.s} - write one of its variants (from: {en.s}.{en.kids[0].s})`) }
if g_mt_to[t] == null { mc_row_err(m, t, `to is not a state of {en.s} - write one of its variants (to: {en.s}.{en.kids[0].s})`) }
o += 1
}
}
# `on`: "" for the tick, else an action that names a row
function mc_on(m: int, t: int, e: Node) -> pointer {
if e == null { return "" }
if e.kind != E_STR {
mc_row_err(m, t, "on is an action's name as a string (on: \"Spook\"), or \"\" for a transition the tick asks")
return ""
}
if len(e.s) == 0 { return "" }
if act_index(e.s) < 0 {
mc_row_err(m, t, `on: "{e.s}" names no action - declare it (action {e.s} {{ @Target who: int = -1 }}), or leave on "" for the tick`)
return e.s
}
if rr_target(e.s) == null { mc_row_err(m, t, `on: "{e.s}" names no row - mark the field of {e.s} that holds the row's handle @Target (@Target who: int = -1)`) }
return e.s
}
# a guard's or an enter's function, named with `fn name` (null: none)
function mc_fn_of(m: int, t: int, e: Node, what: pointer) -> pointer {
if e == null or e.kind == E_NULL { return null }
if e.kind != E_FNREF {
mc_row_err(m, t, `{what} names a function of {g_mc_rec[m]}'s module: {what}: fn deer_{what}`)
return null
}
let f = mc_fn_decl(e.s)
if f == null {
mc_row_err(m, t, `{what}: fn {e.s} - there is no function {e.s}`)
return null
}
if not mc_is_fn(e.s) { push(g_mc_fns, e.s) }
return e.s
}
# ---- the graph -----------------------------------------------------------------------------------------
function mc_graph(m: int) -> void {
let en = mc_enum_decl(g_mc_enum[m])
var t = 0
while t < len(g_mt_key) {
if g_mt_from[t] != null and rr_same(g_mt_from[t], g_mt_to[t]) and g_mt_guard[t] == null {
mc_row_err(m, t, `a transition from {g_mt_from[t]} to itself with no guard would be taken every time - give it a guard, or remove it`)
}
var u = 0
while u < t {
if g_mt_from[u] != null and rr_same(g_mt_from[u], g_mt_from[t]) and (g_mt_on[u] == g_mt_on[t]) and g_mt_guard[u] == null {
mc_row_err(m, t, `{g_mt_key[u]} and {g_mt_key[t]} both leave {g_mt_from[t]} {mc_trigger(g_mt_on[t])}, and {g_mt_key[u]} has no guard - {g_mt_key[t]} could never be taken; give {g_mt_key[u]} a guard`)
}
u += 1
}
t += 1
}
# reached from the start, and a way out of each
let reached = new []pointer
push(reached, g_mc_start[m])
var i = 0
while i < len(reached) {
t = 0
while t < len(g_mt_key) {
if g_mt_from[t] != null and g_mt_to[t] != null and (g_mt_from[t] == reached[i]) and not mc_in(reached, g_mt_to[t]) { push(reached, g_mt_to[t]) }
t += 1
}
i += 1
}
i = 0
while i < len(en.kids) {
let v = en.kids[i].s
if not mc_in(reached, v) { mc_err(m, `{en.s}.{v} is never reached from the start, {en.s}.{g_mc_start[m]} - no transition leads to it; add one, or remove the state`) }
var out = false
t = 0
while t < len(g_mt_key) {
if g_mt_from[t] != null and (g_mt_from[t] == v) { out = true }
t += 1
}
if not out { mc_err(m, `{en.s}.{v} has no way out - no transition leaves it; add one (from: {en.s}.{v})`) }
i += 1
}
}
function mc_trigger(on: pointer) -> pointer {
if len(on) == 0 { return "on the tick" }
return `on {on}`
}
function mc_in(xs: []pointer, s: pointer) -> bool {
var i = 0
while i < len(xs) {
if (xs[i] == s) { return true }
i += 1
}
return false
}

View file

@ -0,0 +1,226 @@
# 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
}
}

View file

@ -1071,6 +1071,7 @@ function parse_one_decl_in() -> void {
else if a == "Namespace" { eat_op("("); ns_name = eat_id(); eat_op(")") } # @Namespace(Name) package Foo.* namespace (#62)
else if a == "deterministic" { is_det = true }
else if a == "RowVerb" { is_rowverb = true }
else if a == "Machine" { mc_parse_attr() } # @Machine(Record.field) - 27.1, machines.ludic
else if (a == "AppendOnly") or (a == "ByKey") { at_decl(a) } # a registry's, for editors (attrs.ludic)
else if a == "PerMap" { # a map-scoped registry (permap.ludic)
at_decl(a)
@ -1103,6 +1104,7 @@ function parse_one_decl_in() -> void {
}
skipnl()
}
mc_attr_placed() # @Machine is a registry's (machines.ludic)
if is_id("friend") and (toks[pi + 1].text == "module") {
mod_parse_friend() # frontend/modules.ludic
return

View file

@ -93,6 +93,7 @@ function parse_registry() -> void {
push(g_rg_open, 0)
push(g_rg_from, from)
i18n_registry_declared() # i18n.ludic: its @TextKey, before the attributes are taken
mc_take(v, pm) # 27.1: @Machine(Record.field) (machines.ludic)
push(g_rg_at, at_registry_take()) # attrs.ludic
pm_declared(pm, chunk, v.s)
if from != null and not pm { res_read(v, from) }

View file

@ -94,7 +94,7 @@ function vocab_keywords() -> void {
vc_kw("statement", "in", "Separates a for's variables from what they range over; module m in layer L; reducer T in S.table on A.")
vc_kw("statement", "where", "A query's filter: for (p) in query [Pos] where p.x > 0.")
vc_kw("statement", "match", "Picks the arm whose values match.")
vc_kw("statement", "machine", "A state machine over a value, one block per state.")
vc_kw("statement", "machine", "A state machine over a value, one block per state (a machine that is data is a @Machine registry).")
vc_kw("statement", "become", "Moves a machine (or the program) to another state or scene.")
vc_kw("statement", "return", "Returns from a function.")
vc_kw("statement", "break", "Leaves the innermost loop.")
@ -220,6 +220,7 @@ function vocab_attributes() -> void {
vc_at("ByKey", "checked", "registry", "", "Entries are saved by key, so their order is free.")
vc_at("PerMap", "checked", "registry", "", "Its rows are read per map, from that map's directory, when the map loads.")
vc_at("Chunked", "checked", "registry", "(n)", "A @PerMap table read a chunk of n by n metres at a time.")
vc_at("Machine", "checked", "registry", "(Record.field)", "The registry is a state machine's transitions (from, to, on, guard, enter) over that enum field of a table's rows: the compiler writes its reducers and tick, and nothing else writes the field.")
vc_at("TextKey", "checked", "registry", "(\"prefix\")", "The prefix of its @Text fields' derived keys, in place of the registry's name in snake case.")
# a declaration's
vc_at("export", "checked", "declaration", "", "The declaration is exported (the same as the export keyword).")