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>
359 lines
10 KiB
Text
359 lines
10 KiB
Text
# check_stmt.ludic — L4: statements, and the pass itself. check_program runs between the parse and
|
|
# the emitter over everything the program wrote (not the runtime spliced in after it): every
|
|
# function, the entry, the tests, the globals' initializers and every @On listener with its
|
|
# event's fields in scope. ECS handler, query, spawn and machine bodies bind names the checker
|
|
# does not model, so they are left to the emitter.
|
|
function ck_block(b: Node) -> void {
|
|
if b == null { return }
|
|
if b.kind != N_BLOCK {
|
|
ck_stmt(b)
|
|
return
|
|
}
|
|
let m = ck_mark()
|
|
var i = 0
|
|
while i < len(b.kids) {
|
|
ck_stmt(b.kids[i])
|
|
i += 1
|
|
}
|
|
ck_pop(m)
|
|
}
|
|
function ck_target(t: Node) -> pointer {
|
|
if t.kind == E_ID { return ck_id(t) }
|
|
return ck_expr(t)
|
|
}
|
|
function ck_let(s: Node) -> void {
|
|
var t: pointer = "?"
|
|
ck_expect = s.ty
|
|
if s.a != null { t = ck_expr(s.a) }
|
|
if s.ty != null {
|
|
if s.a != null { ck_give(s.ty, t, s.a, s.s) }
|
|
ck_bind(s.s, s.ty)
|
|
if s.tps != null and (s.tps == "state") { # an entry point's state (state.ludic)
|
|
if not is_state_ty(s.ty) { ck_err("state", s, `an entry point takes states, and {s.s}: {s.ty} is not one`) }
|
|
ck_set_ro(s.uns == 0)
|
|
} else { ck_let_from(s.a, s.ty) }
|
|
return
|
|
}
|
|
ck_bind(s.s, t)
|
|
# a local bound to a read-only state, or to a reference out of one, is read-only too
|
|
if s.a != null and s.a.kind == E_ID {
|
|
let li = ck_local(s.a.s)
|
|
if li >= 0 and li < ck_top - 1 and ck_is_ro(li) { ck_ro_like(li) }
|
|
}
|
|
ck_let_from(s.a, t)
|
|
}
|
|
function ck_assign(s: Node) -> void {
|
|
# a local rebound is not a write through it
|
|
if not (s.a.kind == E_ID and ck_local(s.a.s) >= 0) { ck_write_check(s.a, "this assignment") }
|
|
let lt = ck_target(s.a)
|
|
ck_machine_write(s.a, "this assignment")
|
|
if (s.s == "=") { ck_expect = lt }
|
|
let rt = ck_expr(s.b)
|
|
var what: pointer = "this assignment"
|
|
if s.a.kind == E_ID { what = s.a.s }
|
|
if s.a.kind == E_MEMBER { what = `field {s.a.s}` }
|
|
if (s.s == "=") {
|
|
ck_give(lt, rt, s.b, what)
|
|
return
|
|
}
|
|
let op = s.s[0..1]
|
|
let res = ck_binop(s, op, lt, rt, s.a, s.b)
|
|
ck_give(lt, res, s.b, what)
|
|
}
|
|
# 27.1: a @Machine's field is its table's to change - a transition writes it, and nothing else does
|
|
function ck_machine_write(t: Node, what: pointer) -> void {
|
|
if t == null or t.kind != E_MEMBER or not (ck_mem_node == t) or ck_unknown(ck_mem_base) { return }
|
|
let m = mc_of_field(ck_mem_base, t.s)
|
|
if m < 0 or mc_is_gen(ck_fn_node) { return }
|
|
ck_err("machine", t, `{what}: {g_mc_rec[m]}.{t.s} is the machine {g_mc_reg[m]}'s (@Machine({g_mc_rec[m]}.{t.s})) - it changes only by a transition in its table; add a row to {g_mc_reg[m]}, or dispatch the action one names`)
|
|
}
|
|
function ck_match(s: Node) -> void {
|
|
ck_expr(s.a)
|
|
var i = 0
|
|
while i < len(s.kids) {
|
|
let arm = s.kids[i]
|
|
let m = ck_mark()
|
|
var p = 0
|
|
while p < len(arm.kids) {
|
|
let pat = arm.kids[p]
|
|
# `Door(n) =>` binds its payload's names
|
|
if pat.kind == E_CALL {
|
|
var q = 0
|
|
while q < len(pat.kids) {
|
|
if pat.kids[q].kind == E_ID { ck_bind(pat.kids[q].s, "?") }
|
|
q += 1
|
|
}
|
|
}
|
|
p += 1
|
|
}
|
|
ck_block(arm.a)
|
|
ck_pop(m)
|
|
i += 1
|
|
}
|
|
}
|
|
function ck_stmt(s: Node) -> void {
|
|
if s == null { return }
|
|
let k = s.kind
|
|
if k == N_BLOCK { ck_block(s); return }
|
|
if k == S_LET { ck_let(s); return }
|
|
if k == S_ASSIGN { ck_assign(s); return }
|
|
if k == S_IF {
|
|
ck_cond(s.a, ck_expr(s.a))
|
|
ck_block(s.b)
|
|
ck_block(s.c)
|
|
return
|
|
}
|
|
if k == S_WHILE {
|
|
ck_cond(s.a, ck_expr(s.a))
|
|
ck_block(s.b)
|
|
return
|
|
}
|
|
if k == S_FOR {
|
|
ck_expr(s.a)
|
|
ck_expr(s.b)
|
|
let m = ck_mark()
|
|
ck_bind(s.s, "int")
|
|
ck_block(s.c)
|
|
ck_pop(m)
|
|
return
|
|
}
|
|
if k == S_RETURN {
|
|
if s.a == null { return }
|
|
ck_expect = ck_ret
|
|
let t = ck_expr(s.a)
|
|
if (ck_ret != "void") { ck_give(ck_ret, t, s.a, "the result") }
|
|
return
|
|
}
|
|
if k == S_EXPR { ck_expr(s.a); return }
|
|
if k == S_MATCH { ck_match(s); return }
|
|
if k == S_EMIT { ck_emit(s); return }
|
|
if k == S_UNSAFE {
|
|
ck_unsafe_here(s)
|
|
ck_unsafe += 1
|
|
ck_block(s.a)
|
|
ck_unsafe -= 1
|
|
return
|
|
}
|
|
# a machine's store: its states' `become` writes it
|
|
if k == S_MACHINE {
|
|
if s.a != null and not (s.a.kind == E_INT) { ck_write_check(s.a, "a machine's become") }
|
|
if s.a != null and s.a.kind == E_MEMBER {
|
|
ck_expr(s.a)
|
|
ck_machine_write(s.a, "a machine block's become")
|
|
}
|
|
ck_walk(s)
|
|
return
|
|
}
|
|
# 0.S: a query's body, a spawn's fields, a machine's states - read for what they touch
|
|
if k == S_QUERY {
|
|
let m = ck_mark()
|
|
var t = 0
|
|
var i = 0
|
|
while i < len(s.kids) {
|
|
var ty: pointer = "?"
|
|
if s.c != null {
|
|
while t < len(s.c.kids) and s.c.kids[t].ival == 1 { t += 1 }
|
|
if t < len(s.c.kids) { ty = s.c.kids[t].s }
|
|
t += 1
|
|
}
|
|
ck_bind(s.kids[i].s, ty)
|
|
i += 1
|
|
}
|
|
ck_any(s.b)
|
|
ck_block(s.a)
|
|
ck_pop(m)
|
|
return
|
|
}
|
|
ck_walk(s)
|
|
}
|
|
# a name given to two parameters: the second would shadow the first, and nothing could reach it
|
|
function ck_param_twice(d: Node, i: int) -> void {
|
|
var j = 0
|
|
while j < i {
|
|
if d.kids[j].kind == N_PARAM and (d.kids[j].s == d.kids[i].s) {
|
|
ck_err("arity", d.kids[i], `{d.s} names two parameters {d.kids[i].s}`)
|
|
return
|
|
}
|
|
j += 1
|
|
}
|
|
}
|
|
function ck_fn_body(d: Node) -> void {
|
|
mg_enter(d, 0)
|
|
let m = ck_mark()
|
|
var i = 0
|
|
while i < len(d.kids) {
|
|
if d.kids[i].kind == N_PARAM {
|
|
ck_param_twice(d, i)
|
|
ck_bind_param(d.kids[i])
|
|
}
|
|
i += 1
|
|
}
|
|
ck_ret = d.ty
|
|
if ck_ret == null { ck_ret = "void" }
|
|
ck_fn_node = d
|
|
if d.uns == 1 {
|
|
ck_unsafe_here(d)
|
|
ck_unsafe += 1
|
|
}
|
|
ck_block(d.a)
|
|
if d.uns == 1 { ck_unsafe -= 1 }
|
|
ck_ret = "void"
|
|
ck_fn_node = null
|
|
ck_pop(m)
|
|
mg_leave()
|
|
}
|
|
function ck_listener(l: Node) -> void {
|
|
mg_enter(l, 4)
|
|
let ev = find_event(l.s)
|
|
ck_vis(ev, l.s, l)
|
|
let m = ck_mark()
|
|
if ev != null {
|
|
var i = 0
|
|
while i < len(ev.kids) {
|
|
ck_bind(ev.kids[i].s, ev.kids[i].ty)
|
|
i += 1
|
|
}
|
|
}
|
|
ck_ret = "void"
|
|
ck_block(l.a)
|
|
ck_pop(m)
|
|
mg_leave()
|
|
}
|
|
# a program's function named like one of the runtime's takes every call the runtime makes to its
|
|
# own - the runtime's ui_measure started answering to a package's - so the name is refused; one
|
|
# named like a compiler built-in never ran at all (every call to `run` was C's system()), so too
|
|
function ck_runtime_shadows() -> void {
|
|
var u = 0
|
|
while u < g_prog_user_end {
|
|
let d = prog[u]
|
|
if d.kind == N_FN and (is_intrinsic(d.s) or is_intrinsic2(d.s) or is_call_builtin(d.s)) { ck_err("shadow", d, `{d.s} is a built-in of the compiler, and a call to it would never reach this function; choose another name`) }
|
|
u += 1
|
|
}
|
|
var r = g_prog_user_end
|
|
while r < len(prog) {
|
|
let d = prog[r]
|
|
if d.kind == N_FN {
|
|
let u = ck_fn(d.s)
|
|
if u != null and u != d { ck_err("shadow", u, `{d.s} is also a function of the engine runtime ({d.file}); choose another name`) }
|
|
}
|
|
# a type the runtime declares - `property PadButton` beside its `enum PadButton` compiled,
|
|
# and the two layouts met in the IR
|
|
if ck_type_kind(d) != "" {
|
|
var k = 0
|
|
while k < g_prog_user_end and k < len(prog) {
|
|
let o = prog[k]
|
|
if ck_type_kind(o) != "" and (o.s == d.s) {
|
|
ck_err("shadow", o, `{d.s} is the runtime's {ck_type_kind(d)} ({d.file}); choose another name for this {ck_type_kind(o)}`)
|
|
}
|
|
k += 1
|
|
}
|
|
}
|
|
r += 1
|
|
}
|
|
}
|
|
function ck_type_kind(d: Node) -> pointer {
|
|
if d.kind == N_ENUM { return "enum" }
|
|
if d.kind == N_COMP and d.uns == 2 { return "state" }
|
|
if d.kind == N_COMP { return "property" }
|
|
if d.kind == N_STRUCT { return "record" }
|
|
if d.kind == N_EVENT { return "event" }
|
|
return ""
|
|
}
|
|
function ck_hooks(hs: []Node) -> void {
|
|
var i = 0
|
|
while i < len(hs) {
|
|
mg_enter(hs[i], 4)
|
|
ck_block(hs[i].a)
|
|
mg_leave()
|
|
i += 1
|
|
}
|
|
}
|
|
function ck_scene_block(b: Node) -> void {
|
|
if b == null { return }
|
|
mg_enter(b, 5)
|
|
ck_block(b)
|
|
mg_leave()
|
|
}
|
|
# a retained ui widget (N_UI, whose number is also S_IF's): its props and its children
|
|
function ck_widget(w: Node) -> void {
|
|
if w == null { return }
|
|
var i = 0
|
|
while w.b != null and i < len(w.b.kids) {
|
|
ck_expr(w.b.kids[i].a)
|
|
i += 1
|
|
}
|
|
i = 0
|
|
while i < len(w.kids) {
|
|
ck_widget(w.kids[i])
|
|
i += 1
|
|
}
|
|
}
|
|
# the pass also makes the generics real (check_gen.ludic), so it always runs
|
|
function check_program() -> void {
|
|
gen_collect()
|
|
ck_index()
|
|
ck_runtime_shadows()
|
|
ck_tynames() # 0.R4: a type written names a type
|
|
var i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_FN { ck_fn_body(d) }
|
|
if d.kind == N_MAIN or d.kind == N_TEST {
|
|
mg_enter(d, 1)
|
|
ck_block(d.a)
|
|
mg_leave()
|
|
}
|
|
if d.kind == N_UI and d.ival == 1 and g_migrate { # 0.S2: a ui block's props, for what they read
|
|
g_mg_cur = -3
|
|
ck_widget(d.a)
|
|
g_mg_cur = -1
|
|
}
|
|
if d.kind == N_SYS and g_migrate { # a handler's body: only the migration reads it here
|
|
mg_enter(d, 2)
|
|
ck_block(d.a)
|
|
mg_leave()
|
|
}
|
|
if (d.kind == N_VAR or d.kind == N_CONST) and d.a != null {
|
|
ck_vis_file = d.file
|
|
ck_vis_skip = d.kind == N_VAR and is_port_var(d)
|
|
ck_give(d.ty, ck_expr(d.a), d.a, d.s)
|
|
ck_vis_file = null
|
|
ck_vis_skip = false
|
|
}
|
|
i += 1
|
|
}
|
|
var j = 0
|
|
while j < len(g_onlisten) {
|
|
ck_listener(g_onlisten[j])
|
|
j += 1
|
|
}
|
|
if g_migrate { # 0.S2: the hooks' bodies and the scenes', for what they touch
|
|
ck_hooks(g_onspawn)
|
|
ck_hooks(g_ondespawn)
|
|
ck_hooks(g_onattach)
|
|
ck_hooks(g_ondetach)
|
|
ck_hooks(g_onenable)
|
|
ck_hooks(g_ondisable)
|
|
j = 0
|
|
while j < len(g_scenes) {
|
|
ck_scene_block(g_scenes[j].a)
|
|
ck_scene_block(g_scenes[j].b)
|
|
j += 1
|
|
}
|
|
}
|
|
# a test block is checked like `entry`: that is also what makes its generic calls real
|
|
j = 0
|
|
while j < len(g_tests) {
|
|
mg_enter(g_tests[j], 3)
|
|
ck_block(g_tests[j].a)
|
|
mg_leave()
|
|
j += 1
|
|
}
|
|
gen_finish()
|
|
if ck_errors > 0 and not ck_reporting() and not g_migrate {
|
|
if g_diag_json { diag_exit(1) }
|
|
let m = `{itoa(ck_errors)} type error(s)\n`
|
|
file_write(file_stderr(), m, len(m))
|
|
exit(1)
|
|
}
|
|
}
|