feat(lang): 27.3 - a reducer on a table's row: reducer T in S.table on A, @Target, Row<T>, @RowVerb, @Column
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, S.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 the queue keeps, one per row reducer, filled in place, so a targeted action allocates nothing; a stale handle runs nothing, and LUDIC_ACTIONS_LOG=1 prints a line for it (@alloc_ok). Row reducers order among an action's by their state's name, then the table's path. Checked at compile time (actions_rows.ludic): the row reaches r.rec and r.h only - r.tb / r.row refused, the view never assigned, stored, copied or handed on except to a @RowVerb (a function of the record's own module taking Row<T> first; any other function taking a row is refused); a field marked @Column (a table column mirrors it) is not written through r.rec; only the module owning the state declares a row reducer; one @Target, an int, per action; the 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 left out of `reducers`. ludic deps and ludic-lsp name a row reducer `reducer Deer in Herd.deer on Spook`. vocab: @Target, @Column, @RowVerb; docs/language pages; LANGUAGE.md "A reducer on a row"; examples actions/rows and ten rejects; test.ludic feat, reject and schema cases (not run); changes/row-reducers.md. Reseeded; bootstrap-cfree fixpoint holds (307497 lines); Maroon Lake's `ludic build --check` is clean against this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d22abae476
commit
ab0b84b87e
37 changed files with 125923 additions and 110956 deletions
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue