Merge lang/machines e4265f5f: phase 27.1 - a state machine as data (@Machine(Record.field) on a registry of transitions: generated row reducers and tick, the field written only by its table, graph checks, schema machines); reseeded, syntax regenerated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d7adc28125
34 changed files with 131221 additions and 109416 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
|
- **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.
|
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
|
`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
|
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
|
(`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
|
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
|
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.
|
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
|
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
|
```ludic
|
||||||
program TurnOrder {
|
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,
|
# row_reducers record, table (State.path), state, action, target (its @Target field), predicted,
|
||||||
# net, module, at row_verbs name, record, module, at (27.3)
|
# net, module, at row_verbs name, record, module, at (27.3)
|
||||||
# actions also carry their target: the @Target field, or null
|
# 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
|
# events name, module, at, cancellable, net (@ToServer / @ToClients), emits (sites) and
|
||||||
# listeners (the @On handlers: handler, module, at)
|
# listeners (the @On handlers: handler, module, at)
|
||||||
# ports name, module, at, bound, members (name, type, default as written, required)
|
# 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)
|
cx_row_reducers(b)
|
||||||
buf_puts(b, "\n ],\n \"row_verbs\": [")
|
buf_puts(b, "\n ],\n \"row_verbs\": [")
|
||||||
cx_row_verbs(b)
|
cx_row_verbs(b)
|
||||||
|
buf_puts(b, "\n ],\n \"machines\": [")
|
||||||
|
cx_machines(b)
|
||||||
buf_puts(b, "\n ],\n \"dispatch\": [")
|
buf_puts(b, "\n ],\n \"dispatch\": [")
|
||||||
cx_dispatch(b)
|
cx_dispatch(b)
|
||||||
buf_puts(b, "\n ],\n \"events\": [")
|
buf_puts(b, "\n ],\n \"events\": [")
|
||||||
|
|
@ -318,6 +322,60 @@ function cx_row_reducers(b: Buf) -> void {
|
||||||
i += 1
|
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 {
|
function cx_row_verbs(b: Buf) -> void {
|
||||||
let keys = new []pointer
|
let keys = new []pointer
|
||||||
var i = 0
|
var i = 0
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
var ck_names: []pointer = new []pointer
|
var ck_names: []pointer = new []pointer
|
||||||
var ck_tys: []pointer = new []pointer
|
var ck_tys: []pointer = new []pointer
|
||||||
var ck_ret: pointer = "void" # the result type of the function being checked
|
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_errors: int = 0
|
||||||
var ck_report: int = -1 # LUDIC_CHECK_REPORT=1: list every mix-up by category, fail nothing
|
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 "?"
|
return "?"
|
||||||
}
|
}
|
||||||
let bt = ck_expr(b)
|
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 "?" }
|
if ck_unknown(bt) { return "?" }
|
||||||
let r = ck_record(bt)
|
let r = ck_record(bt)
|
||||||
if r != null {
|
if r != null {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ function ck_assign(s: Node) -> void {
|
||||||
# a local rebound is not a write through it
|
# 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") }
|
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)
|
let lt = ck_target(s.a)
|
||||||
|
ck_machine_write(s.a, "this assignment")
|
||||||
if (s.s == "=") { ck_expect = lt }
|
if (s.s == "=") { ck_expect = lt }
|
||||||
let rt = ck_expr(s.b)
|
let rt = ck_expr(s.b)
|
||||||
var what: pointer = "this assignment"
|
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)
|
let res = ck_binop(s, op, lt, rt, s.a, s.b)
|
||||||
ck_give(lt, res, s.b, what)
|
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 {
|
function ck_match(s: Node) -> void {
|
||||||
ck_expr(s.a)
|
ck_expr(s.a)
|
||||||
var i = 0
|
var i = 0
|
||||||
|
|
@ -129,6 +137,10 @@ function ck_stmt(s: Node) -> void {
|
||||||
# a machine's store: its states' `become` writes it
|
# a machine's store: its states' `become` writes it
|
||||||
if k == S_MACHINE {
|
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 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)
|
ck_walk(s)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -178,6 +190,7 @@ function ck_fn_body(d: Node) -> void {
|
||||||
}
|
}
|
||||||
ck_ret = d.ty
|
ck_ret = d.ty
|
||||||
if ck_ret == null { ck_ret = "void" }
|
if ck_ret == null { ck_ret = "void" }
|
||||||
|
ck_fn_node = d
|
||||||
if d.uns == 1 {
|
if d.uns == 1 {
|
||||||
ck_unsafe_here(d)
|
ck_unsafe_here(d)
|
||||||
ck_unsafe += 1
|
ck_unsafe += 1
|
||||||
|
|
@ -185,6 +198,7 @@ function ck_fn_body(d: Node) -> void {
|
||||||
ck_block(d.a)
|
ck_block(d.a)
|
||||||
if d.uns == 1 { ck_unsafe -= 1 }
|
if d.uns == 1 { ck_unsafe -= 1 }
|
||||||
ck_ret = "void"
|
ck_ret = "void"
|
||||||
|
ck_fn_node = null
|
||||||
ck_pop(m)
|
ck_pop(m)
|
||||||
mg_leave()
|
mg_leave()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ function parse_reducer() -> void {
|
||||||
push(g_red_state, st)
|
push(g_red_state, st)
|
||||||
push(g_red_action, act)
|
push(g_red_action, act)
|
||||||
push(g_red_rec, null) # a state's reducer: no row (actions_rows.ludic)
|
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_tbl, null)
|
||||||
push(g_red_tgt, null)
|
push(g_red_tgt, null)
|
||||||
push(prog, f)
|
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
|
# after the program is read: the reducers checked, each dispatch numbered, and the queue, its push
|
||||||
# and its drain written
|
# and its drain written
|
||||||
function actions_finish() -> void {
|
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)
|
rows_finish_checks() # 27.3: @Target, @RowVerb (actions_rows.ludic)
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < len(g_red_nodes) {
|
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_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_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_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_comp: []pointer = new []pointer # every @Target field: its record, name, type, node
|
||||||
var g_tgt_field: []pointer = new []pointer
|
var g_tgt_field: []pointer = new []pointer
|
||||||
var g_tgt_node: []Node = new []Node
|
var g_tgt_node: []Node = new []Node
|
||||||
|
|
@ -42,7 +43,7 @@ function parse_row_reducer() -> void {
|
||||||
pi = k + 1
|
pi = k + 1
|
||||||
toks[pi].text = "function" # read the rest as a function named for the action
|
toks[pi].text = "function" # read the rest as a function named for the action
|
||||||
let f = parse_fn()
|
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
|
f.vis = 1
|
||||||
push(g_red_nodes, f)
|
push(g_red_nodes, f)
|
||||||
push(g_red_state, st)
|
push(g_red_state, st)
|
||||||
|
|
@ -50,8 +51,19 @@ function parse_row_reducer() -> void {
|
||||||
push(g_red_rec, rec)
|
push(g_red_rec, rec)
|
||||||
push(g_red_tbl, path)
|
push(g_red_tbl, path)
|
||||||
push(g_red_tgt, null)
|
push(g_red_tgt, null)
|
||||||
|
push(g_red_mc, g_mc_gen_reg)
|
||||||
push(prog, f)
|
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 {
|
function rr_shape() -> void {
|
||||||
perr("a row reducer is `reducer Record in State.table on Action(r: mut Row<Record>, a: Action) { ... }`")
|
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}`
|
return `{rec}__in__{st}__{p}`
|
||||||
}
|
}
|
||||||
function rr_wrap_name(i: int) -> pointer {
|
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
|
# a field's @Target / @Column, read by parse_component
|
||||||
function rr_field_attr(comp: pointer, f: Node, is_target: bool, is_column: bool) -> void {
|
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
|
i = 0
|
||||||
while i < len(prog) {
|
while i < len(prog) {
|
||||||
let d = prog[i]
|
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
|
i += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +227,7 @@ function rr_check(i: int) -> void {
|
||||||
rr_params(i, f, who)
|
rr_params(i, f, who)
|
||||||
var j = 0
|
var j = 0
|
||||||
while j < i {
|
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
|
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 {
|
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"
|
var name: pointer = "a method"
|
||||||
if n.a != null and n.a.kind == E_ID { name = n.a.s }
|
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`)
|
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 == "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 == "deterministic" { is_det = true }
|
||||||
else if a == "RowVerb" { is_rowverb = 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 == "AppendOnly") or (a == "ByKey") { at_decl(a) } # a registry's, for editors (attrs.ludic)
|
||||||
else if a == "PerMap" { # a map-scoped registry (permap.ludic)
|
else if a == "PerMap" { # a map-scoped registry (permap.ludic)
|
||||||
at_decl(a)
|
at_decl(a)
|
||||||
|
|
@ -1103,6 +1104,7 @@ function parse_one_decl_in() -> void {
|
||||||
}
|
}
|
||||||
skipnl()
|
skipnl()
|
||||||
}
|
}
|
||||||
|
mc_attr_placed() # @Machine is a registry's (machines.ludic)
|
||||||
if is_id("friend") and (toks[pi + 1].text == "module") {
|
if is_id("friend") and (toks[pi + 1].text == "module") {
|
||||||
mod_parse_friend() # frontend/modules.ludic
|
mod_parse_friend() # frontend/modules.ludic
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ function parse_registry() -> void {
|
||||||
push(g_rg_open, 0)
|
push(g_rg_open, 0)
|
||||||
push(g_rg_from, from)
|
push(g_rg_from, from)
|
||||||
i18n_registry_declared() # i18n.ludic: its @TextKey, before the attributes are taken
|
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
|
push(g_rg_at, at_registry_take()) # attrs.ludic
|
||||||
pm_declared(pm, chunk, v.s)
|
pm_declared(pm, chunk, v.s)
|
||||||
if from != null and not pm { res_read(v, from) }
|
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", "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", "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", "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", "become", "Moves a machine (or the program) to another state or scene.")
|
||||||
vc_kw("statement", "return", "Returns from a function.")
|
vc_kw("statement", "return", "Returns from a function.")
|
||||||
vc_kw("statement", "break", "Leaves the innermost loop.")
|
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("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("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("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.")
|
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
|
# a declaration's
|
||||||
vc_at("export", "checked", "declaration", "", "The declaration is exported (the same as the export keyword).")
|
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-target",
|
||||||
"annot-column",
|
"annot-column",
|
||||||
"annot-rowverb",
|
"annot-rowverb",
|
||||||
|
"annot-machine",
|
||||||
"annot-permap",
|
"annot-permap",
|
||||||
"annot-range",
|
"annot-range",
|
||||||
"annot-ref",
|
"annot-ref",
|
||||||
|
|
|
||||||
|
|
@ -269,8 +269,20 @@ function dp_fn_name(n: pointer) -> pointer {
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
# 27.3: a row reducer's symbol says `Deer__in__Herd__deer` for `Deer in Herd.deer`
|
# 27.3: a row reducer's symbol says `Deer__in__Herd__deer` for `Deer in Herd.deer`; one a @Machine
|
||||||
function dp_row_name(s: pointer) -> pointer {
|
# 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 out = ""
|
||||||
var seen = false
|
var seen = false
|
||||||
var i = 0
|
var i = 0
|
||||||
|
|
@ -287,7 +299,7 @@ function dp_row_name(s: pointer) -> pointer {
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out + tail
|
||||||
}
|
}
|
||||||
function dp_own(k: int) -> bool { return dp_pkg[k] == 0 }
|
function dp_own(k: int) -> bool { return dp_pkg[k] == 0 }
|
||||||
# an edge the numbers count: between two of the program's own modules
|
# 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/state.ludic")
|
||||||
push(f, "selfhost/frontend/actions.ludic")
|
push(f, "selfhost/frontend/actions.ludic")
|
||||||
push(f, "selfhost/frontend/actions_rows.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/migrate.ludic")
|
||||||
push(f, "selfhost/frontend/registry.ludic")
|
push(f, "selfhost/frontend/registry.ludic")
|
||||||
push(f, "selfhost/frontend/registry_finish.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/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/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")
|
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/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)")
|
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 ==")
|
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/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", "{\"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/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("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", "{\"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")
|
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")
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,10 @@ static const char* LUDIC_PHASES[] = {
|
||||||
static const char* LUDIC_ATTRIBUTES[] = {
|
static const char* LUDIC_ATTRIBUTES[] = {
|
||||||
"Ref","OneOf","Range","Unit","Asset","Color","Node","Clip","Material","Tint","Derived","Text",
|
"Ref","OneOf","Range","Unit","Asset","Color","Node","Clip","Material","Tint","Derived","Text",
|
||||||
"Multiline","Key","Computed","Sync","frame","max","owns","Target","Column","AppendOnly","ByKey",
|
"Multiline","Key","Computed","Sync","frame","max","owns","Target","Column","AppendOnly","ByKey",
|
||||||
"PerMap","Chunked","TextKey","export","Public","On","Queries","OnSpawn","OnDespawn","OnAttach",
|
"PerMap","Chunked","Machine","TextKey","export","Public","On","Queries","OnSpawn","OnDespawn",
|
||||||
"OnDetach","OnEnable","OnDisable","OnStart","OnQuit","Owned","Server","Predicted","ToServer",
|
"OnAttach","OnDetach","OnEnable","OnDisable","OnStart","OnQuit","Owned","Server","Predicted",
|
||||||
"ToClients","System","EngineSystem","Namespace","deterministic","ClearColor","alloc_ok",
|
"ToServer","ToClients","System","EngineSystem","Namespace","deterministic","ClearColor",
|
||||||
"creates","releases","RowVerb","Handles","Reads","Writes", 0
|
"alloc_ok","creates","releases","RowVerb","Handles","Reads","Writes", 0
|
||||||
};
|
};
|
||||||
/* ludic-dev syntax: end */
|
/* ludic-dev syntax: end */
|
||||||
static const char* LUDIC_WIDGETS[] = {
|
static const char* LUDIC_WIDGETS[] = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue