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:
parent
a637aec63e
commit
e4265f5fdb
33 changed files with 131217 additions and 109412 deletions
76
LANGUAGE.md
76
LANGUAGE.md
|
|
@ -443,6 +443,78 @@ program Herds {
|
|||
- **Dice** a row reducer rolls come from the row's own `Rng` or its owner's, never `Random.*`: it
|
||||
runs in the drain, outside its owner's tick, and the world's stream is co-op's shared order.
|
||||
|
||||
**A machine as data** (27.1). A row's state - an animal idling, fleeing, drinking - is an enum field
|
||||
of its record, and the machine that moves it is a registry marked `@Machine(Record.field)`: one row
|
||||
a transition, which the compiler turns into the reducers and the tick. The table is the whole
|
||||
machine, and a studio edits it as a graph.
|
||||
|
||||
```ludic
|
||||
import "ludic.base"
|
||||
program Moods {
|
||||
enum Mood { Calm, Wary, Fled }
|
||||
property Deer {
|
||||
mood: Mood = Mood.Calm # the start: the field's default
|
||||
fear: int = 0
|
||||
}
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = "" # an action's name, or "" for a transition the tick asks
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Spook", enter: fn deer_startle }
|
||||
def DeerSteps bolts { from: Mood.Wary, to: Mood.Fled, on: "Spook", guard: fn deer_afraid }
|
||||
def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_settled }
|
||||
def DeerSteps home { from: Mood.Fled, to: Mood.Calm }
|
||||
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear > 1 }
|
||||
function deer_settled(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear == 0 }
|
||||
function deer_startle(r: mut Row<Deer>, h: Herd) -> void { r.rec.fear += 1 }
|
||||
entry (h: mut Herd, m: mut DeerStepsMachine) {
|
||||
h.deer = table_new(0, 0)
|
||||
dispatch Spook { who: tb_add(h.deer, new Deer) }
|
||||
drain_actions()
|
||||
deer_steps_tick(m, h)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **The registry's record** has `from` and `to` (variants of the field's enum), `on: string` (an
|
||||
action's name, `""` for a transition the tick asks), and may have `guard: fn(Row<Record>, reads...)
|
||||
-> bool` and `enter: fn(Row<Record>, reads...) -> void`, each taking the row first and then the
|
||||
states it reads. Its rows may live in an `.lres` (`from "deer_steps.lres"`), where a value is
|
||||
written as in code: `bolts { from: Mood.Wary, to: Mood.Fled, on: "Spook", guard: fn deer_afraid }`.
|
||||
The states are the enum's variants; the record is held in one state's `Table<Record>`, and the
|
||||
registry is that state's module's.
|
||||
- **What the compiler writes.** For each action an `on` names (it must have a `@Target`), a row
|
||||
reducer: the row's current state, the first transition from it on that action - in the table's
|
||||
order - whose guard passes (no guard passes always), the field set, then `enter`. When a row
|
||||
leaves a state on a guard alone, `state DeerStepsMachine` (the tick's kept row view) and
|
||||
`deer_steps_tick(m: mut DeerStepsMachine, s: mut Herd, reads...)`, which takes every row of the
|
||||
table through the same first match, one transition a row a tick; the program calls it from its
|
||||
system. A program's own row reducer on the same action runs before the machine's, so an enter
|
||||
that needs the action's payload finds it on the row. Nothing is allocated: the row views are kept.
|
||||
- **The table is the whole machine.** Its field is written by nothing else - an assignment or a
|
||||
`machine` block's `become` anywhere else is refused: `this assignment: Deer.mood is the machine
|
||||
DeerSteps's (@Machine(Deer.mood)) - it changes only by a transition in its table`. A new row takes
|
||||
its state in its `new` (a save's load does too). A guard and an enter are functions of the
|
||||
record's module and keep a row reducer's rules - the row reaches `r.rec` and `r.h`, goes 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 graph is checked**, each an error at its row: a state never reached from the start; a state
|
||||
with no way out; an `on` naming no action, or an action naming no row; a transition from a state
|
||||
to itself with no guard; and two ways out of one state on one trigger behind an unguarded first
|
||||
(`settles and stays both leave Wary on Spook, and settles has no guard - stays could never be
|
||||
taken`).
|
||||
- `ludic schema`'s code section lists each machine (`machines`: `registry`, `record`, `field`,
|
||||
`enum`, `table`, `start`, `states`, `actions`, `tick`, `module`, `at`), and `ludic deps` names its
|
||||
reducers `reducer Deer in Herd.deer on Spook (machine DeerSteps)`. The `machine` block stays for
|
||||
a machine that is only code.
|
||||
|
||||
`ludic deps` reports the widest function - the most states any function or entry point of the
|
||||
program's own takes - and `--check` holds it as a ratchet like its other numbers
|
||||
(`widest_function 12` in the baseline file). A function value's states are supplied where it is called, so a
|
||||
|
|
@ -2293,7 +2365,9 @@ if hero.mode == HeroState.Rolling { … } # readable wherever
|
|||
```
|
||||
|
||||
A machine's store is a state's field (`machine hero.mode`), a local, or a register index; a
|
||||
`become` writes it, so the function needs its state `mut`. A state that names no variant of the
|
||||
`become` writes it, so the function needs its state `mut`. (A machine that is data - a table of transitions the
|
||||
studio edits, over a field of a table's rows - is a `@Machine` registry: "A machine as data", above;
|
||||
its field is refused to a `machine` block.) A state that names no variant of the
|
||||
store's enum is a compile error. A bare (payload-free) enum is an `int`-sized type wherever a type
|
||||
is written — a `var`, a parameter, a field, a return.
|
||||
|
||||
|
|
|
|||
35
changes/machines.md
Normal file
35
changes/machines.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A state machine as data: `@Machine(Record.field)`** (phase 27.1). 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. Its rows are data - an `.lres` or `def`s - so the studio edits the machine as a graph.
|
||||
|
||||
- **The compiler writes the machine.** For each action an `on` names, a row reducer in the
|
||||
registry's file (`reducer Deer in Herd.deer on Spook`, beside any the program writes, after them):
|
||||
the row's state, the first transition from it on that action whose guard passes, the field set,
|
||||
`enter` run. 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, called from the program's system. Guards and enters are called by name; nothing allocates.
|
||||
- **The table is the whole machine.** The field is written by nothing else: an assignment to it or
|
||||
a `machine` block's `become` over it outside the written code is a type error (`Deer.mood is the
|
||||
machine DeerSteps's (@Machine(Deer.mood)) - it changes only by a transition in its table`); a new
|
||||
row takes its state in its `new`. A guard and an enter take the row first, are the record's
|
||||
module's, and keep a row reducer's rules (`r.rec` and `r.h` only, a `@Column` field only through a
|
||||
`@RowVerb`); a guard writes nothing through its row.
|
||||
- **The graph is checked**, each an error naming its row: 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, and two ways out of a state on one trigger behind an unguarded first. Also refused:
|
||||
`@Machine` on anything but 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, and 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`); the registry carries `@Machine` among its
|
||||
attributes. `ludic deps` names a machine's reducer `reducer Deer in Herd.deer on Spook (machine
|
||||
DeerSteps)`. vocab: `@Machine`; docs `annot-machine`; LANGUAGE.md "A machine as data".
|
||||
- Examples `actions/machine` (with `deer_steps.lres`); rejects for an unreachable state, no way out,
|
||||
an unknown action, an action with no row, a self-transition with no guard, an ambiguous trigger, a
|
||||
guard that writes, the field written by hand, `@Machine` off a registry and a field that is no
|
||||
enum. The `machine` block stays for a machine that is only code.
|
||||
24
docs/language/annotations/annot-machine.md
Normal file
24
docs/language/annotations/annot-machine.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: annot-machine
|
||||
name: @Machine
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Machine
|
||||
sig: @Machine(Deer.mood) registry DeerSteps of DeerStep from "deer_steps.lres"
|
||||
tip: A registry of a state machine's transitions over an enum field of a table's rows: the compiler writes its reducers and tick.
|
||||
order: 84
|
||||
---
|
||||
|
||||
<code>@Machine(Record.field)</code> marks a registry as the transitions of a state machine over that enum field of the records a state's <code>Table<Record></code> holds. The registry's record has <code>from</code> and <code>to</code> (the enum's variants), <code>on</code> (an action's name, or <code>""</code> for a transition the tick asks), and a <code>guard</code> and an <code>enter</code> taking the row first. The start is the field's default. The compiler writes a row reducer for each action an <code>on</code> names and, for the guard-only transitions, <code><registry>_tick</code>; each takes the first transition from the row's state whose guard passes, sets the field and runs <code>enter</code>. Nothing else writes the field. A state never reached, a state with no way out, an <code>on</code> naming no action (or one with no <code>@Target</code>), a self-transition with no guard and a second way out behind an unguarded first on one trigger are compile errors.
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — a fragment: the record, the table's state and the functions are elsewhere
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
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"
|
||||
```
|
||||
|
|
@ -9,7 +9,7 @@ tip: An explicit state machine over an int var — dispatches on the store's val
|
|||
order: 6
|
||||
---
|
||||
|
||||
A <code>machine</code> turns an integer store into an explicit state machine, replacing brittle `if phase == N` chains. It reads the store — a named program-scope `var` is the modern choice — and dispatches to the matching `state` block; inside a state, `become Name` transitions to another state of the same machine. States number themselves by declaration order (the first is `0`, the next `1`, and so on), so you never write magic constants, though `state Name = expr` is accepted when a state needs a specific value. Because `become` compiles to a single store back into the `var`, the whole machine lowers to plain branches with no dispatch table. Place a `machine` inside a handler so it runs each frame.
|
||||
A <code>machine</code> turns an integer store into an explicit state machine, replacing brittle `if phase == N` chains. It reads the store — a named program-scope `var` is the modern choice — and dispatches to the matching `state` block; inside a state, `become Name` transitions to another state of the same machine. States number themselves by declaration order (the first is `0`, the next `1`, and so on), so you never write magic constants, though `state Name = expr` is accepted when a state needs a specific value. Because `become` compiles to a single store back into the `var`, the whole machine lowers to plain branches with no dispatch table. Place a `machine` inside a handler so it runs each frame. A machine that is data - a table of transitions over an enum field of a table's rows, which the studio edits as a graph - is a <code>@Machine</code> registry instead, and its field is refused to a <code>machine</code> block.
|
||||
|
||||
```ludic
|
||||
program TurnOrder {
|
||||
|
|
|
|||
6
examples/actions/deer_steps.lres
Normal file
6
examples/actions/deer_steps.lres
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# deer_steps.lres - Machines' DeerSteps: how a deer's mood changes (examples/actions/machine.ludic)
|
||||
startled { from: Mood.Calm, to: Mood.Wary, on: "Spook", enter: fn deer_startle }
|
||||
bolts { from: Mood.Wary, to: Mood.Fled, on: "Spook", guard: fn deer_afraid, enter: fn deer_bolt }
|
||||
uneasy { from: Mood.Wary, to: Mood.Wary, on: "Spook", guard: fn deer_calm_enough, enter: fn deer_startle }
|
||||
settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_settled }
|
||||
home { from: Mood.Fled, to: Mood.Calm, guard: fn deer_away, enter: fn deer_rest }
|
||||
53
examples/actions/machine.ludic
Normal file
53
examples/actions/machine.ludic
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# machine.ludic - 27.1: a state machine as data. Deer.mood is the state, and the registry marked
|
||||
# @Machine(Deer.mood) is its transitions, one row each in deer_steps.lres: from, to, the action it
|
||||
# is `on` ("" for one the tick asks), a guard and an enter. The compiler writes a row reducer for
|
||||
# each action a row names and deer_steps_tick for the rest; nothing else writes Deer.mood.
|
||||
#
|
||||
# Running it prints: 0 0 1 1
|
||||
import "ludic.base"
|
||||
program Machines {
|
||||
numbers float
|
||||
enum Mood { Calm, Wary, Fled }
|
||||
property Deer {
|
||||
mood: Mood = Mood.Calm # the start
|
||||
fear: int = 0
|
||||
x: float = 0.0
|
||||
}
|
||||
state Herd {
|
||||
deer: Table<Deer> = null
|
||||
far: float = 10.0
|
||||
}
|
||||
action Spook { @Target who: int = -1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep as DS from "deer_steps.lres"
|
||||
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear >= 2 }
|
||||
function deer_calm_enough(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear < 2 }
|
||||
function deer_settled(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear == 0 }
|
||||
function deer_away(r: Row<Deer>, h: Herd) -> bool { return r.rec.x > h.far }
|
||||
function deer_startle(r: mut Row<Deer>, h: Herd) -> void { r.rec.fear += 1 }
|
||||
function deer_bolt(r: mut Row<Deer>, h: Herd) -> void { r.rec.x += 20.0 }
|
||||
function deer_rest(r: mut Row<Deer>, h: Herd) -> void { r.rec.fear = 0 }
|
||||
|
||||
entry (h: mut Herd, m: mut DeerStepsMachine) {
|
||||
let tb: Table<Deer> = table_new(0, 0)
|
||||
h.deer = tb
|
||||
let a = tb_add(tb, new Deer)
|
||||
let b = tb_add(tb, new Deer)
|
||||
dispatch Spook { who: a } # Calm -> Wary, fear 1
|
||||
dispatch Spook { who: a } # Wary, not afraid yet: stays, fear 2
|
||||
dispatch Spook { who: a } # afraid: Wary -> Fled, 20 m off
|
||||
dispatch Spook { who: b } # Calm -> Wary, fear 1
|
||||
drain_actions()
|
||||
deer_steps_tick(m, h) # a is far enough: Fled -> Calm, fear 0; b is still uneasy
|
||||
let ra = tb_rec(tb, tb_row(tb, a))
|
||||
let rb = tb_rec(tb, tb_row(tb, b))
|
||||
print(`{int(ra.mood)} {ra.fear} {int(rb.mood)} {rb.fear}`)
|
||||
}
|
||||
}
|
||||
22
examples/rejected/machine_ambiguous.ludic
Normal file
22
examples/rejected/machine_ambiguous.ludic
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# 27.1: two ways out of a state on one trigger: the first has a guard
|
||||
import "ludic.base"
|
||||
program MachineAmbiguous {
|
||||
enum Mood { Calm, Wary }
|
||||
property Deer { mood: Mood = Mood.Calm, fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
action Shout { loud: int = 1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear > 1 }
|
||||
def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Spook" }
|
||||
def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, on: "Spook" }
|
||||
def DeerSteps stays { from: Mood.Wary, to: Mood.Calm, on: "Spook", guard: fn deer_afraid }
|
||||
entry { print(1) }
|
||||
}
|
||||
25
examples/rejected/machine_field_written.ludic
Normal file
25
examples/rejected/machine_field_written.ludic
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# 27.1: a machine's field changes only by a transition in its table
|
||||
import "ludic.base"
|
||||
program MachineFieldWritten {
|
||||
enum Mood { Calm, Wary }
|
||||
property Deer { mood: Mood = Mood.Calm, fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
action Shout { loud: int = 1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear > 1 }
|
||||
def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Spook" }
|
||||
def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_afraid }
|
||||
function calm_all(h: mut Herd) -> void {
|
||||
let d = tb_rec(h.deer, 0)
|
||||
d.mood = Mood.Calm
|
||||
}
|
||||
entry { print(1) }
|
||||
}
|
||||
25
examples/rejected/machine_guard_writes.ludic
Normal file
25
examples/rejected/machine_guard_writes.ludic
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# 27.1: a guard asks and changes nothing
|
||||
import "ludic.base"
|
||||
program MachineGuardWrites {
|
||||
enum Mood { Calm, Wary }
|
||||
property Deer { mood: Mood = Mood.Calm, fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
action Shout { loud: int = 1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear > 1 }
|
||||
def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Spook", guard: fn deer_nervous }
|
||||
def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_afraid }
|
||||
function deer_nervous(r: Row<Deer>, h: Herd) -> bool {
|
||||
r.rec.fear += 1
|
||||
return true
|
||||
}
|
||||
entry { print(1) }
|
||||
}
|
||||
22
examples/rejected/machine_no_way_out.ludic
Normal file
22
examples/rejected/machine_no_way_out.ludic
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# 27.1: every state of a machine has a way out
|
||||
import "ludic.base"
|
||||
program MachineNoWayOut {
|
||||
enum Mood { Calm, Wary, Fled }
|
||||
property Deer { mood: Mood = Mood.Calm, fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
action Shout { loud: int = 1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear > 1 }
|
||||
def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Spook" }
|
||||
def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_afraid }
|
||||
def DeerSteps bolts { from: Mood.Wary, to: Mood.Fled, on: "Spook", guard: fn deer_afraid }
|
||||
entry { print(1) }
|
||||
}
|
||||
10
examples/rejected/machine_not_enum.ludic
Normal file
10
examples/rejected/machine_not_enum.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# 27.1: a machine's state is a plain enum field with a start
|
||||
import "ludic.base"
|
||||
program MachineNotEnum {
|
||||
property Deer { mood: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
property DeerStep { from: int = 0, to: int = 0, on: string = "" }
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
def DeerSteps a { from: 0, to: 1 }
|
||||
entry { print(1) }
|
||||
}
|
||||
8
examples/rejected/machine_not_registry.ludic
Normal file
8
examples/rejected/machine_not_registry.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# 27.1: @Machine marks a registry of transitions
|
||||
import "ludic.base"
|
||||
program MachineNotRegistry {
|
||||
enum Mood { Calm, Wary }
|
||||
property Deer { mood: Mood = Mood.Calm }
|
||||
@Machine(Deer.mood) property DeerStep { from: Mood = Mood.Calm }
|
||||
entry { print(1) }
|
||||
}
|
||||
21
examples/rejected/machine_on_no_target.ludic
Normal file
21
examples/rejected/machine_on_no_target.ludic
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# 27.1: a transition's action names the row it moves (@Target)
|
||||
import "ludic.base"
|
||||
program MachineOnNoTarget {
|
||||
enum Mood { Calm, Wary }
|
||||
property Deer { mood: Mood = Mood.Calm, fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
action Shout { loud: int = 1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear > 1 }
|
||||
def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Shout" }
|
||||
def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_afraid }
|
||||
entry { print(1) }
|
||||
}
|
||||
21
examples/rejected/machine_on_unknown.ludic
Normal file
21
examples/rejected/machine_on_unknown.ludic
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# 27.1: a transition's `on` names an action
|
||||
import "ludic.base"
|
||||
program MachineOnUnknown {
|
||||
enum Mood { Calm, Wary }
|
||||
property Deer { mood: Mood = Mood.Calm, fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
action Shout { loud: int = 1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear > 1 }
|
||||
def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Spok" }
|
||||
def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_afraid }
|
||||
entry { print(1) }
|
||||
}
|
||||
22
examples/rejected/machine_self_no_guard.ludic
Normal file
22
examples/rejected/machine_self_no_guard.ludic
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# 27.1: a transition from a state to itself has a guard
|
||||
import "ludic.base"
|
||||
program MachineSelfNoGuard {
|
||||
enum Mood { Calm, Wary }
|
||||
property Deer { mood: Mood = Mood.Calm, fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
action Shout { loud: int = 1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear > 1 }
|
||||
def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Spook" }
|
||||
def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_afraid }
|
||||
def DeerSteps again { from: Mood.Wary, to: Mood.Wary, on: "Spook" }
|
||||
entry { print(1) }
|
||||
}
|
||||
22
examples/rejected/machine_unreachable.ludic
Normal file
22
examples/rejected/machine_unreachable.ludic
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# 27.1: every state of a machine is reached from its start
|
||||
import "ludic.base"
|
||||
program MachineUnreachable {
|
||||
enum Mood { Calm, Wary, Fled }
|
||||
property Deer { mood: Mood = Mood.Calm, fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
action Shout { loud: int = 1 }
|
||||
property DeerStep {
|
||||
from: Mood = Mood.Calm
|
||||
to: Mood = Mood.Calm
|
||||
on: string = ""
|
||||
guard: fn(Row<Deer>, Herd) -> bool = null
|
||||
enter: fn(Row<Deer>, Herd) -> void = null
|
||||
}
|
||||
@Machine(Deer.mood) registry DeerSteps of DeerStep
|
||||
function deer_afraid(r: Row<Deer>, h: Herd) -> bool { return r.rec.fear > 1 }
|
||||
def DeerSteps startled { from: Mood.Calm, to: Mood.Wary, on: "Spook" }
|
||||
def DeerSteps settles { from: Mood.Wary, to: Mood.Calm, guard: fn deer_afraid }
|
||||
def DeerSteps home { from: Mood.Fled, to: Mood.Calm, guard: fn deer_afraid }
|
||||
entry { print(1) }
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
# 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)
|
||||
|
|
@ -74,6 +76,8 @@ function sc_code(b: Buf) -> void {
|
|||
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\": [")
|
||||
|
|
@ -318,6 +322,60 @@ function cx_row_reducers(b: Buf) -> void {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
var ck_names: []pointer = new []pointer
|
||||
var ck_tys: []pointer = new []pointer
|
||||
var ck_ret: pointer = "void" # the result type of the function being checked
|
||||
var ck_fn_node: Node = null # the function being checked (27.1: a machine's own may set its field)
|
||||
var ck_mem_node: Node = null # the member expression ck_member typed last, and its base's type
|
||||
var ck_mem_base: pointer = null
|
||||
var ck_errors: int = 0
|
||||
var ck_report: int = -1 # LUDIC_CHECK_REPORT=1: list every mix-up by category, fail nothing
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,8 @@ function ck_member(e: Node) -> pointer {
|
|||
return "?"
|
||||
}
|
||||
let bt = ck_expr(b)
|
||||
ck_mem_node = e # 27.1: what a write to it writes into (ck_machine_write)
|
||||
ck_mem_base = bt
|
||||
if ck_unknown(bt) { return "?" }
|
||||
let r = ck_record(bt)
|
||||
if r != null {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ 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"
|
||||
|
|
@ -59,6 +60,13 @@ function ck_assign(s: Node) -> void {
|
|||
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
|
||||
|
|
@ -129,6 +137,10 @@ function ck_stmt(s: Node) -> void {
|
|||
# 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
|
||||
}
|
||||
|
|
@ -178,6 +190,7 @@ function ck_fn_body(d: Node) -> void {
|
|||
}
|
||||
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
|
||||
|
|
@ -185,6 +198,7 @@ function ck_fn_body(d: Node) -> void {
|
|||
ck_block(d.a)
|
||||
if d.uns == 1 { ck_unsafe -= 1 }
|
||||
ck_ret = "void"
|
||||
ck_fn_node = null
|
||||
ck_pop(m)
|
||||
mg_leave()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
|
|
|||
431
selfhost/frontend/machines.ludic
Normal file
431
selfhost/frontend/machines.ludic
Normal 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
|
||||
}
|
||||
226
selfhost/frontend/machines_write.ludic
Normal file
226
selfhost/frontend/machines_write.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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).")
|
||||
|
|
|
|||
114925
selfhost/ludicc.seed.ll
114925
selfhost/ludicc.seed.ll
File diff suppressed because one or more lines are too long
124513
selfhost/ludicc.win.seed.ll
124513
selfhost/ludicc.win.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -41,6 +41,7 @@
|
|||
"annot-target",
|
||||
"annot-column",
|
||||
"annot-rowverb",
|
||||
"annot-machine",
|
||||
"annot-permap",
|
||||
"annot-range",
|
||||
"annot-ref",
|
||||
|
|
|
|||
|
|
@ -269,8 +269,20 @@ function dp_fn_name(n: pointer) -> pointer {
|
|||
}
|
||||
return n
|
||||
}
|
||||
# 27.3: a row reducer's symbol says `Deer__in__Herd__deer` for `Deer in Herd.deer`
|
||||
function dp_row_name(s: pointer) -> pointer {
|
||||
# 27.3: a row reducer's symbol says `Deer__in__Herd__deer` for `Deer in Herd.deer`; one a @Machine
|
||||
# wrote ends `__machine__DeerSteps` (27.1)
|
||||
function dp_row_name(s0: pointer) -> pointer {
|
||||
var s = s0
|
||||
var tail = ""
|
||||
var k = 0
|
||||
while k + 11 <= slen(s0) {
|
||||
if s0[k .. k + 11] == "__machine__" {
|
||||
s = s0[0 .. k]
|
||||
tail = ` (machine {s0[k + 11 .. slen(s0)]})`
|
||||
break
|
||||
}
|
||||
k += 1
|
||||
}
|
||||
var out = ""
|
||||
var seen = false
|
||||
var i = 0
|
||||
|
|
@ -287,7 +299,7 @@ function dp_row_name(s: pointer) -> pointer {
|
|||
i += 1
|
||||
}
|
||||
}
|
||||
return out
|
||||
return out + tail
|
||||
}
|
||||
function dp_own(k: int) -> bool { return dp_pkg[k] == 0 }
|
||||
# an edge the numbers count: between two of the program's own modules
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/state.ludic")
|
||||
push(f, "selfhost/frontend/actions.ludic")
|
||||
push(f, "selfhost/frontend/actions_rows.ludic")
|
||||
push(f, "selfhost/frontend/machines.ludic")
|
||||
push(f, "selfhost/frontend/machines_write.ludic")
|
||||
push(f, "selfhost/frontend/migrate.ludic")
|
||||
push(f, "selfhost/frontend/registry.ludic")
|
||||
push(f, "selfhost/frontend/registry_finish.ludic")
|
||||
|
|
|
|||
|
|
@ -1252,6 +1252,17 @@ function cmd_dev_test() -> int {
|
|||
reject_case("rejected/row_reducer_writes_state", "a row reducer writes its row alone, and n is a mut Noise", "a row reducer writes no state")
|
||||
reject_case("rejected/rowverb_no_row", "@RowVerb scare: a row verb takes the row first", "a @RowVerb takes its row first")
|
||||
reject_case("rejected/row_reducer_owner", "Herd is module herd's, and this reducer is in the program", "only the state's module reduces its table's rows")
|
||||
feat_case("actions/machine", "", "0 0 1 1", "machine.ludic (27.1: @Machine - a state machine as data: the reducer on Spook, first match by guard, enter, and the tick's guard-only transitions)")
|
||||
reject_case("rejected/machine_unreachable", "Mood.Fled is never reached from the start, Mood.Calm", "every state of a machine is reached from its start")
|
||||
reject_case("rejected/machine_no_way_out", "Mood.Fled has no way out", "every state of a machine has a way out")
|
||||
reject_case("rejected/machine_on_unknown", "on: \"Spok\" names no action", "a machine's on names an action")
|
||||
reject_case("rejected/machine_on_no_target", "on: \"Shout\" names no row", "a machine's action names its row with @Target")
|
||||
reject_case("rejected/machine_self_no_guard", "a transition from Wary to itself with no guard", "a self-transition has a guard")
|
||||
reject_case("rejected/machine_ambiguous", "settles and stays both leave Wary on Spook, and settles has no guard", "one trigger, one unguarded way out")
|
||||
reject_case("rejected/machine_guard_writes", "guard deer_nervous asks and changes nothing", "a machine's guard writes nothing through its row")
|
||||
reject_case("rejected/machine_field_written", "Deer.mood is the machine DeerSteps's (@Machine(Deer.mood))", "a machine's field changes only by a transition in its table")
|
||||
reject_case("rejected/machine_not_registry", "@Machine(Deer.mood) marks the registry of a machine's transitions", "@Machine is on a registry")
|
||||
reject_case("rejected/machine_not_enum", "Deer.mood is int - a machine's state is a plain enum", "a machine's state is an enum field")
|
||||
feat_case("state/counter", "", "11 3 heard 5", "counter.ludic (0.S: a state, mut and read-only parameters, entry, listener and fn-value injection)")
|
||||
feat_case("state/port_field", "", "40", "port_field.ludic (0.S: a port member bound to a state's field)")
|
||||
spec_case("state/tested", "== 2 passed, 0 failed ==")
|
||||
|
|
@ -1373,6 +1384,7 @@ function cmd_dev_test() -> int {
|
|||
schema_case("actions/modules", "{\"action\": \"PickUp\", \"module\": null, \"at\": \"examples/actions/modules.ludic:8:21\"}", "--emit-schema: code.dispatch - every dispatch site")
|
||||
schema_case("actions/rows", "{\"record\": \"Deer\", \"table\": \"Herd.deer\", \"state\": \"Herd\", \"action\": \"Spook\", \"target\": \"who\", \"predicted\": false, \"net\": null, \"module\": null, \"at\": \"examples/actions/rows.ludic:17:32\"}", "--emit-schema: code.row_reducers - a row reducer's record, table, action and target")
|
||||
schema_case("actions/rows", "{\"name\": \"deer_move\", \"record\": \"Deer\", \"module\": null, \"at\": \"examples/actions/rows.ludic:24:21\"}", "--emit-schema: code.row_verbs - a @RowVerb and its record")
|
||||
schema_case("actions/machine", "{\"registry\": \"DeerSteps\", \"record\": \"Deer\", \"field\": \"mood\", \"enum\": \"Mood\", \"table\": \"Herd.deer\", \"start\": \"Calm\", \"states\": [\"Calm\", \"Wary\", \"Fled\"], \"actions\": [\"Spook\"], \"tick\": \"deer_steps_tick\", \"module\": null, \"at\": \"examples/actions/machine.ludic:28:32\"}", "--emit-schema: code.machines - a @Machine's registry, field, enum, table, start, actions and tick")
|
||||
schema_case("actions/rows", "{\"name\": \"Spook\", \"module\": null, \"at\": \"examples/actions/rows.ludic:14:10\", \"target\": \"who\"}", "--emit-schema: code.actions - an action's @Target field")
|
||||
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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue