# 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 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 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 var g_act_main: pointer = null # the program's own file, where the drain is written const ACTION_PASSES: int = 64 # action NAME { fields } - a record function parse_action() -> void { let c = parse_component() c.uns = 3 c.vis = 1 # the program's own file makes and keeps its records 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(g_red_rec, null) # a state's reducer: no row (actions_rows.ludic) push(g_red_mc, null) # ... and no machine wrote it (machines.ludic) push(g_red_tbl, null) push(g_red_tgt, null) push(prog, f) } # 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() } # its record is the queue's: one of the action's kept records, filled as `new` fills a fresh one let take = node(E_CALL) let tc = node(E_ID) tc.s = `ludic_act_new__{name}` tc.pos = -1 take.a = tc take.pos = -1 nw.b = take 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 { machines_finish() # 27.1: each @Machine checked and its reducers written rows_finish_checks() # 27.3: @Target, @RowVerb (actions_rows.ludic) var i = 0 while i < len(g_red_nodes) { if g_red_rec[i] != null { rr_check(i) } else { 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 } # the queue and the drain are the program's, written in its own file: in the file of the first # action they would belong to its module, and it would call every module's reducers var file = g_parse_file if g_act_main != null { file = g_act_main } let line = 1 # 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 } # 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 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[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 { 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 } } # 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" var a = 0 while a < len(g_act_names) { 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 + 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" 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" 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) { 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" } a += 1 } src = src + " }\n List.clear(q.kinds)\n List.clear(q.vals)\n q.head = 0\n q.depth = 0\n" # in place: a drain a phase, seven a frame, allocates nothing a = 0 while a < len(g_act_names) { src = src + ` q.used{itoa(a)} = 0\n` # every record read: each is free again a += 1 } src = src + "}\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 } # each action's next kept record, made only when all it has are queued: `dispatch` fills it in # place, and the drain hands them all back once every reducer has read them function act_new_src() -> pointer { var src = "" var a = 0 while a < len(g_act_names) { let n = g_act_names[a] let k = itoa(a) src = src + `export function ludic_act_new__{n}(q: mut LudicActions) -> {n} {{\n if q.used{k} >= len(q.kept{k}) {{ push(q.kept{k}, new {n}) }}\n q.used{k} += 1\n return q.kept{k}[q.used{k} - 1]\n}}\n` a += 1 } 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 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(red_key(x), red_key(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") or str_starts(name, "ludic_act_new__") }