feat(lang): 0.R1 - actions and reducers
action Name { fields } is a typed record; reducer State on Action(s: mut State, a: Action) { ... }
in the module that owns the state takes exactly that state and the action (a second state is
refused); dispatch Action { fields } queues one from anywhere, the queue supplied by the runtime.
The queue is drained at the end of every phase of the frame loop, after every phase of ludic.base's
core_tick_all, and by drain_actions(): in dispatch order, each action's reducers in the order of
their states' names, an action a reducer dispatches queued behind, a queue still growing after 64
rounds stopped with the action named. Examples actions/pack, phases, runaway; rejects for a second
state, a reducer on a non-action and an unknown dispatch; ludic.base's actions_test; LANGUAGE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3a87d02696
commit
808c4a6f7a
19 changed files with 415 additions and 1 deletions
56
LANGUAGE.md
56
LANGUAGE.md
|
|
@ -314,6 +314,62 @@ ludic migrate state packages <every example program> packages/ludic.lab/example/
|
||||||
migrate: 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files
|
migrate: 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Actions and reducers (`action`, `reducer`, `dispatch`)
|
||||||
|
|
||||||
|
Threading states makes a function's signature say what it touches, and it shows where one function
|
||||||
|
does everything: an input handler that reads the keys and then changes the world itself takes every
|
||||||
|
state the world has. An action separates the two. The input says WHAT happened; each module decides
|
||||||
|
what that means for its own state, and nothing else:
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Pack {
|
||||||
|
state Bag {
|
||||||
|
items: []int = new []int
|
||||||
|
weight: int = 0
|
||||||
|
}
|
||||||
|
state Log { lines: []string = new []string }
|
||||||
|
action PickUp { item: int, kg: int = 1 } # what happened: a typed record
|
||||||
|
|
||||||
|
reducer Bag on PickUp(b: mut Bag, a: PickUp) { # in the module that owns Bag
|
||||||
|
push(b.items, a.item)
|
||||||
|
b.weight += a.kg
|
||||||
|
}
|
||||||
|
reducer Log on PickUp(l: mut Log, a: PickUp) { push(l.lines, `picked {a.item}`) }
|
||||||
|
|
||||||
|
handler Keys phase Input {
|
||||||
|
if Input.key() == 'e' { dispatch PickUp { item: 7 } } # a translator: keys to actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`action Name { fields }`** is a record, with defaults like any. `export action` for other
|
||||||
|
modules to dispatch it.
|
||||||
|
- **`reducer State on Action(s: mut State, a: Action) { ... }`** takes exactly its state and the
|
||||||
|
action, in that order. A second state is refused (`reducer Pack on Buy: a reducer takes one state,
|
||||||
|
and w is a Wallet - what it needs to know rides in the action`), and so is a call inside it to a
|
||||||
|
function that needs another, since nothing supplies one there. What it needs to know rides in the
|
||||||
|
action, filled by whoever dispatches it. Several reducers may handle one action, one per state;
|
||||||
|
a reducer is not called by name.
|
||||||
|
- **`dispatch Action { fields }`** queues the action, from anywhere: a handler, a function, a
|
||||||
|
listener, a reducer. The queue is the runtime's, supplied like an entry point's state, so
|
||||||
|
dispatching needs no state parameter.
|
||||||
|
- **When the queue is drained:** at the end of every phase of the frame loop (so what the `Input`
|
||||||
|
phase dispatches is reduced before `Update`); after every phase of the ludic.base system runner
|
||||||
|
(`core_tick_all`); and wherever the program calls `drain_actions()` (an `entry` program, a test,
|
||||||
|
a loop of its own). Draining runs the actions in the order they were dispatched, and each action's
|
||||||
|
reducers in the order of their states' names - never the order of imports - so the same actions
|
||||||
|
make the same changes on every machine and in a replay.
|
||||||
|
- **An action a reducer dispatches** is queued behind the rest and reduced in the same drain, never
|
||||||
|
re-entrantly. A queue still growing after 64 rounds of that stops the program, naming the action:
|
||||||
|
`actions: Ping is still being dispatched after 64 rounds of reducers - a reducer dispatches what
|
||||||
|
dispatches it`.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
`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).
|
||||||
|
|
||||||
### Types are checked before anything is emitted
|
### Types are checked before anything is emitted
|
||||||
|
|
||||||
Between the parse and the emitter a checker walks every function, the entry, the tests, the
|
Between the parse and the emitter a checker walks every function, the entry, the tests, the
|
||||||
|
|
|
||||||
11
changes/actions.md
Normal file
11
changes/actions.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**Actions and reducers.** `action PickUp { item: int }` is a typed record of something that
|
||||||
|
happened; `reducer Bag on PickUp(b: mut Bag, a: PickUp) { ... }`, in the module that owns the state,
|
||||||
|
says what it means for that one state - a reducer takes exactly its state and the action, and a
|
||||||
|
second state is refused; `dispatch PickUp { item: 7 }` queues one from anywhere. The queue is drained
|
||||||
|
at the end of every phase of the frame loop, after every phase of ludic.base's `core_tick_all`, and
|
||||||
|
where a program calls `drain_actions()`: in dispatch order, each action's reducers in the order of
|
||||||
|
their states' names, an action a reducer dispatches queued behind (a queue still growing after 64
|
||||||
|
rounds stops the program, naming the action). `ludic deps` reports `widest_function` - the most
|
||||||
|
states any function or entry point of the program takes - and `--check` ratchets it.
|
||||||
38
examples/actions/pack.ludic
Normal file
38
examples/actions/pack.ludic
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# pack.ludic - 0.R: what happened is an action; what it means for a state is that state's reducer.
|
||||||
|
# The entry dispatches; drain_actions() runs every reducer of each action, in order, each given its
|
||||||
|
# own state - and an action a reducer dispatches goes behind the queue.
|
||||||
|
program PackActions {
|
||||||
|
state Pack {
|
||||||
|
items: []int = new []int
|
||||||
|
weight: int = 0
|
||||||
|
}
|
||||||
|
state Log {
|
||||||
|
lines: []string = new []string
|
||||||
|
}
|
||||||
|
action PickUp { item: int, kg: int = 1 }
|
||||||
|
action Drop { item: int }
|
||||||
|
action Overload {}
|
||||||
|
|
||||||
|
reducer Pack on PickUp(p: mut Pack, a: PickUp) {
|
||||||
|
push(p.items, a.item)
|
||||||
|
p.weight += a.kg
|
||||||
|
if p.weight > 10 { dispatch Overload {} }
|
||||||
|
}
|
||||||
|
reducer Log on PickUp(l: mut Log, a: PickUp) { push(l.lines, `picked {a.item}`) }
|
||||||
|
reducer Pack on Drop(p: mut Pack, a: Drop) {
|
||||||
|
var kept = new []int
|
||||||
|
for i in 0 .. len(p.items) { if p.items[i] != a.item { push(kept, p.items[i]) } }
|
||||||
|
p.items = kept
|
||||||
|
}
|
||||||
|
reducer Log on Overload(l: mut Log, a: Overload) { push(l.lines, "too heavy") }
|
||||||
|
|
||||||
|
entry (p: Pack, l: Log) {
|
||||||
|
dispatch PickUp { item: 7 }
|
||||||
|
dispatch PickUp { item: 9, kg: 12 }
|
||||||
|
dispatch Drop { item: 7 }
|
||||||
|
print(len(p.items)) # 0: nothing is reduced until the queue is drained
|
||||||
|
drain_actions()
|
||||||
|
print(`{len(p.items)} {p.items[0]} {p.weight}`)
|
||||||
|
for i in 0 .. len(l.lines) { print(l.lines[i]) }
|
||||||
|
}
|
||||||
|
}
|
||||||
24
examples/actions/phases.ludic
Normal file
24
examples/actions/phases.ludic
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# phases.ludic - 0.R: in the frame loop the queue is drained at the end of every phase, so what the
|
||||||
|
# Input phase dispatches is reduced before Update reads it. The input translates keys to actions and
|
||||||
|
# touches no state of its own.
|
||||||
|
program PhaseActions {
|
||||||
|
state Hero {
|
||||||
|
x: int = 0
|
||||||
|
jumps: int = 0
|
||||||
|
}
|
||||||
|
action Move { dx: int }
|
||||||
|
action Jump {}
|
||||||
|
reducer Hero on Move(h: mut Hero, a: Move) { h.x += a.dx }
|
||||||
|
reducer Hero on Jump(h: mut Hero, a: Jump) { h.jumps += 1 }
|
||||||
|
|
||||||
|
handler Keys phase Input {
|
||||||
|
let k = Input.key()
|
||||||
|
if k == 'd' { dispatch Move { dx: 1 } }
|
||||||
|
if k == 'a' { dispatch Move { dx: -1 } }
|
||||||
|
if k == ' ' { dispatch Jump {} }
|
||||||
|
if k == 'q' { quit() }
|
||||||
|
}
|
||||||
|
handler Show(h: Hero) phase Update {
|
||||||
|
print(`{h.x} {h.jumps}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
examples/actions/runaway.ludic
Normal file
15
examples/actions/runaway.ludic
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# runaway.ludic - 0.R: a reducer that dispatches what dispatches it is stopped, by name, after
|
||||||
|
# 64 rounds, rather than spinning
|
||||||
|
program Runaway {
|
||||||
|
state Echo { n: int = 0 }
|
||||||
|
action Ping {}
|
||||||
|
reducer Echo on Ping(e: mut Echo, a: Ping) {
|
||||||
|
e.n += 1
|
||||||
|
dispatch Ping {}
|
||||||
|
}
|
||||||
|
entry {
|
||||||
|
dispatch Ping {}
|
||||||
|
drain_actions()
|
||||||
|
print("not reached")
|
||||||
|
}
|
||||||
|
}
|
||||||
5
examples/rejected/dispatch_unknown.ludic
Normal file
5
examples/rejected/dispatch_unknown.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# 0.R: only an action is dispatched
|
||||||
|
program DispatchUnknown {
|
||||||
|
action Buy { cost: int }
|
||||||
|
entry { dispatch Sell { cost: 1 } }
|
||||||
|
}
|
||||||
7
examples/rejected/reducer_not_action.ludic
Normal file
7
examples/rejected/reducer_not_action.ludic
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# 0.R: a reducer is on an action
|
||||||
|
program ReducerNotAction {
|
||||||
|
state Pack { n: int = 0 }
|
||||||
|
property Buy { cost: int = 0 }
|
||||||
|
reducer Pack on Buy(p: mut Pack, a: Buy) { p.n += 1 }
|
||||||
|
entry { print(1) }
|
||||||
|
}
|
||||||
8
examples/rejected/reducer_two_states.ludic
Normal file
8
examples/rejected/reducer_two_states.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# 0.R: a reducer takes its own state and the action; what else it needs rides in the action
|
||||||
|
program ReducerTwoStates {
|
||||||
|
state Pack { n: int = 0 }
|
||||||
|
state Wallet { cash: int = 0 }
|
||||||
|
action Buy { cost: int }
|
||||||
|
reducer Pack on Buy(p: mut Pack, w: mut Wallet) { p.n += 1 }
|
||||||
|
entry { print(1) }
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,7 @@ export function core_tick_all(base_st: mut BaseState, t: Tick) -> void {
|
||||||
for i in 0 .. len(all) {
|
for i in 0 .. len(all) {
|
||||||
if all[i].phase == ph and all[i].tick != null { all[i].tick(t) }
|
if all[i].phase == ph and all[i].tick != null { all[i].tick(t) }
|
||||||
}
|
}
|
||||||
|
drain_actions() # 0.R: the phase's actions, reduced before the next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
30
packages/ludic.base/tests/actions_test.ludic
Normal file
30
packages/ludic.base/tests/actions_test.ludic
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# actions_test.ludic - 0.R: the runner drains the action queue after every phase, so what an input
|
||||||
|
# system dispatches is reduced before the simulation reads it, the same tick
|
||||||
|
import "ludic.base"
|
||||||
|
program ActionsTest {
|
||||||
|
numbers float
|
||||||
|
state Door {
|
||||||
|
open: bool = false
|
||||||
|
seen: string = ""
|
||||||
|
}
|
||||||
|
action Knock { times: int }
|
||||||
|
reducer Door on Knock(d: mut Door, a: Knock) {
|
||||||
|
if a.times >= 2 { d.open = true }
|
||||||
|
}
|
||||||
|
function input_tick(t: Tick) -> void { dispatch Knock { times: 2 } }
|
||||||
|
function sim_tick(d: mut Door, t: Tick) -> void {
|
||||||
|
if d.open { d.seen = d.seen + "o" } else { d.seen = d.seen + "c" }
|
||||||
|
}
|
||||||
|
test "an input system's action is reduced before the simulation's phase" (b: mut BaseState, d: Door) {
|
||||||
|
core_clear(b)
|
||||||
|
let i = system_new("input", PH_INPUT)
|
||||||
|
i.tick = fn input_tick
|
||||||
|
core_add(b, i)
|
||||||
|
let s = system_new("sim", PH_SIMULATE)
|
||||||
|
s.tick = fn sim_tick
|
||||||
|
core_add(b, s)
|
||||||
|
core_tick_all(b, tick_new(0.016, 1, 0.0))
|
||||||
|
expect(d.open)
|
||||||
|
expect(d.seen == "o")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -967,6 +967,7 @@ function emit_call(e: Node) -> Val {
|
||||||
vis_check(fn2, name)
|
vis_check(fn2, name)
|
||||||
state_inject_generated(e, fn2) # 0.S: a call the compiler wrote gets its states
|
state_inject_generated(e, fn2) # 0.S: a call the compiler wrote gets its states
|
||||||
state_inject_runtime(e, fn2) # and a call into the runtime
|
state_inject_runtime(e, fn2) # and a call into the runtime
|
||||||
|
if is_action_builtin(fn2.s) { state_inject(e, fn2) } # 0.R: the action queue
|
||||||
call_fill_defaults(e, fn2) # L11 - for code the checker does not walk
|
call_fill_defaults(e, fn2) # L11 - for code the checker does not walk
|
||||||
reorder_named(e, param_labels(fn2))
|
reorder_named(e, param_labels(fn2))
|
||||||
# evaluate args first (their IR is emitted before the call instruction), coercing
|
# evaluate args first (their IR is emitted before the call instruction), coercing
|
||||||
|
|
@ -1119,6 +1120,6 @@ function emit_expr(e: Node) -> Val {
|
||||||
let c = emit_bind(`icmp eq i32 {a.code}, 0`)
|
let c = emit_bind(`icmp eq i32 {a.code}, 0`)
|
||||||
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
||||||
}
|
}
|
||||||
perr("cannot emit expression")
|
perr(`cannot emit expression (node kind {itoa(e.kind)})`)
|
||||||
return val("0", "int")
|
return val("0", "int")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ function vis_check(d: Node, what0: pointer) -> void {
|
||||||
# 0.S: a state's instance, supplied by the runtime (a component's or a view's glue, an entry's
|
# 0.S: a state's instance, supplied by the runtime (a component's or a view's glue, an entry's
|
||||||
# parameters): the code that names it only passes it on
|
# parameters): the code that names it only passes it on
|
||||||
if d.kind == N_VAR and d.uns == 1 and is_state_ty(d.ty) and len(d.s) > 6 and (d.s[0 .. 6] == "state$") { return }
|
if d.kind == N_VAR and d.uns == 1 and is_state_ty(d.ty) and len(d.s) > 6 and (d.s[0 .. 6] == "state$") { return }
|
||||||
|
# 0.R: the action queue and the reducers are the runtime's to call, from any module
|
||||||
|
if d.kind == N_FN and (is_action_builtin(d.s) or (len(d.s) > 14 and (d.s[0 .. 14] == "ludic_reduce__"))) { return }
|
||||||
let what = vis_plain(what0)
|
let what = vis_plain(what0)
|
||||||
var here = g_err_file
|
var here = g_err_file
|
||||||
if here == null { return }
|
if here == null { return }
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,7 @@ function emit_calls_for_phase(phase: pointer) -> void {
|
||||||
emit_engine_systems_for_phase(phase) # engine-owned systems run after every user handler
|
emit_engine_systems_for_phase(phase) # engine-owned systems run after every user handler
|
||||||
emit_dyn_systems_for_phase(phase) # #64: mod-registered systems run last
|
emit_dyn_systems_for_phase(phase) # #64: mod-registered systems run last
|
||||||
if (phase == "Overlay") { emit_scene_menu_render() } # a scene's `shows` menu paints last
|
if (phase == "Overlay") { emit_scene_menu_render() } # a scene's `shows` menu paints last
|
||||||
|
if len(g_act_names) > 0 { emit(` call void {fn_sym("drain_actions")}()\n`) } # 0.R: this phase's actions, reduced
|
||||||
}
|
}
|
||||||
|
|
||||||
# the menu of the live `scene X shows Menu`, drawn on top of everything else
|
# the menu of the live `scene X shows Menu`, drawn on top of everything else
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ function ck_params(f: Node, labels: []pointer, tys: []pointer) -> void {
|
||||||
function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
|
function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
|
||||||
state_inject_generated(e, f)
|
state_inject_generated(e, f)
|
||||||
state_inject_runtime(e, f)
|
state_inject_runtime(e, f)
|
||||||
|
if is_action_builtin(f.s) { state_inject(e, f) } # 0.R: the queue is supplied
|
||||||
mg_call(e, f)
|
mg_call(e, f)
|
||||||
call_fill_defaults(e, f)
|
call_fill_defaults(e, f)
|
||||||
ck_state_args(e, f)
|
ck_state_args(e, f)
|
||||||
|
|
|
||||||
202
selfhost/frontend/actions.ludic
Normal file
202
selfhost/frontend/actions.ludic
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
# actions.ludic — 0.R: actions and reducers. What happened is an action, a typed record; what it
|
||||||
|
# means for a state is that state's reducer, in the module that owns it:
|
||||||
|
#
|
||||||
|
# action PickUp { item: int }
|
||||||
|
# reducer Pack on PickUp(p: mut Pack, a: PickUp) { push(p.items, a.item) }
|
||||||
|
# ...
|
||||||
|
# dispatch PickUp { item: 3 }
|
||||||
|
#
|
||||||
|
# `dispatch` queues the action; the queue is drained - every reducer of each action run in turn,
|
||||||
|
# each supplied its own state - at the end of every phase of the frame loop, after each ludic.base
|
||||||
|
# phase, or where the program calls drain_actions(). A reducer takes exactly its state and the
|
||||||
|
# action. Actions a reducer dispatches go behind the queue, never re-entrant, and a queue still
|
||||||
|
# growing after ACTION_PASSES rounds stops the program with the action that keeps coming.
|
||||||
|
var g_act_names: []pointer = new []pointer # the actions, in the order read
|
||||||
|
var g_act_nodes: []Node = new []Node
|
||||||
|
var g_red_nodes: []Node = new []Node # the reducers
|
||||||
|
var g_red_state: []pointer = new []pointer
|
||||||
|
var g_red_action: []pointer = new []pointer
|
||||||
|
var g_dsp_ids: []Node = new []Node # each dispatch's action number, filled at the end
|
||||||
|
var g_dsp_names: []pointer = new []pointer
|
||||||
|
var g_dsp_at: []Node = new []Node
|
||||||
|
const ACTION_PASSES: int = 64
|
||||||
|
|
||||||
|
# action NAME { fields } - a record
|
||||||
|
function parse_action() -> void {
|
||||||
|
let c = parse_component()
|
||||||
|
c.uns = 3
|
||||||
|
push(prog, c)
|
||||||
|
push(g_act_names, c.s)
|
||||||
|
push(g_act_nodes, c)
|
||||||
|
}
|
||||||
|
# reducer STATE on ACTION(s: mut STATE, a: ACTION) { ... }
|
||||||
|
function parse_reducer() -> void {
|
||||||
|
let st = toks[pi + 1].text
|
||||||
|
if toks[pi + 1].kind != TK_ID or not (toks[pi + 2].text == "on") or toks[pi + 3].kind != TK_ID {
|
||||||
|
perr("a reducer is `reducer State on Action(s: mut State, a: Action) { ... }`")
|
||||||
|
}
|
||||||
|
let act = toks[pi + 3].text
|
||||||
|
pi += 2
|
||||||
|
toks[pi].text = "function" # read the rest as a function named for the action
|
||||||
|
let f = parse_fn()
|
||||||
|
f.s = `ludic_reduce__{act}__{st}`
|
||||||
|
f.vis = 1
|
||||||
|
push(g_red_nodes, f)
|
||||||
|
push(g_red_state, st)
|
||||||
|
push(g_red_action, act)
|
||||||
|
push(prog, f)
|
||||||
|
}
|
||||||
|
# dispatch ACTION { fields } - a statement: the action made and queued
|
||||||
|
function parse_dispatch() -> Node {
|
||||||
|
let at = toks[pi]
|
||||||
|
pi += 1
|
||||||
|
let name = eat_id()
|
||||||
|
let nw = node(E_NEW)
|
||||||
|
nw.s = name
|
||||||
|
if is_op("{") { nw.a = record() }
|
||||||
|
let call = node(E_CALL)
|
||||||
|
let callee = node(E_ID)
|
||||||
|
callee.s = "ludic_act_push"
|
||||||
|
callee.pos = -1
|
||||||
|
call.a = callee
|
||||||
|
call.pos = -1 # the compiler's call: the queue is supplied
|
||||||
|
let id = node(E_INT)
|
||||||
|
id.pos = -1
|
||||||
|
push(call.kids, id)
|
||||||
|
push(call.kids, nw)
|
||||||
|
push(g_dsp_ids, id)
|
||||||
|
push(g_dsp_names, name)
|
||||||
|
push(g_dsp_at, nw)
|
||||||
|
let s = node(S_EXPR)
|
||||||
|
s.a = call
|
||||||
|
s.line = at.line
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
function act_index(name: pointer) -> int {
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_act_names) {
|
||||||
|
if (g_act_names[i] == name) { return i }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
function act_err(n: Node, msg: pointer) -> void {
|
||||||
|
g_err_file = n.file
|
||||||
|
g_err_line = n.line
|
||||||
|
g_parsing = false
|
||||||
|
perr(msg)
|
||||||
|
}
|
||||||
|
# after the program is read: the reducers checked, each dispatch numbered, and the queue, its push
|
||||||
|
# and its drain written
|
||||||
|
function actions_finish() -> void {
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_red_nodes) {
|
||||||
|
red_check(i)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
i = 0
|
||||||
|
while i < len(g_dsp_ids) {
|
||||||
|
let k = act_index(g_dsp_names[i])
|
||||||
|
if k < 0 { act_err(g_dsp_at[i], `dispatch {g_dsp_names[i]}: {g_dsp_names[i]} is not an action - declare it with action {g_dsp_names[i]} {{ ... }}`) }
|
||||||
|
g_dsp_ids[i].ival = k
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
var file = g_parse_file
|
||||||
|
var line = 1
|
||||||
|
if len(g_act_nodes) > 0 {
|
||||||
|
file = g_act_nodes[0].file
|
||||||
|
line = g_act_nodes[0].line
|
||||||
|
}
|
||||||
|
# no actions: drain_actions() is still there (a ludic.base runner calls it), and does nothing
|
||||||
|
if len(g_act_names) == 0 {
|
||||||
|
vw_parse("export function drain_actions() -> void {\n}\n", file, line, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vw_parse(act_src(), file, line, 1)
|
||||||
|
}
|
||||||
|
function red_check(i: int) -> void {
|
||||||
|
let f = g_red_nodes[i]
|
||||||
|
let st = g_red_state[i]
|
||||||
|
let act = g_red_action[i]
|
||||||
|
if act_index(act) < 0 { act_err(f, `reducer {st} on {act}: {act} is not an action - declare it with action {act} {{ ... }}`) }
|
||||||
|
if not is_state_ty(st) { act_err(f, `reducer {st} on {act}: {st} is not a state`) }
|
||||||
|
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
|
||||||
|
}
|
||||||
|
k = 0
|
||||||
|
while k < len(ps) {
|
||||||
|
let p = ps[k]
|
||||||
|
if k > 0 and is_state_ty(p.ty) {
|
||||||
|
act_err(f, `reducer {st} on {act}: a reducer takes one state, and {p.s} is a {p.ty} - what it needs to know rides in the action`)
|
||||||
|
}
|
||||||
|
k += 1
|
||||||
|
}
|
||||||
|
if len(ps) != 2 or not (ps[0].ty == st) or not (ps[1].ty == act) {
|
||||||
|
act_err(f, `reducer {st} on {act}: its parameters are its state and the action - ({reg_lower(st)}: mut {st}, a: {act})`)
|
||||||
|
}
|
||||||
|
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)})`) }
|
||||||
|
j += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# the queue, its push and its drain: each action's reducers in the order of their states' names
|
||||||
|
function act_src() -> pointer {
|
||||||
|
var src = "export state LudicActions {\n kinds: []int = new []int\n vals: []pointer = new []pointer\n head: int = 0\n depth: int = 0\n}\n"
|
||||||
|
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 { 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"
|
||||||
|
src = src + ` if pass >= {itoa(ACTION_PASSES)} {{ ludic_act_runaway(q.kinds[q.head]) }}\n }}\n`
|
||||||
|
src = src + " let k = q.kinds[q.head]\n let v = q.vals[q.head]\n q.head += 1\n"
|
||||||
|
var a = 0
|
||||||
|
while a < len(g_act_names) {
|
||||||
|
let rs = red_sorted(g_act_names[a])
|
||||||
|
if len(rs) > 0 {
|
||||||
|
src = src + ` if k == {itoa(a)} {{\n`
|
||||||
|
var r = 0
|
||||||
|
while r < len(rs) {
|
||||||
|
src = src + ` {g_red_nodes[rs[r]].s}(v)\n`
|
||||||
|
r += 1
|
||||||
|
}
|
||||||
|
src = src + " }\n"
|
||||||
|
}
|
||||||
|
a += 1
|
||||||
|
}
|
||||||
|
src = src + " }\n q.kinds = new []int\n q.vals = new []pointer\n q.head = 0\n q.depth = 0\n}\n"
|
||||||
|
src = src + "export function ludic_act_runaway(k: int) -> void {\n var name = \"?\"\n"
|
||||||
|
a = 0
|
||||||
|
while a < len(g_act_names) {
|
||||||
|
src = src + ` if k == {itoa(a)} {{ name = "{g_act_names[a]}" }}\n`
|
||||||
|
a += 1
|
||||||
|
}
|
||||||
|
src = src + " print(`actions: {name} is still being dispatched after " + itoa(ACTION_PASSES) + " rounds of reducers - a reducer dispatches what dispatches it`)\n exit(1)\n}\n"
|
||||||
|
return src
|
||||||
|
}
|
||||||
|
function red_sorted(act: pointer) -> []int {
|
||||||
|
let out = new []int
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_red_nodes) {
|
||||||
|
if (g_red_action[i] == act) { push(out, i) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
var a = 1
|
||||||
|
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]]) {
|
||||||
|
out[b + 1] = out[b]
|
||||||
|
b -= 1
|
||||||
|
}
|
||||||
|
out[b + 1] = x
|
||||||
|
a += 1
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
# a call to the queue from anywhere: the queue is the runtime's to supply, like a state an entry point
|
||||||
|
# is given (drain_actions() from a ludic.base runner or a program's own loop)
|
||||||
|
function is_action_builtin(name: pointer) -> bool {
|
||||||
|
return (name == "drain_actions") or (name == "ludic_act_push")
|
||||||
|
}
|
||||||
|
|
@ -422,6 +422,7 @@ function mg_call(e: Node, f: Node) -> void {
|
||||||
if not g_migrate or f == null or g_mg_cur < 0 { return }
|
if not g_migrate or f == null or g_mg_cur < 0 { return }
|
||||||
if e.pos < 0 { return } # a generated call gets its states when compiled
|
if e.pos < 0 { return } # a generated call gets its states when compiled
|
||||||
if is_runtime_file(f.file) and not is_runtime_file(e.file) { return } # the runtime's are supplied
|
if is_runtime_file(f.file) and not is_runtime_file(e.file) { return } # the runtime's are supplied
|
||||||
|
if is_action_builtin(f.s) { return } # 0.R: the action queue is supplied too
|
||||||
push(g_mc_call, e)
|
push(g_mc_call, e)
|
||||||
push(g_mc_unit, g_mg_cur)
|
push(g_mc_unit, g_mg_cur)
|
||||||
push(g_mc_callee, mg_unit(f, 0))
|
push(g_mc_callee, mg_unit(f, 0))
|
||||||
|
|
|
||||||
|
|
@ -468,6 +468,7 @@ function stmt_body() -> Node {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
if (t.text == "spawn") { return parse_spawn() }
|
if (t.text == "spawn") { return parse_spawn() }
|
||||||
|
if (t.text == "dispatch") and toks[pi + 1].kind == TK_ID { return parse_dispatch() } # 0.R
|
||||||
if (t.text == "machine") {
|
if (t.text == "machine") {
|
||||||
pi += 1; let n = node(S_MACHINE); n.a = expr(); skipnl(); eat_op("{")
|
pi += 1; let n = node(S_MACHINE); n.a = expr(); skipnl(); eat_op("{")
|
||||||
var sidx = 0 # states auto-number by declaration order
|
var sidx = 0 # states auto-number by declaration order
|
||||||
|
|
@ -1026,6 +1027,8 @@ function parse_one_decl() -> void {
|
||||||
if is_id("view") and (toks[pi + 1].kind == TK_ID) and ((toks[pi + 2].text == "{") or (toks[pi + 2].text == "(")) { parse_view(); return } # L11
|
if is_id("view") and (toks[pi + 1].kind == TK_ID) and ((toks[pi + 2].text == "{") or (toks[pi + 2].text == "(")) { parse_view(); return } # L11
|
||||||
if is_id("component") and (toks[pi + 1].kind == TK_ID) and ((toks[pi + 2].text == "{") or (toks[pi + 2].text == "(")) { parse_ui_component(); return } # L11
|
if is_id("component") and (toks[pi + 1].kind == TK_ID) and ((toks[pi + 2].text == "{") or (toks[pi + 2].text == "(")) { parse_ui_component(); return } # L11
|
||||||
if is_id("state") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_state(); return } # 0.S
|
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("var") {
|
if is_id("var") {
|
||||||
if not g_allow_globals {
|
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`)
|
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`)
|
||||||
|
|
@ -1572,5 +1575,6 @@ function parse_program() -> void {
|
||||||
views_finish() # L11: each view gets its model and call
|
views_finish() # L11: each view gets its model and call
|
||||||
components_finish() # L11: each component gets its class
|
components_finish() # L11: each component gets its class
|
||||||
ui_blocks_states() # 0.S: a ui block reads a state as State.field
|
ui_blocks_states() # 0.S: a ui block reads a state as State.field
|
||||||
|
actions_finish() # 0.R: the reducers checked, the queue written
|
||||||
g_gen_nodes = false
|
g_gen_nodes = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ function selfhost_frags() -> []pointer {
|
||||||
push(f, "selfhost/frontend/privates.ludic")
|
push(f, "selfhost/frontend/privates.ludic")
|
||||||
push(f, "selfhost/frontend/privates_types.ludic")
|
push(f, "selfhost/frontend/privates_types.ludic")
|
||||||
push(f, "selfhost/frontend/state.ludic")
|
push(f, "selfhost/frontend/state.ludic")
|
||||||
|
push(f, "selfhost/frontend/actions.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")
|
||||||
|
|
|
||||||
|
|
@ -945,6 +945,12 @@ function cmd_dev_test() -> int {
|
||||||
feat_case("modules/layers", "", "5", "layers.ludic (L3: `module menu in layer app uses items` - one layer's modules use each other and may go round)")
|
feat_case("modules/layers", "", "5", "layers.ludic (L3: `module menu in layer app uses items` - one layer's modules use each other and may go round)")
|
||||||
reject_case("rejected/layer_reach", "hud uses items.item_count", "a layered module is still held to its uses outside the layer")
|
reject_case("rejected/layer_reach", "hud uses items.item_count", "a layered module is still held to its uses outside the layer")
|
||||||
reject_case("rejected/layer_cycle", "go round in a circle: items -> layer app -> items", "a cycle through a layer and out of it is refused")
|
reject_case("rejected/layer_cycle", "go round in a circle: items -> layer app -> items", "a cycle through a layer and out of it is refused")
|
||||||
|
feat_case("actions/pack", "", "0 1 9 13 picked 7 picked 9 too heavy", "pack.ludic (0.R: actions, reducers - one per state, in the order of the states' names - dispatch, drain_actions, an action a reducer dispatches goes behind)")
|
||||||
|
feat_case("actions/phases", "dd a q", "1 0 2 0 2 1 1 1 1 2 1 2", "phases.ludic (0.R: the frame loop drains the queue after every phase - Input's actions are reduced before Update)")
|
||||||
|
feat_case("actions/runaway", "", "actions: Ping is still being dispatched after 64 rounds of reducers - a reducer dispatches what dispatches it", "runaway.ludic (0.R: a reducer that dispatches what dispatches it is stopped by name)")
|
||||||
|
reject_case("rejected/reducer_two_states", "a reducer takes one state, and w is a Wallet", "a reducer takes 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("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 ==")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue