Merge lang/row-reducers ab0b84b8: phase 27.3 - reducers on a table's row (reducer T in S.table on A, @Target, Row<T>, @RowVerb, @Column), an allocation-free drain, and ludic schema's row_reducers and row_verbs; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
a637aec63e
37 changed files with 125923 additions and 110956 deletions
65
LANGUAGE.md
65
LANGUAGE.md
|
|
@ -386,6 +386,63 @@ program Pack {
|
|||
- **Events stay** for what changes no state - a sound, a notice, telemetry - and `@On` listeners run
|
||||
as the event is emitted. Actions are for changes.
|
||||
|
||||
**A reducer on a row** (27.3). Many of a kind are rows of a ludic.base `Table<T>` (Things, animals,
|
||||
vehicles), and an action may name ONE of them: its field marked `@Target` holds the row's handle,
|
||||
and a reducer written `in` the table runs for that row alone.
|
||||
|
||||
```ludic
|
||||
import "ludic.base"
|
||||
program Herds {
|
||||
numbers float
|
||||
property Deer {
|
||||
@Column x: float = 0.0 # a table column mirrors it
|
||||
fear: int = 0
|
||||
}
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
state Noise { loud: int = 2 }
|
||||
action Spook { @Target who: int = -1, by: int = 1 }
|
||||
action Bolt { @Target who: int = -1, dx: float = 0.0 }
|
||||
|
||||
reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, n: Noise, a: Spook) {
|
||||
r.rec.fear += a.by * n.loud
|
||||
if r.rec.fear > 3 { dispatch Bolt { who: r.h, dx: 5.0 } }
|
||||
}
|
||||
reducer Deer in Herd.deer on Bolt(r: mut Row<Deer>, a: Bolt) { deer_move(r, r.rec.x + a.dx) }
|
||||
|
||||
@RowVerb function deer_move(r: mut Row<Deer>, x: float) -> void {
|
||||
r.rec.x = x
|
||||
tb_set_f(r.tb, 0, r.row, x) # the column and its indexes, with the field
|
||||
}
|
||||
entry (h: mut Herd) {
|
||||
let tb: Table<Deer> = table_new(1, 0)
|
||||
h.deer = tb
|
||||
dispatch Spook { who: tb_add(tb, new Deer), by: 2 }
|
||||
drain_actions()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`reducer Record in State.table on Action(r: mut Row<Record>, reads..., a: Action)`** - the
|
||||
table is a field of the state (a path through its records is fine: `WildState.w.tab`), of type
|
||||
`Table<Record>`, and only the module that owns the state declares a row reducer on it. The row
|
||||
comes first and `mut`, the action last, and between them states it only READS, as any reducer's.
|
||||
- **`@Target`** marks the action's one field holding the handle (an `int`, what `tb_add` returned).
|
||||
One to an action: several rows are the dispatcher's loop, one `dispatch` per handle.
|
||||
- **The drain resolves the handle** (`tb_row`) and hands the reducer a `Row<Record>` (ludic.base:
|
||||
`tb`, `row`, `h`, `rec`) it keeps, one per row reducer and filled in place, so a targeted action
|
||||
allocates nothing. A handle whose row is gone - the animal removed since the dispatch - runs
|
||||
nothing, which is not an error; `LUDIC_ACTIONS_LOG=1` prints a line for it. Row reducers take
|
||||
their place among the action's reducers by their state's name, then the table's.
|
||||
- **The row goes no further.** Inside a row reducer `r.rec` and `r.h` are all it reaches: `r.tb`
|
||||
and `r.row` are refused, the view is not assigned, stored, copied or handed to anything but a
|
||||
**`@RowVerb`** - a function of the record's own module that takes the row first - and a field
|
||||
marked **`@Column`** (one a table column mirrors: a position, a kind, whether it is alive) is not
|
||||
written through `r.rec`: `reducer Deer in Herd.deer on Push: Deer.x is mirrored by a column of the
|
||||
table (@Column) - write it through a @RowVerb, which keeps the column and its indexes with it`.
|
||||
What `things_verify` catches at run time is a compile error inside a row reducer.
|
||||
- **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.
|
||||
|
||||
`ludic deps` reports the widest function - the most states any function or entry point of the
|
||||
program's own takes - and `--check` holds it as a ratchet like its other numbers
|
||||
(`widest_function 12` in the baseline file). A function value's states are supplied where it is called, so a
|
||||
|
|
@ -808,8 +865,12 @@ object with `"schema_version": 1`:
|
|||
without a `uses` line, `friend` null, or `{"of": null}` for a friend of every module and
|
||||
`{"of": [...]}` for `friend module lab of a, b`; `files` its source files and the resource files
|
||||
read into its registries;
|
||||
- `states`, `actions` (`{"name", "module", "at"}`), `reducers` (`{"state", "action", "module",
|
||||
"at"}`) and `dispatch` (every `dispatch`: `{"action", "module", "at"}`);
|
||||
- `states` (`{"name", "module", "at"}`), `actions` (`{"name", "module", "at", "target"}`,
|
||||
`target` its `@Target` field or null), `reducers` (`{"state", "action", "module", "at"}`, a
|
||||
state's), `row_reducers` (`{"record", "table", "state", "action", "target", "predicted", "net",
|
||||
"module", "at"}`: `table` the path as written, `State.field`; `predicted` false and `net` null
|
||||
until `@Predicted` and `@Net` reach reducers), `row_verbs` (`{"name", "record", "module", "at"}`)
|
||||
and `dispatch` (every `dispatch`: `{"action", "module", "at"}`);
|
||||
- `events` - `{"name", "module", "at", "cancellable", "net"}` (`net` `"toserver"` for
|
||||
`@ToServer`, `"toclients"` for `@ToClients`, else null), its `emits` (every `emit`'s place) and
|
||||
its `listeners` (the `@On` handlers: `{"handler", "module", "at"}`);
|
||||
|
|
|
|||
23
changes/row-reducers.md
Normal file
23
changes/row-reducers.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A reducer on a table's row** (phase 27.3). An action names one row of a ludic.base `Table<T>` by
|
||||
its handle, in a field marked `@Target`, and `reducer Deer in Herd.deer on Spook(r: mut Row<Deer>,
|
||||
n: Noise, a: Spook)` runs once, for that row alone (the table may sit down a path, `WildState.w.tab`).
|
||||
|
||||
- **The drain resolves the handle** (`tb_row`) and hands the reducer a `Row<T>` (new in ludic.base:
|
||||
`tb`, `row`, `h`, `rec`) that it keeps, one per row reducer, filled in place: a targeted action
|
||||
allocates nothing. A stale handle runs nothing; `LUDIC_ACTIONS_LOG=1` prints a line for it. Row
|
||||
reducers order among an action's reducers by their state's name, then the table's path.
|
||||
- **The row goes no further**, checked at compile time: a row reducer reaches `r.rec` and `r.h`, not
|
||||
`r.tb` or `r.row`; it does not assign, store, copy or hand on the row except to a `@RowVerb` (a
|
||||
function of the record's own module taking `Row<T>` first); a field marked `@Column` (a table
|
||||
column mirrors it) is written only by a verb; only the module owning the state declares a row
|
||||
reducer; states between the row and the action are read. `mut` is allowed on a `Row<T>` parameter.
|
||||
- **`ludic schema`'s code section** gains `row_reducers` (`record`, `table`, `state`, `action`,
|
||||
`target`, `predicted`, `net`, `module`, `at`) and `row_verbs` (`name`, `record`, `module`, `at`),
|
||||
and every action its `target`; row reducers are not listed under `reducers`. `ludic deps` names a
|
||||
row reducer `reducer Deer in Herd.deer on Spook`; ludic-lsp's outline does too.
|
||||
- Examples `actions/rows`; rejects for a missing `@Target`, two of them, a `@Column` write, reaching
|
||||
the table, a row handed to a non-verb, a table that is not one, the parameter shape, a second
|
||||
state written, a verb with no row, and a reducer outside the state's module. `@Predicted`, `@Net`
|
||||
and `@Broadcast` on actions and row reducers are later steps of phase 27.
|
||||
19
docs/language/annotations/annot-column.md
Normal file
19
docs/language/annotations/annot-column.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
id: annot-column
|
||||
name: @Column
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Column
|
||||
sig: property Deer { @Column x: float = 0.0 }
|
||||
tip: A record's field a column of its table mirrors: written only by a @RowVerb.
|
||||
order: 82
|
||||
---
|
||||
|
||||
<code>@Column</code> marks a field of a table's record that one of the table's columns mirrors - a position, a kind, whether it is alive - so an index reads it. Inside a row reducer it is not written through <code>r.rec</code>: a <code>@RowVerb</code> writes it, and the column and its indexes with it.
|
||||
|
||||
```ludic
|
||||
property Deer {
|
||||
@Column x: float = 0.0
|
||||
fear: int = 0
|
||||
}
|
||||
```
|
||||
19
docs/language/annotations/annot-rowverb.md
Normal file
19
docs/language/annotations/annot-rowverb.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
id: annot-rowverb
|
||||
name: @RowVerb
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @RowVerb
|
||||
sig: @RowVerb function deer_move(r: mut Row<Deer>, x: float) -> void
|
||||
tip: A verb of a table's row: the only function a row reducer hands its row to.
|
||||
order: 83
|
||||
---
|
||||
|
||||
<code>@RowVerb</code> marks a function that changes one row of a table, taking the <code>Row<T></code> first. It is declared in the module that owns the record, reaches the row's table (<code>r.tb</code>, <code>r.row</code>) to keep its columns and indexes, and is the only function a row reducer may hand its row to; a function not marked so is refused a row.
|
||||
|
||||
```ludic
|
||||
@RowVerb function deer_move(r: mut Row<Deer>, x: float) -> void {
|
||||
r.rec.x = x
|
||||
tb_set_f(r.tb, 0, r.row, x)
|
||||
}
|
||||
```
|
||||
16
docs/language/annotations/annot-target.md
Normal file
16
docs/language/annotations/annot-target.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
id: annot-target
|
||||
name: @Target
|
||||
category: annotations
|
||||
kind: annotation
|
||||
tokens: @Target
|
||||
sig: action Spook { @Target who: int = -1 }
|
||||
tip: An action's field holding the handle of the one table row it names.
|
||||
order: 81
|
||||
---
|
||||
|
||||
<code>@Target</code> marks the field of an <code>action</code> that holds a row's handle - what <code>tb_add</code> returned. A row reducer (<code>reducer Deer in Herd.deer on Spook</code>) runs once, for that row alone, and a handle whose row is gone runs nothing. An action has one: several rows are one <code>dispatch</code> per handle.
|
||||
|
||||
```ludic
|
||||
action Spook { @Target who: int = -1, by: int = 1 }
|
||||
```
|
||||
|
|
@ -4,8 +4,8 @@ name: reducer
|
|||
category: structure
|
||||
kind: keyword
|
||||
tokens: reducer
|
||||
sig: reducer State on Action(st: mut State, reads..., a: Action) { ... }
|
||||
tip: Applies an action to one state; it may read others.
|
||||
sig: reducer State on Action(st: mut State, reads..., a: Action) { ... } / reducer Record in State.table on Action(r: mut Row<Record>, reads..., a: Action) { ... }
|
||||
tip: Applies an action to one state, or to the one table row its @Target names; it may read others.
|
||||
order: 62
|
||||
---
|
||||
|
||||
|
|
@ -18,3 +18,12 @@ reducer Pos on Move(p: mut Pos, a: Move) {
|
|||
p.x = p.x + a.dx
|
||||
}
|
||||
```
|
||||
|
||||
Written <code>in State.table</code>, a reducer runs for the one row of that <code>Table<Record></code> the action's <code>@Target</code> field names: it is handed a <code>Row<Record></code>, reaches <code>r.rec</code> and <code>r.h</code>, and changes a field a column mirrors (<code>@Column</code>) only through a <code>@RowVerb</code>. A handle whose row is gone runs nothing.
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — needs ludic.base's Table
|
||||
reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, a: Spook) {
|
||||
r.rec.fear += a.by
|
||||
}
|
||||
```
|
||||
|
|
|
|||
44
examples/actions/rows.ludic
Normal file
44
examples/actions/rows.ludic
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# rows.ludic - 27.3: a reducer on a row. An action that names one row of a Table by its handle
|
||||
# (@Target) is reduced for that row alone: the drain resolves the handle and hands the reducer a
|
||||
# Row it keeps, so nothing is allocated. A field a column mirrors (@Column) is written only by the
|
||||
# record's @RowVerbs, which keep the column with it; a handle whose row is gone runs nothing.
|
||||
import "ludic.base"
|
||||
program RowActions {
|
||||
numbers float
|
||||
property Deer {
|
||||
@Column x: float = 0.0 # mirrored by column 0 of the herd's table
|
||||
fear: int = 0
|
||||
}
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
state Noise { loud: int = 2 }
|
||||
action Spook { @Target who: int = -1, by: int = 1 }
|
||||
action Bolt { @Target who: int = -1, dx: float = 0.0 }
|
||||
|
||||
reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, n: Noise, a: Spook) {
|
||||
r.rec.fear += a.by * n.loud
|
||||
if r.rec.fear > 3 { dispatch Bolt { who: r.h, dx: 5.0 } } # a follow-on, for the same row
|
||||
}
|
||||
reducer Deer in Herd.deer on Bolt(r: mut Row<Deer>, a: Bolt) { deer_move(r, r.rec.x + a.dx) }
|
||||
reducer Noise on Spook(n: mut Noise, a: Spook) { n.loud += 1 } # a state's reducer on the same action
|
||||
|
||||
@RowVerb function deer_move(r: mut Row<Deer>, x: float) -> void {
|
||||
r.rec.x = x
|
||||
tb_set_f(r.tb, 0, r.row, x)
|
||||
}
|
||||
|
||||
entry (h: mut Herd) {
|
||||
let tb: Table<Deer> = table_new(1, 0)
|
||||
h.deer = tb
|
||||
let a = tb_add(tb, new Deer)
|
||||
let b = tb_add(tb, new Deer { x: 10.0 })
|
||||
let c = tb_add(tb, new Deer)
|
||||
tb_remove(tb, c)
|
||||
dispatch Spook { who: a, by: 1 } # Noise's reducer runs after Deer's: 2, then 3 for b
|
||||
dispatch Spook { who: b, by: 2 } # 6 > 3: b bolts
|
||||
dispatch Spook { who: c, by: 9 } # c is gone: its row reducers run nothing
|
||||
drain_actions()
|
||||
let ra = tb_row(tb, a)
|
||||
let rb = tb_row(tb, b)
|
||||
print(`{tb_rec(tb, ra).fear} {tb_rec(tb, rb).fear} {int(tb_rec(tb, rb).x)} {int(tb_f(tb, 0, rb))}`)
|
||||
}
|
||||
}
|
||||
12
examples/rejected/row_reducer_column.ludic
Normal file
12
examples/rejected/row_reducer_column.ludic
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# 27.3: a field a column mirrors is written only by a @RowVerb, which keeps the column with it
|
||||
import "ludic.base"
|
||||
program RowReducerColumn {
|
||||
property Deer {
|
||||
@Column x: int = 0
|
||||
fear: int = 0
|
||||
}
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Push { @Target who: int = -1, dx: int = 1 }
|
||||
reducer Deer in Herd.deer on Push(r: mut Row<Deer>, a: Push) { r.rec.x += a.dx }
|
||||
entry { print(1) }
|
||||
}
|
||||
9
examples/rejected/row_reducer_no_target.ludic
Normal file
9
examples/rejected/row_reducer_no_target.ludic
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# 27.3: a row reducer's action names its row in a field marked @Target
|
||||
import "ludic.base"
|
||||
program RowReducerNoTarget {
|
||||
property Deer { fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { who: int = -1 }
|
||||
reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, a: Spook) { r.rec.fear += 1 }
|
||||
entry { print(1) }
|
||||
}
|
||||
10
examples/rejected/row_reducer_not_verb.ludic
Normal file
10
examples/rejected/row_reducer_not_verb.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# 27.3: a row is handed only to its record's @RowVerbs
|
||||
import "ludic.base"
|
||||
program RowReducerNotVerb {
|
||||
property Deer { fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
function scare(r: mut Row<Deer>) -> void { r.rec.fear += 1 }
|
||||
reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, a: Spook) { scare(r) }
|
||||
entry { print(1) }
|
||||
}
|
||||
7
examples/rejected/row_reducer_owner.ludic
Normal file
7
examples/rejected/row_reducer_owner.ludic
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# 27.3: a table's rows are reduced only by the module that owns its state
|
||||
import "ludic.base"
|
||||
import "row_reducer_owner/herd"
|
||||
program RowReducerOwner {
|
||||
reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, a: Spook) { r.rec.fear += 1 }
|
||||
entry { print(1) }
|
||||
}
|
||||
5
examples/rejected/row_reducer_owner/herd/index.ludic
Normal file
5
examples/rejected/row_reducer_owner/herd/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# herd/index.ludic - the herd's state and its table; its rows are its own module's to reduce
|
||||
module herd uses ludic_base
|
||||
export property Deer { fear: int = 0 }
|
||||
export state Herd { deer: Table<Deer> = null }
|
||||
export action Spook { @Target who: int = -1 }
|
||||
9
examples/rejected/row_reducer_params.ludic
Normal file
9
examples/rejected/row_reducer_params.ludic
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# 27.3: a row reducer takes its row, mut, first and the action last
|
||||
import "ludic.base"
|
||||
program RowReducerParams {
|
||||
property Deer { fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
reducer Deer in Herd.deer on Spook(r: Row<Deer>, a: Spook) { print(r.rec.fear) }
|
||||
entry { print(1) }
|
||||
}
|
||||
11
examples/rejected/row_reducer_reach.ludic
Normal file
11
examples/rejected/row_reducer_reach.ludic
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# 27.3: a row reducer reaches its row's record and handle, not its table
|
||||
import "ludic.base"
|
||||
program RowReducerReach {
|
||||
property Deer { fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
action Spook { @Target who: int = -1 }
|
||||
reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, a: Spook) {
|
||||
tb_remove(r.tb, r.h)
|
||||
}
|
||||
entry { print(1) }
|
||||
}
|
||||
9
examples/rejected/row_reducer_table.ludic
Normal file
9
examples/rejected/row_reducer_table.ludic
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# 27.3: a row reducer names a Table of its record in its state
|
||||
import "ludic.base"
|
||||
program RowReducerTable {
|
||||
property Deer { fear: int = 0 }
|
||||
state Herd { count: int = 0 }
|
||||
action Spook { @Target who: int = -1 }
|
||||
reducer Deer in Herd.count on Spook(r: mut Row<Deer>, a: Spook) { r.rec.fear += 1 }
|
||||
entry { print(1) }
|
||||
}
|
||||
10
examples/rejected/row_reducer_writes_state.ludic
Normal file
10
examples/rejected/row_reducer_writes_state.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# 27.3: a row reducer writes its row alone; the states it declares are read
|
||||
import "ludic.base"
|
||||
program RowReducerWritesState {
|
||||
property Deer { fear: int = 0 }
|
||||
state Herd { deer: Table<Deer> = null }
|
||||
state Noise { loud: int = 0 }
|
||||
action Spook { @Target who: int = -1 }
|
||||
reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, n: mut Noise, a: Spook) { n.loud += 1 }
|
||||
entry { print(1) }
|
||||
}
|
||||
7
examples/rejected/rowverb_no_row.ludic
Normal file
7
examples/rejected/rowverb_no_row.ludic
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# 27.3: a @RowVerb takes its row first
|
||||
import "ludic.base"
|
||||
program RowVerbNoRow {
|
||||
property Deer { fear: int = 0 }
|
||||
@RowVerb function scare(n: int) -> void { print(n) }
|
||||
entry { print(1) }
|
||||
}
|
||||
6
examples/rejected/target_twice.ludic
Normal file
6
examples/rejected/target_twice.ludic
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# 27.3: an action names one row: one @Target field
|
||||
import "ludic.base"
|
||||
program TargetTwice {
|
||||
action Swap { @Target a: int = -1, @Target b: int = -1 }
|
||||
entry { print(1) }
|
||||
}
|
||||
|
|
@ -86,6 +86,11 @@ allocates nothing per query:
|
|||
- **Parallel work reads, never writes, a state.** `Job.parallel_for(n, fn work, ctx)` over a
|
||||
table's columns runs on every core; the compiler refuses a worker that takes a state as `mut`
|
||||
(a result goes into `ctx`, a shared count through a `Sync` handle kept in the state).
|
||||
- **A row, for a row reducer.** `Row<T>` (`tb`, `row`, `h`, `rec`) is one row as the action drain
|
||||
hands it to `reducer T in S.table on A(r: mut Row<T>, a: A)` (LANGUAGE.md, "A reducer on a row"):
|
||||
the action's `@Target` field carries the handle, the drain resolves it and fills a Row it keeps.
|
||||
The table's owner writes a row through its `@RowVerb`s, which take the Row first and keep the
|
||||
columns with the record's `@Column` fields.
|
||||
- **Stable ids.** `IntMap` (`imap_new`, `imap_put`, `imap_get(m, k, none)`, `imap_del`) maps an
|
||||
id kept in a save or a message to a handle without a scan.
|
||||
|
||||
|
|
|
|||
10
packages/ludic.base/ecs_row.ludic
Normal file
10
packages/ludic.base/ecs_row.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# ludic.base/ecs_row.ludic - a Row<T>: one row of a Table<T>, as the action drain hands it to a row
|
||||
# reducer (`reducer T in S.table on A(r: mut Row<T>, a: A)`). The drain keeps one per reducer and
|
||||
# fills it in place, so a targeted action allocates nothing. A row reducer reads `r.rec` and `r.h`;
|
||||
# `r.tb` and `r.row` are for the @RowVerbs of T's own module, which keep the columns and indexes.
|
||||
export property Row<T> {
|
||||
tb: Table<T>
|
||||
row: int = -1 # the row the handle named when the drain resolved it
|
||||
h: int = -1 # the handle the action carried
|
||||
rec: T
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import "save.ludic"
|
|||
import "save_fields.ludic"
|
||||
import "system.ludic"
|
||||
import "ecs_table.ludic"
|
||||
import "ecs_row.ludic"
|
||||
import "ecs_hooks.ludic"
|
||||
import "ecs_table_remove.ludic"
|
||||
import "ecs_table_reserve.ludic"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
# modules name, package, layer, uses (null without a `uses` line) and where, friend, files
|
||||
# states name, module, at actions name, module, at
|
||||
# reducers state, action, module, at dispatch action, module, at
|
||||
# row_reducers record, table (State.path), state, action, target (its @Target field), predicted,
|
||||
# net, module, at row_verbs name, record, module, at (27.3)
|
||||
# actions also carry their target: the @Target field, or null
|
||||
# events name, module, at, cancellable, net (@ToServer / @ToClients), emits (sites) and
|
||||
# listeners (the @On handlers: handler, module, at)
|
||||
# ports name, module, at, bound, members (name, type, default as written, required)
|
||||
|
|
@ -67,6 +70,10 @@ function sc_code(b: Buf) -> void {
|
|||
cx_decls(b, 3)
|
||||
buf_puts(b, "\n ],\n \"reducers\": [")
|
||||
cx_reducers(b)
|
||||
buf_puts(b, "\n ],\n \"row_reducers\": [")
|
||||
cx_row_reducers(b)
|
||||
buf_puts(b, "\n ],\n \"row_verbs\": [")
|
||||
cx_row_verbs(b)
|
||||
buf_puts(b, "\n ],\n \"dispatch\": [")
|
||||
cx_dispatch(b)
|
||||
buf_puts(b, "\n ],\n \"events\": [")
|
||||
|
|
@ -244,6 +251,10 @@ function cx_decls(b: Buf, uns: int) -> void {
|
|||
cx_module(b, d.file)
|
||||
buf_puts(b, ", ")
|
||||
cx_at(b, d)
|
||||
if uns == 3 { # an action: the field naming its row (27.3)
|
||||
buf_puts(b, ", \"target\": ")
|
||||
sc_str(b, rr_target(d.s))
|
||||
}
|
||||
buf_puts(b, "}")
|
||||
i += 1
|
||||
}
|
||||
|
|
@ -253,7 +264,7 @@ function cx_reducers(b: Buf) -> void {
|
|||
var i = 0
|
||||
while i < len(g_red_nodes) {
|
||||
let f = g_red_nodes[i]
|
||||
if cx_written(f) { push(keys, `{g_red_state[i]}\t{g_red_action[i]}\t{cx_at_key(f)}`) } else { push(keys, null) }
|
||||
if cx_written(f) and g_red_rec[i] == null { push(keys, `{g_red_state[i]}\t{g_red_action[i]}\t{cx_at_key(f)}`) } else { push(keys, null) }
|
||||
i += 1
|
||||
}
|
||||
let xs = cx_order(keys)
|
||||
|
|
@ -274,6 +285,64 @@ function cx_reducers(b: Buf) -> void {
|
|||
i += 1
|
||||
}
|
||||
}
|
||||
# 27.3: the reducers on a table's rows, and the verbs that write a row
|
||||
function cx_row_reducers(b: Buf) -> void {
|
||||
let keys = new []pointer
|
||||
var i = 0
|
||||
while i < len(g_red_nodes) {
|
||||
let f = g_red_nodes[i]
|
||||
if cx_written(f) and g_red_rec[i] != null { push(keys, `{g_red_rec[i]}\t{g_red_state[i]}.{g_red_tbl[i]}\t{g_red_action[i]}\t{cx_at_key(f)}`) } else { push(keys, null) }
|
||||
i += 1
|
||||
}
|
||||
let xs = cx_order(keys)
|
||||
i = 0
|
||||
while i < len(xs) {
|
||||
let k = xs[i]
|
||||
let f = g_red_nodes[k]
|
||||
sc_sep(b, i == 0, " ")
|
||||
buf_puts(b, "{\"record\": ")
|
||||
jq_put(b, g_red_rec[k])
|
||||
buf_puts(b, ", \"table\": ")
|
||||
jq_put(b, `{g_red_state[k]}.{g_red_tbl[k]}`)
|
||||
buf_puts(b, ", \"state\": ")
|
||||
jq_put(b, g_red_state[k])
|
||||
buf_puts(b, ", \"action\": ")
|
||||
jq_put(b, g_red_action[k])
|
||||
buf_puts(b, ", \"target\": ")
|
||||
sc_str(b, g_red_tgt[k])
|
||||
buf_puts(b, ", \"predicted\": false, \"net\": null, ") # @Predicted and @Net come with 27.5
|
||||
cx_module(b, f.file)
|
||||
buf_puts(b, ", ")
|
||||
cx_at(b, f)
|
||||
buf_puts(b, "}")
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function cx_row_verbs(b: Buf) -> void {
|
||||
let keys = new []pointer
|
||||
var i = 0
|
||||
while i < len(g_rv_nodes) {
|
||||
let f = g_rv_nodes[i]
|
||||
if cx_written(f) { push(keys, sc_key(f.s, f.file, f.line)) } else { push(keys, null) }
|
||||
i += 1
|
||||
}
|
||||
let xs = cx_order(keys)
|
||||
i = 0
|
||||
while i < len(xs) {
|
||||
let f = g_rv_nodes[xs[i]]
|
||||
sc_sep(b, i == 0, " ")
|
||||
buf_puts(b, "{\"name\": ")
|
||||
jq_put(b, f.s)
|
||||
buf_puts(b, ", \"record\": ")
|
||||
sc_str(b, rv_record(f))
|
||||
buf_puts(b, ", ")
|
||||
cx_module(b, f.file)
|
||||
buf_puts(b, ", ")
|
||||
cx_at(b, f)
|
||||
buf_puts(b, "}")
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function cx_dispatch(b: Buf) -> void {
|
||||
let keys = new []pointer
|
||||
var i = 0
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function vis_check(d: Node, what0: pointer) -> void {
|
|||
# 0.S/0.R: a state's instance, the action queue and a reducer are the runtime's to supply and to
|
||||
# call - the code that names them depends on no module for it
|
||||
if d.kind == N_VAR and d.uns == 1 and str_prefix(d.s, "state$") and is_state_ty(d.ty) { return }
|
||||
if d.kind == N_FN and (str_prefix(d.s, "ludic_reduce__") or is_action_builtin(d.s)) { return }
|
||||
if d.kind == N_FN and (str_prefix(d.s, "ludic_reduce__") or str_prefix(d.s, "ludic_rowred__") or is_action_builtin(d.s)) { return }
|
||||
deps_edge(d, what0) # LUDIC_DEPS: the graph ludic deps reads (emit_deps.ludic)
|
||||
if g_vis_off { return }
|
||||
if d.file == null { return }
|
||||
|
|
|
|||
|
|
@ -108,11 +108,12 @@ function ck_write_check(t: Node, what: pointer) -> void {
|
|||
ck_err("state", t, `{what}: {name} is module-level and immutable all the way down; changing data belongs in a state, passed as a mut parameter`)
|
||||
}
|
||||
}
|
||||
# a function's parameters: `mut` is for a state, and a state without it is read-only
|
||||
# a function's parameters: `mut` is for a state (or a table's row, 27.3), and a state without it is
|
||||
# read-only
|
||||
function ck_bind_param(p: Node) -> void {
|
||||
ck_bind(p.s, p.ty)
|
||||
if p.uns == 1 and not is_state_ty(p.ty) {
|
||||
ck_err("state", p, `{p.s}: mut {p.ty} - mut is for a state parameter, and {p.ty} is not a state`)
|
||||
if p.uns == 1 and not is_state_ty(p.ty) and not rr_is_row_ty(p.ty) {
|
||||
ck_err("state", p, `{p.s}: mut {p.ty} - mut is for a state parameter (or a row, mut Row<T>), and {p.ty} is not a state`)
|
||||
}
|
||||
ck_set_ro(is_state_ty(p.ty) and p.uns == 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ function parse_reducer() -> void {
|
|||
push(g_red_nodes, f)
|
||||
push(g_red_state, st)
|
||||
push(g_red_action, act)
|
||||
push(g_red_rec, null) # a state's reducer: no row (actions_rows.ludic)
|
||||
push(g_red_tbl, null)
|
||||
push(g_red_tgt, null)
|
||||
push(prog, f)
|
||||
}
|
||||
# dispatch ACTION { fields } - a statement: the action made and queued
|
||||
|
|
@ -99,9 +102,10 @@ function act_err(n: Node, msg: pointer) -> void {
|
|||
# after the program is read: the reducers checked, each dispatch numbered, and the queue, its push
|
||||
# and its drain written
|
||||
function actions_finish() -> void {
|
||||
rows_finish_checks() # 27.3: @Target, @RowVerb (actions_rows.ludic)
|
||||
var i = 0
|
||||
while i < len(g_red_nodes) {
|
||||
red_check(i)
|
||||
if g_red_rec[i] != null { rr_check(i) } else { red_check(i) }
|
||||
i += 1
|
||||
}
|
||||
i = 0
|
||||
|
|
@ -148,7 +152,7 @@ function red_check(i: int) -> void {
|
|||
}
|
||||
var j = 0
|
||||
while j < i {
|
||||
if (g_red_state[j] == st) and (g_red_action[j] == act) { act_err(f, `reducer {st} on {act} is declared twice (first at {g_red_nodes[j].file}:{itoa(g_red_nodes[j].line)})`) }
|
||||
if (g_red_state[j] == st) and (g_red_action[j] == act) and g_red_rec[j] == null { act_err(f, `reducer {st} on {act} is declared twice (first at {g_red_nodes[j].file}:{itoa(g_red_nodes[j].line)})`) }
|
||||
j += 1
|
||||
}
|
||||
}
|
||||
|
|
@ -160,8 +164,9 @@ function act_src() -> pointer {
|
|||
src = src + ` kept{itoa(a)}: []{g_act_names[a]} = new []{g_act_names[a]}\n used{itoa(a)}: int = 0\n`
|
||||
a += 1
|
||||
}
|
||||
src = src + "}\n"
|
||||
src = src + rr_fields_src() + "}\n" # 27.3: a row reducer's kept view
|
||||
src = src + act_new_src()
|
||||
src = src + rr_wrappers_src()
|
||||
src = src + "export function ludic_act_push(q: mut LudicActions, k: int, v: pointer) -> void {\n push(q.kinds, k)\n push(q.vals, v)\n}\n"
|
||||
src = src + "export function drain_actions(q: mut LudicActions) -> void {\n if q.depth > 0 or len(q.kinds) == 0 { return }\n q.depth = 1\n var pass = 0\n var round_end = len(q.kinds)\n"
|
||||
src = src + " while q.head < len(q.kinds) {\n if q.head == round_end {\n pass += 1\n round_end = len(q.kinds)\n"
|
||||
|
|
@ -174,7 +179,9 @@ function act_src() -> pointer {
|
|||
src = src + ` if k == {itoa(a)} {{\n`
|
||||
var r = 0
|
||||
while r < len(rs) {
|
||||
src = src + ` {g_red_nodes[rs[r]].s}(v)\n`
|
||||
var callee = g_red_nodes[rs[r]].s
|
||||
if g_red_rec[rs[r]] != null { callee = rr_wrap_name(rs[r]) } # the handle resolved first
|
||||
src = src + ` {callee}(v)\n`
|
||||
r += 1
|
||||
}
|
||||
src = src + " }\n"
|
||||
|
|
@ -210,6 +217,11 @@ function act_new_src() -> pointer {
|
|||
}
|
||||
return src
|
||||
}
|
||||
# a reducer's place among its action's: its state's name, then (a row reducer) its table's path
|
||||
function red_key(i: int) -> pointer {
|
||||
if g_red_tbl[i] == null { return g_red_state[i] }
|
||||
return `{g_red_state[i]}\t{g_red_tbl[i]}`
|
||||
}
|
||||
function red_sorted(act: pointer) -> []int {
|
||||
let out = new []int
|
||||
var i = 0
|
||||
|
|
@ -221,7 +233,7 @@ function red_sorted(act: pointer) -> []int {
|
|||
while a < len(out) {
|
||||
let x = out[a]
|
||||
var b = a - 1
|
||||
while b >= 0 and reg_str_less(g_red_state[x], g_red_state[out[b]]) {
|
||||
while b >= 0 and reg_str_less(red_key(x), red_key(out[b])) {
|
||||
out[b + 1] = out[b]
|
||||
b -= 1
|
||||
}
|
||||
|
|
|
|||
390
selfhost/frontend/actions_rows.ludic
Normal file
390
selfhost/frontend/actions_rows.ludic
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
# actions_rows.ludic — 27.3: a reducer on a row. An action may name one row of a ludic.base
|
||||
# Table<T> by its handle, in a field marked @Target, and a row reducer runs once, for that row alone:
|
||||
#
|
||||
# property Deer { @Column x: float = 0.0, fear: int = 0 } # @Column: a column mirrors it
|
||||
# state Herd { deer: Table<Deer> = null }
|
||||
# action Spook { @Target who: int = -1, by: int = 1 }
|
||||
# reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, n: Noise, a: Spook) { r.rec.fear += a.by }
|
||||
# @RowVerb function deer_move(r: mut Row<Deer>, x: float) -> void { ... tb_set_f(r.tb, ...) }
|
||||
#
|
||||
# The drain resolves the handle (tb_row) and hands the reducer a Row<T> it keeps, one per row
|
||||
# reducer, so nothing is allocated; a stale handle runs nothing (LUDIC_ACTIONS_LOG=1 says so). Inside
|
||||
# a row reducer the row reaches its record and its handle and no further: the table is written only
|
||||
# by the record's module's @RowVerbs, a field a column mirrors (@Column) only by them, and the row
|
||||
# is handed to nothing else. Between the row and the action a row reducer READS states, as any
|
||||
# reducer does; only the module that owns the table's state declares one.
|
||||
var g_red_rec: []pointer = new []pointer # per reducer: the row's record, or null (a state's reducer)
|
||||
var g_red_tbl: []pointer = new []pointer # ... the table's path in its state ("deer", "w.tab")
|
||||
var g_red_tgt: []pointer = new []pointer # ... the action's @Target field, once checked
|
||||
var g_tgt_comp: []pointer = new []pointer # every @Target field: its record, name, type, node
|
||||
var g_tgt_field: []pointer = new []pointer
|
||||
var g_tgt_node: []Node = new []Node
|
||||
var g_col_comp: []pointer = new []pointer # every @Column field: its record and name
|
||||
var g_col_field: []pointer = new []pointer
|
||||
var g_rv_nodes: []Node = new []Node # every @RowVerb function
|
||||
|
||||
# reducer REC in STATE.path on ACTION(r: mut Row<REC>, reads..., a: ACTION) { ... }
|
||||
function parse_row_reducer() -> void {
|
||||
let rec = toks[pi + 1].text
|
||||
let st = toks[pi + 3].text
|
||||
if toks[pi + 3].kind != TK_ID or not (toks[pi + 4].text == ".") { rr_shape() }
|
||||
var k = pi + 5
|
||||
var path: pointer = ""
|
||||
while true {
|
||||
if toks[k].kind != TK_ID { rr_shape() }
|
||||
if len(path) > 0 { path = path + "." }
|
||||
path = path + toks[k].text
|
||||
if not (toks[k + 1].text == ".") { break }
|
||||
k += 2
|
||||
}
|
||||
if not (toks[k + 1].text == "on") or toks[k + 2].kind != TK_ID { rr_shape() }
|
||||
let act = toks[k + 2].text
|
||||
pi = k + 1
|
||||
toks[pi].text = "function" # read the rest as a function named for the action
|
||||
let f = parse_fn()
|
||||
f.s = `ludic_rowred__{act}__{rr_suffix(rec, st, path)}`
|
||||
f.vis = 1
|
||||
push(g_red_nodes, f)
|
||||
push(g_red_state, st)
|
||||
push(g_red_action, act)
|
||||
push(g_red_rec, rec)
|
||||
push(g_red_tbl, path)
|
||||
push(g_red_tgt, null)
|
||||
push(prog, f)
|
||||
}
|
||||
function rr_shape() -> void {
|
||||
perr("a row reducer is `reducer Record in State.table on Action(r: mut Row<Record>, a: Action) { ... }`")
|
||||
}
|
||||
# Deer__in__Herd__deer: a name the drain's own source can spell, read back by `ludic deps` as
|
||||
# `Deer in Herd.deer`
|
||||
function rr_suffix(rec: pointer, st: pointer, path: pointer) -> pointer {
|
||||
var p: pointer = ""
|
||||
var i = 0
|
||||
while i < len(path) {
|
||||
if path[i] == '.' { p = p + "__" } else { p = p + path[i .. i + 1] }
|
||||
i += 1
|
||||
}
|
||||
return `{rec}__in__{st}__{p}`
|
||||
}
|
||||
function rr_wrap_name(i: int) -> pointer {
|
||||
return `ludic_reduce__{g_red_action[i]}__{rr_suffix(g_red_rec[i], g_red_state[i], g_red_tbl[i])}`
|
||||
}
|
||||
# a field's @Target / @Column, read by parse_component
|
||||
function rr_field_attr(comp: pointer, f: Node, is_target: bool, is_column: bool) -> void {
|
||||
if is_target {
|
||||
push(g_tgt_comp, comp)
|
||||
push(g_tgt_field, f.s)
|
||||
push(g_tgt_node, f)
|
||||
}
|
||||
if is_column {
|
||||
push(g_col_comp, comp)
|
||||
push(g_col_field, f.s)
|
||||
}
|
||||
}
|
||||
function rr_is_column(comp: pointer, field: pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(g_col_comp) {
|
||||
if (g_col_comp[i] == comp) and (g_col_field[i] == field) { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
# a record, a state or an action declared in prog, by name
|
||||
function rr_decl(name: pointer) -> Node {
|
||||
var i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_COMP and (d.s == name) { return d }
|
||||
i += 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
function rr_field_ty(ty: pointer, name: pointer) -> pointer {
|
||||
let d = rr_decl(ty)
|
||||
if d == null { return null }
|
||||
var i = 0
|
||||
while i < len(d.kids) {
|
||||
if d.kids[i].kind == N_FIELD and (d.kids[i].s == name) { return d.kids[i].ty }
|
||||
i += 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
function rr_is_verb(name: pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(g_rv_nodes) {
|
||||
if (g_rv_nodes[i].s == name) { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
function rr_is_row_ty(t: pointer) -> bool { return t != null and len(t) > 4 and (t[0 .. 4] == "Row$") }
|
||||
function rr_mod(file: pointer) -> pointer {
|
||||
let m = module_of(file)
|
||||
if m == null { return "" }
|
||||
return m
|
||||
}
|
||||
function rr_mod_name(file: pointer) -> pointer {
|
||||
let m = rr_mod(file)
|
||||
if len(m) == 0 { return "the program" }
|
||||
return `module {m}`
|
||||
}
|
||||
|
||||
# ---- the checks -----------------------------------------------------------------------------------
|
||||
# every @Target on an action, one to an action, an int; every @RowVerb takes a row first and is its
|
||||
# record's module's; a function handed a row is a @RowVerb
|
||||
function rows_finish_checks() -> void {
|
||||
var i = 0
|
||||
while i < len(g_tgt_comp) {
|
||||
let f = g_tgt_node[i]
|
||||
let c = g_tgt_comp[i]
|
||||
if act_index(c) < 0 { act_err(f, `@Target {c}.{f.s}: @Target marks the field of an action that holds the handle of the row it names, and {c} is not an action`) }
|
||||
if not (f.ty == "int") { act_err(f, `@Target {c}.{f.s}: a row's handle is an int (@Target {f.s}: int = -1), not a {f.ty}`) }
|
||||
var j = 0
|
||||
while j < i {
|
||||
if (g_tgt_comp[j] == c) { act_err(f, `{c} has two @Target fields, {g_tgt_field[j]} and {f.s}: an action names one row - dispatch it once per row`) }
|
||||
j += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
i = 0
|
||||
while i < len(g_rv_nodes) {
|
||||
rv_check(g_rv_nodes[i])
|
||||
i += 1
|
||||
}
|
||||
i = 0
|
||||
while i < len(prog) {
|
||||
let d = prog[i]
|
||||
if d.kind == N_FN and d.pos >= 0 and d.tps == null and not rr_is_verb(d.s) and not str_starts(d.s, "ludic_rowred__") { rr_no_row_param(d) }
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function rv_check(f: Node) -> void {
|
||||
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 rr_is_row_ty(first.ty) { act_err(f, `@RowVerb {f.s}: a row verb takes the row first - ({"r"}: mut Row<Record>, ...)`) }
|
||||
let rec = first.ty[4 .. len(first.ty)]
|
||||
let d = rr_decl(rec)
|
||||
if d != null and not (rr_mod(d.file) == rr_mod(f.file)) {
|
||||
act_err(f, `@RowVerb {f.s} is declared in {rr_mod_name(f.file)}, and {rec} is {rr_mod_name(d.file)}'s: a row changes only through its record's own module's verbs`)
|
||||
}
|
||||
k = 0
|
||||
var n = 0
|
||||
while k < len(f.kids) {
|
||||
let p = f.kids[k]
|
||||
if p.kind == N_PARAM {
|
||||
if n > 0 and rr_is_row_ty(p.ty) { act_err(f, `@RowVerb {f.s}: a row verb takes one row, first, and {p.s} is a second`) }
|
||||
n += 1
|
||||
}
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
function rr_no_row_param(f: Node) -> void {
|
||||
var k = 0
|
||||
while k < len(f.kids) {
|
||||
let p = f.kids[k]
|
||||
if p.kind == N_PARAM and rr_is_row_ty(p.ty) {
|
||||
let rec = p.ty[4 .. len(p.ty)]
|
||||
act_err(f, `{f.s} is handed a row ({p.s}: Row<{rec}>), and is not a @RowVerb: only the verbs of {rec}'s own module take a row - mark it @RowVerb there`)
|
||||
}
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
# a row reducer: its table, its action's target, its parameters, and a body that stays in its row
|
||||
function rr_check(i: int) -> void {
|
||||
let f = g_red_nodes[i]
|
||||
let st = g_red_state[i]
|
||||
let act = g_red_action[i]
|
||||
let rec = g_red_rec[i]
|
||||
let path = g_red_tbl[i]
|
||||
let who = `reducer {rec} in {st}.{path} on {act}`
|
||||
if act_index(act) < 0 { act_err(f, `{who}: {act} is not an action - declare it with action {act} {{ ... }}`) }
|
||||
if not is_state_ty(st) { act_err(f, `{who}: {st} is not a state`) }
|
||||
let sd = rr_decl(st)
|
||||
if sd != null and not (rr_mod(sd.file) == rr_mod(f.file)) {
|
||||
act_err(f, `{who}: {st} is {rr_mod_name(sd.file)}'s, and this reducer is in {rr_mod_name(f.file)} - a table's rows are reduced only by the module that owns its state`)
|
||||
}
|
||||
let ty = rr_path_ty(f, who, st, path)
|
||||
if not (ty == `Table${rec}`) { act_err(f, `{who}: {st}.{path} is {rr_ty_text(ty)}, not Table<{rec}>`) }
|
||||
let t = rr_target(act)
|
||||
if t == null { act_err(f, `{who}: {act} names no row - mark the field that holds the row's handle @Target ({"@Target who: int = -1"})`) }
|
||||
g_red_tgt[i] = t
|
||||
rr_params(i, f, who)
|
||||
var j = 0
|
||||
while j < i {
|
||||
if g_red_rec[j] != null and (g_red_state[j] == st) and (g_red_action[j] == act) and (g_red_rec[j] == rec) and (g_red_tbl[j] == path) { act_err(f, `{who} is declared twice (first at {g_red_nodes[j].file}:{itoa(g_red_nodes[j].line)})`) }
|
||||
j += 1
|
||||
}
|
||||
}
|
||||
# Table$Deer -> Table<Deer>, for a message
|
||||
function rr_ty_text(ty: pointer) -> pointer {
|
||||
if ty == null { return "?" }
|
||||
var i = 0
|
||||
while i < len(ty) {
|
||||
if ty[i] == '$' { return `{ty[0 .. i]}<{ty[i + 1 .. len(ty)]}>` }
|
||||
i += 1
|
||||
}
|
||||
return ty
|
||||
}
|
||||
# the type at the end of the path through the state's fields
|
||||
function rr_path_ty(f: Node, who: pointer, st: pointer, path: pointer) -> pointer {
|
||||
var ty = st
|
||||
var a = 0
|
||||
var i = 0
|
||||
while i <= len(path) {
|
||||
if i == len(path) or path[i] == '.' {
|
||||
let name = path[a .. i]
|
||||
let next = rr_field_ty(ty, name)
|
||||
if next == null { act_err(f, `{who}: {ty} has no field {name}`) }
|
||||
ty = next
|
||||
a = i + 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return ty
|
||||
}
|
||||
function rr_target(act: pointer) -> pointer {
|
||||
var i = 0
|
||||
while i < len(g_tgt_comp) {
|
||||
if (g_tgt_comp[i] == act) { return g_tgt_field[i] }
|
||||
i += 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
# (r: mut Row<REC>, reads..., a: ACTION); read in that order, kept as (reads..., r, a) so the drain
|
||||
# supplies the reads as it supplies any reducer's states
|
||||
function rr_params(i: int, f: Node, who: pointer) -> void {
|
||||
let rec = g_red_rec[i]
|
||||
let act = g_red_action[i]
|
||||
var ps = new []Node
|
||||
var k = 0
|
||||
while k < len(f.kids) {
|
||||
if f.kids[k].kind == N_PARAM { push(ps, f.kids[k]) }
|
||||
k += 1
|
||||
}
|
||||
if len(ps) < 2 or not (ps[0].ty == `Row${rec}`) or ps[0].uns != 1 or not (ps[len(ps) - 1].ty == act) {
|
||||
act_err(f, `{who}: its parameters are its row, any states it reads, and the action - (r: mut Row<{rec}>, a: {act})`)
|
||||
}
|
||||
k = 1
|
||||
while k < len(ps) - 1 {
|
||||
let p = ps[k]
|
||||
if not is_state_ty(p.ty) { act_err(f, `{who}: {p.s} is a {rr_ty_text(p.ty)} - between its row and the action a row reducer takes only states it reads; what it needs to know rides in the action`) }
|
||||
if p.uns == 1 { act_err(f, `{who}: a row reducer writes its row alone, and {p.s} is a mut {p.ty} - read it ({p.s}: {p.ty}), or dispatch an action {p.ty}'s own reducer takes`) }
|
||||
k += 1
|
||||
}
|
||||
rr_walk(f.a, i, ps[0].s)
|
||||
let kids = new []Node
|
||||
k = 1
|
||||
while k < len(ps) - 1 {
|
||||
push(kids, ps[k])
|
||||
k += 1
|
||||
}
|
||||
push(kids, ps[0])
|
||||
push(kids, ps[len(ps) - 1])
|
||||
f.kids = kids
|
||||
}
|
||||
function rr_err(i: int, n: Node, msg: pointer) -> void {
|
||||
var at = n
|
||||
if at.file == null or at.line == 0 { at = g_red_nodes[i] }
|
||||
act_err(at, `reducer {g_red_rec[i]} in {g_red_state[i]}.{g_red_tbl[i]} on {g_red_action[i]}: {msg}`)
|
||||
}
|
||||
# the body: `r.rec` and `r.h` only, no field a column mirrors written, the row handed only to a verb
|
||||
function rr_walk(n: Node, i: int, r: pointer) -> void {
|
||||
if n == null { return }
|
||||
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")) { rr_err(i, n, `{r}.{n.s}: a row reducer reaches its row's record and handle ({r}.rec, {r}.h) and no further - its table is written by {g_red_rec[i]}'s @RowVerbs and read through its state`) }
|
||||
return
|
||||
}
|
||||
if n.kind == S_ASSIGN and n.a != null and n.a.kind == E_MEMBER and n.a.a != null {
|
||||
let t = n.a
|
||||
if t.a.kind == E_ID and (t.a.s == r) { rr_err(i, n, `{r}.{t.s} is the drain's view of the row, not the reducer's to set - change {r}.rec's fields, or call a @RowVerb`) }
|
||||
if 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(g_red_rec[i], t.s) {
|
||||
rr_err(i, n, `{g_red_rec[i]}.{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 {
|
||||
rr_walk(n.a, i, r)
|
||||
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) { rr_call_verb(i, n, r) } else { rr_walk(n.kids[k], i, r) }
|
||||
k += 1
|
||||
}
|
||||
return
|
||||
}
|
||||
if n.kind == E_ID and (n.s == r) { rr_err(i, n, `{r} goes no further than its record, its handle and {g_red_rec[i]}'s @RowVerbs: the drain keeps it, so it is not stored, copied or handed on`) }
|
||||
rr_walk(n.a, i, r)
|
||||
rr_walk(n.b, i, r)
|
||||
rr_walk(n.c, i, r)
|
||||
var j = 0
|
||||
while j < len(n.kids) {
|
||||
rr_walk(n.kids[j], i, r)
|
||||
j += 1
|
||||
}
|
||||
}
|
||||
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 }
|
||||
var name: pointer = "a method"
|
||||
if n.a != null and n.a.kind == E_ID { name = n.a.s }
|
||||
rr_err(i, n, `{name} is handed the row, and is not a @RowVerb: only the verbs of {g_red_rec[i]}'s own module take a row`)
|
||||
}
|
||||
|
||||
# ---- the drain's side ------------------------------------------------------------------------------
|
||||
# the kept views, one per row reducer, and the switch for a stale handle's line
|
||||
function rr_fields_src() -> pointer {
|
||||
var src: pointer = ""
|
||||
var any = false
|
||||
var i = 0
|
||||
while i < len(g_red_nodes) {
|
||||
if g_red_rec[i] != null {
|
||||
src = src + ` row{itoa(i)}: Row<{g_red_rec[i]}> = new Row<{g_red_rec[i]}>\n`
|
||||
any = true
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if any { src = src + " row_log: int = -1\n" }
|
||||
return src
|
||||
}
|
||||
# per row reducer: the handle resolved, the kept view filled, the reducer called; a stale handle
|
||||
# runs nothing
|
||||
function rr_wrappers_src() -> pointer {
|
||||
var src: pointer = ""
|
||||
var any = false
|
||||
var i = 0
|
||||
while i < len(g_red_nodes) {
|
||||
if g_red_rec[i] != null {
|
||||
any = true
|
||||
let n = itoa(i)
|
||||
let act = g_red_action[i]
|
||||
src = src + `export function {rr_wrap_name(i)}(q: mut LudicActions, s: mut {g_red_state[i]}, a: {act}) -> void {{\n`
|
||||
src = src + ` let h = a.{g_red_tgt[i]}\n let tb = s.{g_red_tbl[i]}\n var row = -1\n if tb != null {{ row = tb_row(tb, h) }}\n`
|
||||
src = src + ` if row < 0 {{\n ludic_act_stale(q, "{act}", "{g_red_rec[i]}", h)\n return\n }}\n`
|
||||
src = src + ` let r = q.row{n}\n r.tb = tb\n r.row = row\n r.h = h\n r.rec = tb_rec(tb, row)\n {g_red_nodes[i].s}(r, a)\n}}\n`
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if any {
|
||||
src = src + "@alloc_ok(\"a stale handle's line, printed only under LUDIC_ACTIONS_LOG\")\n"
|
||||
src = src + "export function ludic_act_stale(q: mut LudicActions, act: string, rec: string, h: int) -> void {\n"
|
||||
src = src + " if q.row_log < 0 {\n q.row_log = 0\n if Os.has_env(\"LUDIC_ACTIONS_LOG\") { q.row_log = 1 }\n }\n"
|
||||
src = src + " if q.row_log == 1 { print(`actions: {act} names a {rec} that is gone (handle {h}) - its row reducers run nothing`) }\n}\n"
|
||||
}
|
||||
return src
|
||||
}
|
||||
# a @RowVerb's record, from the row it takes first (null when it takes none: refused in the build)
|
||||
function rv_record(f: Node) -> pointer {
|
||||
var k = 0
|
||||
while k < len(f.kids) {
|
||||
let p = f.kids[k]
|
||||
if p.kind == N_PARAM {
|
||||
if rr_is_row_ty(p.ty) { return p.ty[4 .. len(p.ty)] }
|
||||
return null
|
||||
}
|
||||
k += 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -782,7 +782,7 @@ function mg_prune_unit(u: int, n: Node) -> void {
|
|||
let kind = g_mu_kind[u]
|
||||
if kind == 0 {
|
||||
if n.vw >= 0 { return }
|
||||
if s_has_prefix(n.s, "ludic_reduce__") { return } # a reducer takes its state whether it reads it or not
|
||||
if s_has_prefix(n.s, "ludic_reduce__") or s_has_prefix(n.s, "ludic_rowred__") { return } # a reducer takes its state whether it reads it or not
|
||||
var i = 0
|
||||
while i < len(n.kids) {
|
||||
let p = n.kids[i]
|
||||
|
|
|
|||
|
|
@ -1034,6 +1034,7 @@ function parse_one_decl_in() -> void {
|
|||
var esys_phase: pointer = null
|
||||
var ns_name: pointer = null # @Namespace(Name) — a package-provided Foo.* namespace (#62)
|
||||
var is_det = false # @deterministic — no floating point inside (emit_float.ludic)
|
||||
var is_rowverb = false # @RowVerb — a verb of a table's row (27.3, actions_rows.ludic)
|
||||
# attributes and `export` in any order: `@ToClients export event E` keeps its @ToClients
|
||||
while is_op("@") or (is_id("export") and (toks[pi + 1].kind == TK_ID or toks[pi + 1].text == "@")) {
|
||||
if is_id("export") {
|
||||
|
|
@ -1069,6 +1070,7 @@ function parse_one_decl_in() -> void {
|
|||
else if a == "EngineSystem" { eat_op("("); esys_comp = eat_id(); eat_op(","); esys_phase = eat_id(); eat_op(")") } # @EngineSystem(Comp, Phase) package engine system (#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 == "RowVerb" { is_rowverb = true }
|
||||
else if (a == "AppendOnly") or (a == "ByKey") { at_decl(a) } # a registry's, for editors (attrs.ludic)
|
||||
else if a == "PerMap" { # a map-scoped registry (permap.ludic)
|
||||
at_decl(a)
|
||||
|
|
@ -1191,6 +1193,7 @@ function parse_one_decl_in() -> void {
|
|||
if is_id("state") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_state(); return } # 0.S
|
||||
if is_id("action") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_action(); return } # 0.R
|
||||
if is_id("reducer") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "on") { parse_reducer(); return }
|
||||
if is_id("reducer") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "in") { parse_row_reducer(); return } # 27.3
|
||||
if is_id("var") {
|
||||
if not g_allow_globals {
|
||||
perr(`a module-level var is refused: a module's changing data is its state (state Name {{ ... }}), passed to the functions that use it - or, if it never changes, a let`)
|
||||
|
|
@ -1215,6 +1218,7 @@ function parse_one_decl_in() -> void {
|
|||
let f = parse_fn()
|
||||
if is_unsafe { f.uns = 1 }
|
||||
if is_det { push(g_det_names, f.s) }
|
||||
if is_rowverb { push(g_rv_nodes, f) }
|
||||
if g_aok_pending { push(g_alloc_ok_names, f.s); g_aok_pending = false }
|
||||
if g_res_pending_make != null { push(g_res_make_fn, f.s); push(g_res_make_kind, g_res_pending_make); g_res_pending_make = null }
|
||||
if g_res_pending_free != null { push(g_res_free_fn, f.s); push(g_res_free_kind, g_res_pending_free); g_res_pending_free = null }
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ function parse_component() -> Node {
|
|||
var is_frame = false # @frame — a fn stored here runs every frame (25.2)
|
||||
var cap = 0 # @max(n) — this list never holds more than n (25.5a)
|
||||
var fat = -1 # its attributes, for the schema (attrs.ludic)
|
||||
var is_target = false # @Target — an action's field naming a row (27.3)
|
||||
var is_column = false # @Column — a field a table's column mirrors (27.3)
|
||||
while is_op("@") { # one or more, on its line or the lines above
|
||||
pi += 1
|
||||
let ann = eat_id()
|
||||
|
|
@ -20,6 +22,11 @@ function parse_component() -> Node {
|
|||
if fat < 0 { fat = at_group() }
|
||||
at_push(fat, ann, "[]", null, 0)
|
||||
}
|
||||
else if (ann == "Target") or (ann == "Column") {
|
||||
if ann == "Target" { is_target = true } else { is_column = true }
|
||||
if fat < 0 { fat = at_group() }
|
||||
at_push(fat, ann, "[]", null, 0)
|
||||
}
|
||||
else if ann == "owns" { # @owns(PhysShape) — this field holds a handle its record owns (25.5e)
|
||||
eat_op("(")
|
||||
g_own_pending = eat_id()
|
||||
|
|
@ -49,6 +56,7 @@ function parse_component() -> Node {
|
|||
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
|
||||
f.at = fat
|
||||
if is_frame { push(g_frame_fields, `{n.s}.{f.s}`) }
|
||||
if is_target or is_column { rr_field_attr(n.s, f, is_target, is_column) }
|
||||
if cap > 0 { push(g_cap_keys, `{n.s}.{f.s}`); push(g_cap_vals, cap) }
|
||||
if g_own_pending != null { push(g_own_type, n.s); push(g_own_field, f.s); push(g_own_kind, g_own_pending); g_own_pending = null }
|
||||
if is_op("=") {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function vocab_keywords() -> void {
|
|||
vc_kw("declaration", "state", "A module's changing data: one instance, reached only as a parameter. Inside a machine, one of its states.")
|
||||
vc_kw("declaration", "event", "Something that happened, fired with emit and heard by @On listeners.")
|
||||
vc_kw("declaration", "action", "What a player asked for, dispatched and applied by each state's reducer.")
|
||||
vc_kw("declaration", "reducer", "Applies an action to one state: reducer HikerState on Move(h: mut HikerState, a: Move).")
|
||||
vc_kw("declaration", "reducer", "Applies an action to one state, or to the one row it names: reducer HikerState on Move(h: mut HikerState, a: Move); reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, a: Spook).")
|
||||
vc_kw("declaration", "enum", "A set of named integer variants.")
|
||||
vc_kw("declaration", "model", "An entity kind: the components an entity of it carries.")
|
||||
vc_kw("declaration", "prefab", "A model with preset component values.")
|
||||
|
|
@ -91,7 +91,7 @@ function vocab_keywords() -> void {
|
|||
vc_kw("statement", "else", "The branch taken when the if's condition does not hold.")
|
||||
vc_kw("statement", "while", "Loops while a condition holds.")
|
||||
vc_kw("statement", "for", "for i in a..b, or for (vars) in query [...].")
|
||||
vc_kw("statement", "in", "Separates a for's variables from what they range over; module m in layer L.")
|
||||
vc_kw("statement", "in", "Separates a for's variables from what they range over; module m in layer L; reducer T in S.table on A.")
|
||||
vc_kw("statement", "where", "A query's filter: for (p) in query [Pos] where p.x > 0.")
|
||||
vc_kw("statement", "match", "Picks the arm whose values match.")
|
||||
vc_kw("statement", "machine", "A state machine over a value, one block per state.")
|
||||
|
|
@ -131,7 +131,7 @@ function vocab_declarations() -> void {
|
|||
vc_decl("state", "state Name { field: Type = default, ... }", "A module's changing data, one instance, passed as a parameter.")
|
||||
vc_decl("event", "event [cancellable] Name { field: Type, ... }", "Something that happened.")
|
||||
vc_decl("action", "action Name { field: Type, ... }", "What a player asked for.")
|
||||
vc_decl("reducer", "reducer State on Action(st: mut State, reads..., a: Action) { ... }", "Applies an action to one state.")
|
||||
vc_decl("reducer", "reducer State on Action(st: mut State, reads..., a: Action) { ... } | reducer Record in State.table on Action(r: mut Row<Record>, reads..., a: Action) { ... }", "Applies an action to one state, or to the one row of a table its @Target names.")
|
||||
vc_decl("enum", "enum Name { A, B, C }", "Named integer variants.")
|
||||
vc_decl("model", "model Name { Component, ... }", "An entity kind.")
|
||||
vc_decl("prefab", "prefab Name of Model { Component { field: v } }", "A model with preset values.")
|
||||
|
|
@ -213,6 +213,8 @@ function vocab_attributes() -> void {
|
|||
vc_at("frame", "checked", "field", "", "A fn stored here runs every frame: a root of the frame-allocation count.")
|
||||
vc_at("max", "checked", "field", "(n)", "The most the list may hold; growing past it fails the run.")
|
||||
vc_at("owns", "checked", "field", "(Kind)", "The field holds a handle of that kind its record owns: releasing a sibling and losing it is counted.")
|
||||
vc_at("Target", "checked", "field", "", "An action's field holding the handle of the one table row it names: its row reducers run for that row alone.")
|
||||
vc_at("Column", "checked", "field", "", "A record's field a column of its table mirrors: a row reducer writes it only through a @RowVerb.")
|
||||
# a registry's
|
||||
vc_at("AppendOnly", "checked", "registry", "", "Entries are only ever appended: an entry's index is saved.")
|
||||
vc_at("ByKey", "checked", "registry", "", "Entries are saved by key, so their order is free.")
|
||||
|
|
@ -245,6 +247,7 @@ function vocab_attributes() -> void {
|
|||
vc_at("alloc_ok", "checked", "function handler statement", "(\"why\")", "Allocates in a frame on purpose; the reason is required.")
|
||||
vc_at("creates", "checked", "function", "(Kind)", "Returns a handle of that kind the caller must release.")
|
||||
vc_at("releases", "checked", "function", "(Kind)", "Releases a handle of that kind.")
|
||||
vc_at("RowVerb", "checked", "function", "", "A verb of a table's row, taking Row<T> first: the only function a row reducer hands its row to.")
|
||||
# read past and kept as a label
|
||||
vc_at("Handles", "recorded", "handler program", "(Name)", "Names what a handler (or a program) is responsible for; no effect.")
|
||||
vc_at("Reads", "recorded", "handler", "(Component, ...)", "Documents the components a handler reads; no effect.")
|
||||
|
|
@ -327,7 +330,7 @@ function vocab_check() -> void {
|
|||
}
|
||||
}
|
||||
function vc_field_attr(a: pointer) -> bool {
|
||||
return at_field_known(a) or (a == "Computed") or (a == "Sync") or (a == "frame") or (a == "max") or (a == "owns")
|
||||
return at_field_known(a) or (a == "Computed") or (a == "Sync") or (a == "frame") or (a == "max") or (a == "owns") or (a == "Target") or (a == "Column")
|
||||
}
|
||||
|
||||
function vc_list(b: Buf, kind: pointer, key: pointer) -> void {
|
||||
|
|
|
|||
116250
selfhost/ludicc.seed.ll
116250
selfhost/ludicc.seed.ll
File diff suppressed because one or more lines are too long
119749
selfhost/ludicc.win.seed.ll
119749
selfhost/ludicc.win.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -38,6 +38,9 @@
|
|||
"annot-node",
|
||||
"annot-oneof",
|
||||
"annot-owns",
|
||||
"annot-target",
|
||||
"annot-column",
|
||||
"annot-rowverb",
|
||||
"annot-permap",
|
||||
"annot-range",
|
||||
"annot-ref",
|
||||
|
|
|
|||
|
|
@ -262,13 +262,33 @@ function dp_pad(s: pointer, n: int) -> pointer {
|
|||
}
|
||||
# a function as its source names it: a reducer is `reducer Bag on PickUp`, not its symbol
|
||||
function dp_fn_name(n: pointer) -> pointer {
|
||||
if not s_starts(n, "ludic_reduce__") { return n }
|
||||
if not s_starts(n, "ludic_reduce__") and not s_starts(n, "ludic_rowred__") { return n }
|
||||
let rest = n[14 .. slen(n)]
|
||||
for i in 0 .. slen(rest) - 1 {
|
||||
if rest[i] == '_' and rest[i + 1] == '_' { return `reducer {rest[i + 2 .. slen(rest)]} on {rest[0 .. i]}` }
|
||||
if rest[i] == '_' and rest[i + 1] == '_' { return `reducer {dp_row_name(rest[i + 2 .. slen(rest)])} on {rest[0 .. i]}` }
|
||||
}
|
||||
return n
|
||||
}
|
||||
# 27.3: a row reducer's symbol says `Deer__in__Herd__deer` for `Deer in Herd.deer`
|
||||
function dp_row_name(s: pointer) -> pointer {
|
||||
var out = ""
|
||||
var seen = false
|
||||
var i = 0
|
||||
while i < slen(s) {
|
||||
if not seen and i + 6 <= slen(s) and s[i .. i + 6] == "__in__" {
|
||||
out = out + " in "
|
||||
seen = true
|
||||
i += 6
|
||||
} else if seen and i + 2 <= slen(s) and s[i .. i + 2] == "__" {
|
||||
out = out + "."
|
||||
i += 2
|
||||
} else {
|
||||
out = out + s[i .. i + 1]
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
function dp_own(k: int) -> bool { return dp_pkg[k] == 0 }
|
||||
# an edge the numbers count: between two of the program's own modules
|
||||
function dp_counted(e: int) -> bool { return dp_own(dp_ef[e]) and dp_own(dp_et[e]) }
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/privates_types.ludic")
|
||||
push(f, "selfhost/frontend/state.ludic")
|
||||
push(f, "selfhost/frontend/actions.ludic")
|
||||
push(f, "selfhost/frontend/actions_rows.ludic")
|
||||
push(f, "selfhost/frontend/migrate.ludic")
|
||||
push(f, "selfhost/frontend/registry.ludic")
|
||||
push(f, "selfhost/frontend/registry_finish.ludic")
|
||||
|
|
|
|||
|
|
@ -1241,6 +1241,17 @@ function cmd_dev_test() -> int {
|
|||
reject_case("rejected/reducer_two_states", "a reducer writes one state, and w is a mut Wallet", "a reducer writes exactly one state")
|
||||
reject_case("rejected/reducer_not_action", "Buy is not an action", "a reducer is on an action")
|
||||
reject_case("rejected/dispatch_unknown", "dispatch Sell: Sell is not an action", "only an action is dispatched")
|
||||
feat_case("actions/rows", "", "2 6 15 15", "rows.ludic (27.3: a reducer on a table's row - @Target, Row<T>, a @RowVerb, a follow-on for the same row, a stale handle runs nothing)")
|
||||
reject_case("rejected/row_reducer_no_target", "Spook names no row - mark the field that holds the row's handle @Target", "a row reducer's action names its row with @Target")
|
||||
reject_case("rejected/target_twice", "Swap has two @Target fields, a and b", "an action names one row")
|
||||
reject_case("rejected/row_reducer_column", "Deer.x is mirrored by a column of the table (@Column)", "a @Column field is written only by a @RowVerb")
|
||||
reject_case("rejected/row_reducer_reach", "r.tb: a row reducer reaches its row's record and handle", "a row reducer does not reach its table")
|
||||
reject_case("rejected/row_reducer_not_verb", "scare is handed a row (r: Row<Deer>), and is not a @RowVerb", "a row is handed only to a @RowVerb")
|
||||
reject_case("rejected/row_reducer_table", "Herd.count is int, not Table<Deer>", "a row reducer names a Table of its record")
|
||||
reject_case("rejected/row_reducer_params", "its parameters are its row, any states it reads, and the action", "a row reducer's row comes first and mut")
|
||||
reject_case("rejected/row_reducer_writes_state", "a row reducer writes its row alone, and n is a mut Noise", "a row reducer writes no state")
|
||||
reject_case("rejected/rowverb_no_row", "@RowVerb scare: a row verb takes the row first", "a @RowVerb takes its row first")
|
||||
reject_case("rejected/row_reducer_owner", "Herd is module herd's, and this reducer is in the program", "only the state's module reduces its table's rows")
|
||||
feat_case("state/counter", "", "11 3 heard 5", "counter.ludic (0.S: a state, mut and read-only parameters, entry, listener and fn-value injection)")
|
||||
feat_case("state/port_field", "", "40", "port_field.ludic (0.S: a port member bound to a state's field)")
|
||||
spec_case("state/tested", "== 2 passed, 0 failed ==")
|
||||
|
|
@ -1360,6 +1371,9 @@ function cmd_dev_test() -> int {
|
|||
schema_case("lang/i18n/i18n", "{\"english\": \"Close\", \"keys\": [\"map.close\", \"pause.close\"]}", "--emit-schema: lang.split - one English under several keys")
|
||||
schema_case("actions/modules", "{\"state\": \"Bag\", \"action\": \"PickUp\", \"module\": \"bag\", \"at\": \"examples/actions/mods/bag/index.ludic:4:16\"}", "--emit-schema: code.reducers - a reducer's state, action, module and place")
|
||||
schema_case("actions/modules", "{\"action\": \"PickUp\", \"module\": null, \"at\": \"examples/actions/modules.ludic:8:21\"}", "--emit-schema: code.dispatch - every dispatch site")
|
||||
schema_case("actions/rows", "{\"record\": \"Deer\", \"table\": \"Herd.deer\", \"state\": \"Herd\", \"action\": \"Spook\", \"target\": \"who\", \"predicted\": false, \"net\": null, \"module\": null, \"at\": \"examples/actions/rows.ludic:17:32\"}", "--emit-schema: code.row_reducers - a row reducer's record, table, action and target")
|
||||
schema_case("actions/rows", "{\"name\": \"deer_move\", \"record\": \"Deer\", \"module\": null, \"at\": \"examples/actions/rows.ludic:24:21\"}", "--emit-schema: code.row_verbs - a @RowVerb and its record")
|
||||
schema_case("actions/rows", "{\"name\": \"Spook\", \"module\": null, \"at\": \"examples/actions/rows.ludic:14:10\", \"target\": \"who\"}", "--emit-schema: code.actions - an action's @Target field")
|
||||
schema_case("modules/ports", "{\"name\": \"Clock\", \"module\": \"clock\", \"at\": \"examples/modules/station/clock/index.ludic:4:13\", \"bound\": true, \"members\": [{\"name\": \"now\", \"type\": \"fn()->int\", \"default\": null, \"required\": true}, {\"name\": \"day\", \"type\": \"fn()->int\", \"default\": \"fn first_day\", \"required\": false}]}", "--emit-schema: code.ports - a port's members, their fn types and defaults")
|
||||
schema_case("modules/ports", "{\"port\": \"Clock\", \"member\": \"now\", \"fn\": \"game_hours\", \"value\": \"fn game_hours\", \"module\": \"app\", \"at\": \"examples/modules/station/app/index.ludic:3:14\"}", "--emit-schema: code.binds - a bind's member, the function it names and where")
|
||||
schema_case("modules/ports", "{\"name\": \"clock\", \"package\": null, \"layer\": null, \"uses\": [\"units\"], \"uses_at\": \"examples/modules/station/clock/index.ludic:3\", \"friend\": null, \"files\": [\"examples/modules/station/clock/index.ludic\"]}", "--emit-schema: code.modules - a module's uses and files")
|
||||
|
|
|
|||
|
|
@ -883,6 +883,17 @@ program LudicLsp {
|
|||
let sidx = sym_add(LS_REDUCER, nt, parent)
|
||||
pD.syms[sidx].decl = "reducer"
|
||||
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
|
||||
if pis("in") { # 27.3: reducer Record in State.table on Action - "Record in State.table on Action"
|
||||
padv()
|
||||
let st = pname()
|
||||
if st >= 0 { pD.syms[sidx].name = pD.syms[sidx].name + " in " + ttext(pD, st) }
|
||||
while st >= 0 and pis(".") {
|
||||
padv()
|
||||
let ft = pname()
|
||||
if ft < 0 { break }
|
||||
pD.syms[sidx].name = pD.syms[sidx].name + "." + ttext(pD, ft)
|
||||
}
|
||||
}
|
||||
if pis("on") { padv(); let at = pname(); if at >= 0 { pD.syms[sidx].name = pD.syms[sidx].name + " on " + ttext(pD, at) } }
|
||||
pD.syms[sidx].detail = "reducer " + pD.syms[sidx].name
|
||||
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue