feat(lang): 0.R5 - a reducer writes one state and may read others, declared between its state and the action

What only becomes known inside the drain - a value another reducer just set, the map in play - no longer
has to be faked into the action, so a verb that reads several systems while it changes one is a reducer
instead of an act handed its states. A second state to write is still refused, and the message says the
way out. examples/actions/reads.ludic; reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-26 18:23:48 +03:00
parent dfcc851d2a
commit c64f8750e2
8 changed files with 48037 additions and 47917 deletions

View file

@ -360,12 +360,14 @@ program Pack {
- **`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.
- **`reducer State on Action(s: mut State, a: Action) { ... }`** WRITES exactly its state, and takes
the action last. Between the two it may declare states it only READS - `reducer Wallet on Buy(w:
mut Wallet, s: Shop, a: Buy)` - supplied like its own, for what is only known inside the drain (a
price another reducer just set, the map in play, where the save lives). A second state to write
is refused (`reducer Pack on Buy: a reducer writes one state, and w is a mut Wallet - read it (w:
Wallet), or dispatch an action Wallet's own reducer takes`), and so is a call inside it to a
function that writes another. What the dispatcher knows rides in the action. 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.

8
changes/reducer-reads.md Normal file
View file

@ -0,0 +1,8 @@
bump: minor
type: feat
**A reducer may read other states.** `reducer Wallet on Buy(w: mut Wallet, s: Shop, a: Buy)` writes
its own state and reads the ones declared between it and the action, supplied by the drain like its
own. What only becomes known inside the drain - a value another reducer just set, the map in play,
where the save lives - no longer has to be faked into the action, and a verb that reads several
systems while it changes one is a reducer instead of an act. A second state to WRITE is still
refused, now with the way out in the message (`examples/actions/reads.ludic`).

View file

@ -0,0 +1,23 @@
# reads.ludic - 0.R5: a reducer writes one state and may READ others, declared between its state and
# the action: what only becomes known inside the drain (a price another reducer set) need not ride in
# the action, and still no reducer changes a state it does not own.
program ReducerReads {
state Shop { price: int = 3 }
state Wallet { cash: int = 10 }
state Pack { n: int = 0 }
action Rise { by: int }
action Buy { }
reducer Shop on Rise(s: mut Shop, a: Rise) { s.price += a.by }
reducer Wallet on Buy(w: mut Wallet, s: Shop, a: Buy) {
if w.cash >= s.price { w.cash -= s.price }
}
reducer Pack on Buy(p: mut Pack, w: Wallet, a: Buy) { p.n += 1 }
entry (s: Shop, w: Wallet, p: Pack) {
dispatch Rise { by: 2 }
dispatch Buy { }
drain_actions()
print(`{s.price} {w.cash} {p.n}`)
}
}

View file

@ -1,8 +1,8 @@
# 0.R: a reducer takes its own state and the action; what else it needs rides in the action
# 0.R: a reducer writes its own state and may read others; a second state to write is refused
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 }
reducer Pack on Buy(p: mut Pack, w: mut Wallet, a: Buy) { p.n += 1 }
entry { print(1) }
}

View file

@ -8,9 +8,9 @@
#
# `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.
# phase, or where the program calls drain_actions(). A reducer writes exactly its own state and may
# READ others, declared between it 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.
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
@ -126,16 +126,16 @@ function red_check(i: int) -> void {
if f.kids[k].kind == N_PARAM { push(ps, f.kids[k]) }
k += 1
}
k = 0
while k < len(ps) {
# between its state and the action: states it only READS, never another to write
k = 1
while k < len(ps) - 1 {
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`)
}
if not is_state_ty(p.ty) { act_err(f, `reducer {st} on {act}: {p.s} is a {p.ty} - between its state and the action a reducer takes only states it reads; what it needs to know rides in the action`) }
if p.uns == 1 { act_err(f, `reducer {st} on {act}: a reducer writes one state, 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
}
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})`)
if len(ps) < 2 or not (ps[0].ty == st) or not (ps[len(ps) - 1].ty == act) {
act_err(f, `reducer {st} on {act}: its parameters are its state, any states it reads, and the action - ({reg_lower(st)}: mut {st}, a: {act})`)
}
var j = 0
while j < i {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1013,6 +1013,7 @@ function cmd_dev_test() -> int {
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)")
controller_case("actions/ui_press", "", "5 10", "ui_press.ludic (0.R4: an action a UI press dispatches is reduced before ludic.ui hands control back)")
feat_case("actions/reads", "", "5 5 1", "reads.ludic (0.R5: a reducer reads other states, declared between its own and the action)")
feat_case("actions/modules", "", "2", "modules.ludic (0.R: an action in one module, its reducer in another)")
actions_deps_case()
deps_reach_case()
@ -1020,7 +1021,7 @@ function cmd_dev_test() -> int {
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/registry_count_key", "def Tools count: its constant would be TL_COUNT", "a registry key named count is refused")
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_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("state/counter", "", "11 3 heard 5", "counter.ludic (0.S: a state, mut and read-only parameters, entry, listener and fn-value injection)")