ludic.base's Queue<T> carries a QueueTag (its name and pending count): queue_new(name), q_push(q, v), q_drain(q), q_clear(q) take no BaseState, and core_undrained(tags) names the given queues still holding facts. A reducer on a package's state can now call that package's verbs (wallet_earn(wallet_st, n)). ludic migrate state --prune (ludicc --migrate-prune) takes out each state parameter a function no longer uses, nor anything it calls, and the argument that fills it - including an argument for a parameter the callee has dropped, which is taken out before the call is checked, so a generic's T is told by the argument that says it. A reducer keeps its state. --dry-run now counts the edits it would make. Every package was moved with it: 608 base_st parameters and their arguments, 1889 edits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
6.2 KiB
Text
203 lines
6.2 KiB
Text
# state.ludic — 0.S: no function writes a global. A module's changing data is a `state` record:
|
|
#
|
|
# state Hiker { hips: int = -1, spine: int = -1 }
|
|
#
|
|
# The program holds exactly one instance of each, which no code can name - it is the hidden global
|
|
# `state$Hiker` (a name the lexer cannot produce). It reaches code only as a parameter, `h: mut
|
|
# Hiker` to change it or `h: Hiker` to read it, and the runtime supplies it at the entry points:
|
|
# - a body that declares it: `entry (h: mut Hiker)`, `handler Draw(h: Hiker) phase Render`,
|
|
# `@On(E) handler Heard(h: mut Hiker)`, `test "name" (h: mut Hiker)` - each is a `let` at the
|
|
# body's top bound to the instance;
|
|
# - a function value: `fn f` of a function whose leading parameters are states is that function
|
|
# with them supplied (emit_fnval.ludic writes the thunk), so a system, a port's bind and any
|
|
# callback a package calls are entry points without saying so;
|
|
# - a call the compiler writes: a namespace method's target, a runtime built-in.
|
|
# A module-level `var` is refused (a `state` holds it), unless the build says --globals (the
|
|
# toolchain's own programs, which are not part of this); a module-level `let` is deeply immutable.
|
|
var g_state_names: []pointer = new []pointer
|
|
var g_allow_globals: bool = false
|
|
var g_gen_nodes: bool = false # the finishing passes are writing code: its nodes are generated
|
|
|
|
# a runtime function called from outside the runtime: a built-in, whose states are the runtime's
|
|
function is_runtime_file(f: pointer) -> bool { return f != null and has_sub(f, "runtime/native/") }
|
|
function state_inject_runtime(e: Node, f: Node) -> void {
|
|
if e != null and f != null and is_runtime_file(f.file) and not is_runtime_file(e.file) { state_inject(e, f) }
|
|
}
|
|
# a call the compiler wrote itself (a view's model, a component's glue): its callee's states are
|
|
# supplied - the runtime's side of an entry point
|
|
function state_inject_generated(e: Node, f: Node) -> void {
|
|
if e != null and e.pos < 0 { state_inject(e, f) }
|
|
}
|
|
# `(h: mut Hiker)` copied onto the front of a function's parameters (a view's functions)
|
|
function state_params_onto(f: Node, ps: []Node) -> void {
|
|
if len(ps) == 0 { return }
|
|
let kids = new []Node
|
|
var i = 0
|
|
while i < len(ps) {
|
|
let p = node(N_PARAM)
|
|
p.s = ps[i].s
|
|
p.ty = ps[i].ty
|
|
p.uns = ps[i].uns
|
|
p.file = ps[i].file
|
|
p.line = ps[i].line
|
|
push(kids, p)
|
|
i += 1
|
|
}
|
|
i = 0
|
|
while i < len(f.kids) {
|
|
push(kids, f.kids[i])
|
|
i += 1
|
|
}
|
|
f.kids = kids
|
|
}
|
|
|
|
function state_global(name: pointer) -> pointer { return `state${name}` }
|
|
function is_state_ty(t: pointer) -> bool {
|
|
if t == null { return false }
|
|
var i = 0
|
|
while i < len(g_state_names) {
|
|
if (g_state_names[i] == t) { return true }
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
# state NAME { fields } - a record, and its one instance
|
|
function parse_state() -> void {
|
|
let c = parse_component()
|
|
c.uns = 2
|
|
push(prog, c)
|
|
push(g_state_names, c.s)
|
|
let v = node(N_VAR)
|
|
v.s = state_global(c.s)
|
|
v.ty = c.s
|
|
v.line = c.line
|
|
v.uns = 1
|
|
let nw = node(E_NEW)
|
|
nw.s = c.s
|
|
nw.file = c.file
|
|
nw.line = c.line
|
|
v.a = nw
|
|
push(prog, v)
|
|
}
|
|
# `name: mut T` in a parameter list: the `mut`, when it is there
|
|
function parse_param_mut(p: Node) -> void {
|
|
p.pos2 = toks[pi].pos # where `mut ` goes, when a migration adds it
|
|
if is_id("mut") and toks[pi + 1].kind == TK_ID {
|
|
pi += 1
|
|
p.uns = 1
|
|
}
|
|
}
|
|
# `(h: mut Hiker, ...)` before an entry point's body: the states the runtime supplies, as the
|
|
# `let`s that open the body
|
|
function parse_entry_params() -> []Node {
|
|
let out = new []Node
|
|
if not is_op("(") { return out }
|
|
pi += 1
|
|
while not is_op(")") {
|
|
skipnl()
|
|
let p = node(N_PARAM)
|
|
p.s = eat_id()
|
|
eat_op(":")
|
|
parse_param_mut(p)
|
|
p.ty = ptype()
|
|
push(out, p)
|
|
skipnl()
|
|
if is_op(",") { pi += 1 }
|
|
}
|
|
eat_op(")")
|
|
return out
|
|
}
|
|
# a call the compiler writes (a namespace method's target, a runtime built-in) to a function whose
|
|
# leading parameters are states: the instances, unless the call already gives every parameter
|
|
function state_inject(e: Node, f: Node) -> void {
|
|
if f == null or e == null { return }
|
|
var k = 0
|
|
var total = 0
|
|
var i = 0
|
|
while i < len(f.kids) {
|
|
if f.kids[i].kind == N_PARAM {
|
|
if total == k and is_state_ty(f.kids[i].ty) { k += 1 }
|
|
total += 1
|
|
}
|
|
i += 1
|
|
}
|
|
if k == 0 or len(e.kids) + k > total { return }
|
|
var named = false # `f(x: 1)`: the states go in by name too
|
|
i = 0
|
|
while i < len(e.kids) {
|
|
if e.kids[i].kind == E_FINIT { named = true }
|
|
i += 1
|
|
}
|
|
let args = new []Node
|
|
i = 0
|
|
while i < k {
|
|
let id = node(E_ID)
|
|
id.s = state_global(f.kids[i].ty)
|
|
id.file = e.file
|
|
id.line = e.line
|
|
if named {
|
|
let fi = node(E_FINIT)
|
|
fi.s = f.kids[i].s
|
|
fi.a = id
|
|
push(args, fi)
|
|
} else { push(args, id) }
|
|
i += 1
|
|
}
|
|
i = 0
|
|
while i < len(e.kids) {
|
|
push(args, e.kids[i])
|
|
i += 1
|
|
}
|
|
e.kids = args
|
|
}
|
|
# a retained `ui` block is an entry point: `font: MenuState.title_font` reads the instance
|
|
function ui_state_refs(n: Node) -> void {
|
|
if n == null { return }
|
|
if n.kind == E_MEMBER and n.a != null and n.a.kind == E_ID and is_state_ty(n.a.s) { n.a.s = state_global(n.a.s) }
|
|
ui_state_refs(n.a)
|
|
ui_state_refs(n.b)
|
|
ui_state_refs(n.c)
|
|
var i = 0
|
|
while i < len(n.kids) {
|
|
ui_state_refs(n.kids[i])
|
|
i += 1
|
|
}
|
|
}
|
|
function ui_blocks_states() -> void {
|
|
var i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_UI and prog[i].ival == 1 { ui_state_refs(prog[i].a) }
|
|
i += 1
|
|
}
|
|
}
|
|
function entry_bind(body: Node, ps: []Node) -> void {
|
|
if len(ps) == 0 or body == null { return }
|
|
let lets = new []Node
|
|
var i = 0
|
|
while i < len(ps) {
|
|
let p = ps[i]
|
|
let l = node(S_LET)
|
|
l.s = p.s
|
|
l.ty = p.ty
|
|
l.file = p.file
|
|
l.line = p.line
|
|
l.ival = 0
|
|
l.uns = p.uns # 1: a `mut` state; 0: read-only
|
|
l.tps = "state" # an entry point's state: checked to be one
|
|
l.pos2 = p.pos2
|
|
l.pos = p.pos
|
|
let id = node(E_ID)
|
|
id.s = state_global(p.ty)
|
|
id.file = p.file
|
|
id.line = p.line
|
|
l.a = id
|
|
push(lets, l)
|
|
i += 1
|
|
}
|
|
var k = 0
|
|
while k < len(body.kids) {
|
|
push(lets, body.kids[k])
|
|
k += 1
|
|
}
|
|
body.kids = lets
|
|
}
|