diff --git a/packages/ludic.base/tests/actions_test.ludic b/packages/ludic.base/tests/actions_test.ludic index a2f92370..c7055351 100644 --- a/packages/ludic.base/tests/actions_test.ludic +++ b/packages/ludic.base/tests/actions_test.ludic @@ -11,6 +11,15 @@ program ActionsTest { reducer Door on Knock(d: mut Door, a: Knock) { if a.times >= 2 { d.open = true } } + state Tally { + sum: int = 0 + words: string = "" + } + action Note { n: int = 7, word: string = "hi" } + reducer Tally on Note(t: mut Tally, a: Note) { + t.sum = t.sum + a.n + t.words = t.words + a.word + } 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" } @@ -27,4 +36,16 @@ program ActionsTest { expect(d.open) expect_eq(d.seen, "o") } + test "a dispatched record is kept and filled again: what a dispatch leaves out is its default, not the last one's" (t: Tally) { + dispatch Note { n: 1, word: "a" } + dispatch Note { n: 2, word: "b" } + drain_actions() + expect_eq(t.sum, 3) + dispatch Note { } + dispatch Note { word: "c" } + dispatch Note { n: 5 } + drain_actions() + expect_eq(t.sum, 3 + 7 + 7 + 5) + expect_eq(t.words, "abhichi") + } } diff --git a/selfhost/backend/emit_call.ludic b/selfhost/backend/emit_call.ludic index 52e134bc..300509de 100644 --- a/selfhost/backend/emit_call.ludic +++ b/selfhost/backend/emit_call.ludic @@ -1060,7 +1060,7 @@ function emit_expr(e: Node) -> Val { if e.kind == E_STR { return val(emit_str_const(e.s), "string") } if e.kind == E_NEW { if is_slice_ty(e.s) { return emit_new_slice(e.s) } - return emit_new_struct(e.s, e.a) + return emit_new_struct(e.s, e.a, e.b) } if e.kind == E_LIST { return emit_list(e) } # [a, b, c] -> a fresh slice # fn name -> the function's address, for a worker entry point. The OS-thread runtime calls it diff --git a/selfhost/backend/emit_new.ludic b/selfhost/backend/emit_new.ludic index 63d71c65..1bc48a58 100644 --- a/selfhost/backend/emit_new.ludic +++ b/selfhost/backend/emit_new.ludic @@ -23,14 +23,16 @@ function rec_field(rec: Node, fname: pointer) -> Node { # `new T` / `new T { field: value, ... }` — allocate a record and seed each # field: a value from the override record if given, else the field's declared # default. `rec` is the E_REC of overrides (or null for the bare `new T`). -function emit_new_struct(name: pointer, rec: Node) -> Val { +# `into`: a record to fill in place of a fresh one (a dispatch's, kept by the action queue) +function emit_new_struct(name: pointer, rec: Node, into: Node) -> Val { let s = layout_node(name) # a struct or a property — same shape if (s == null) and port_find(name) >= 0 { perr(`{name} is a port: it is filled once with 'bind {name} {{ ... }}' where the program is put together, not made with new`) } if (s == null) { perr(`unknown record type {name} in new`) } vis_check(s, name) let lty = layout_ty(name) let sz = emit_sizeof(lty) - let obj = emit_bind(`call ptr @malloc(i64 {sz})`) + var obj = "" + if into != null { obj = emit_expr(into).code } else { obj = emit_bind(`call ptr @malloc(i64 {sz})`) } var f = 0 while f < len(s.kids) { let fd = s.kids[f] diff --git a/selfhost/frontend/actions.ludic b/selfhost/frontend/actions.ludic index e9790a05..cee678c3 100644 --- a/selfhost/frontend/actions.ludic +++ b/selfhost/frontend/actions.ludic @@ -26,6 +26,7 @@ const ACTION_PASSES: int = 64 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) @@ -55,6 +56,14 @@ function parse_dispatch() -> Node { 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" @@ -145,13 +154,20 @@ function red_check(i: int) -> void { } # 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" + 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 + "}\n" + src = src + act_new_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" - var a = 0 + a = 0 while a < len(g_act_names) { let rs = red_sorted(g_act_names[a]) if len(rs) > 0 { @@ -165,7 +181,13 @@ function act_src() -> pointer { } a += 1 } - src = src + " }\n List.clear(q.kinds)\n List.clear(q.vals)\n q.head = 0\n q.depth = 0\n}\n" # in place: a drain a phase, seven a frame, allocates nothing + 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) { @@ -175,6 +197,19 @@ function act_src() -> pointer { 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 +} function red_sorted(act: pointer) -> []int { let out = new []int var i = 0 @@ -198,5 +233,5 @@ function red_sorted(act: pointer) -> []int { # 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") + return (name == "drain_actions") or (name == "ludic_act_push") or str_starts(name, "ludic_act_new__") }