wip(0.S1, 0.S2): state records, mut and read-only state parameters, entry injection, module var refused; ludic migrate state

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 13:45:09 +03:00
parent 63a1fa1378
commit 1e8b5b0523
35 changed files with 1227 additions and 30 deletions

View file

@ -66,6 +66,7 @@ function ns_alias_labels(i: int) -> []pointer {
return out
}
function emit_alias_call(i: int, e: Node) -> Val {
state_inject(e, find_fn(g_al_target[i])) # 0.S: the target's states, supplied
call_mixed_to_named(e, ns_alias_labels(i))
reorder_named(e, ns_alias_labels(i))
let id = node(E_ID)

View file

@ -89,6 +89,9 @@ property Node {
vis: int = 0 # L3: 1 when the declaration is `export`ed from its module
tps: pointer = null # L5: a generic declaration's type parameters, "T|U"; null when not generic
uns: int = 0 # L7: 1 on an `unsafe function`
pos: int = -1 # 0.S: byte offset of its first token in its file (-1: generated)
pos2: int = -1 # 0.S: a second place a rewrite needs (a declaration's end, a '(')
mg: int = -1 # 0.S2: its index in the migration's tables (migrate.ludic)
}
# every node remembers where it was parsed (file + the line of the token the
@ -98,6 +101,9 @@ function node(kind: int) -> Node {
n.kind = kind
n.kids = new []Node
n.file = g_parse_file
if g_parsing and pi < len(toks) { n.line = toks[pi].line }
if g_parsing and pi < len(toks) {
n.line = toks[pi].line
n.pos = toks[pi].pos
}
return n
}

View file

@ -11,13 +11,19 @@ const TK_EOF: int = 5
const TK_FLOAT: int = 6
const TK_INTERP: int = 7 # `text {expr} text` — raw content, split by the parser
property Tok { kind: int = 0, text: pointer = null, ival: int = 0, line: int = 0 }
property Tok { kind: int = 0, text: pointer = null, ival: int = 0, line: int = 0, pos: int = -1, end: int = -1 }
# where the tokens are in their file (0.S's migration rewrites source by position): a file's own
# text starts at 0, an interpolation hole at its place in the file, and generated code has none (-1)
var g_lex_base: int = -1
var lx_start: int = 0
var toks: []Tok
function tok_push(kind: int, text: pointer, ival: int, line: int) -> void {
let t = new Tok
t.kind = kind; t.text = text; t.ival = ival; t.line = line
if g_lex_base >= 0 { t.pos = g_lex_base + lx_start }
push(toks, t)
}
@ -102,7 +108,12 @@ function lex_fail(line: int, msg: pointer) -> void {
exit(1)
}
function lex(src: pointer) -> void { lex_at(src, 1) }
function lex(src: pointer) -> void {
let saved = g_lex_base
g_lex_base = 0
lex_at(src, 1)
g_lex_base = saved
}
# lex `src` with its first line numbered `first_line` (an interpolation hole is
# re-lexed on its own, and keeps the line of the string it sits in)
@ -111,7 +122,14 @@ function lex_at(src: pointer, first_line: int) -> void {
var i = 0
var line = first_line
let n = len(src)
var lx_n = 0
while i < n {
# the token the last turn pushed ends where this one starts looking
if len(toks) > lx_n {
if g_lex_base >= 0 { toks[len(toks) - 1].end = g_lex_base + i }
lx_n = len(toks)
}
lx_start = i
let c = src[i]
if c == '\n' { tok_push(TK_NL, null, 0, line); line += 1; i += 1; continue }
if c == ' ' or c == '\t' or c == '\r' { i += 1; continue }
@ -236,5 +254,7 @@ function lex_at(src: pointer, first_line: int) -> void {
if is_op1(c) { tok_push(TK_OP, src[i..i + 1], 0, line); i += 1; continue }
lex_error(line, c)
}
if len(toks) > lx_n and g_lex_base >= 0 { toks[len(toks) - 1].end = g_lex_base + i }
lx_start = i
tok_push(TK_EOF, null, 0, line)
}

View file

@ -0,0 +1,406 @@
# migrate.ludic — 0.S2: `ludicc --migrate-state <plan>` works out how a program's module-level
# vars become states and writes the edits as a plan (`ludic migrate state` applies it):
# 1. each module's vars are one state, `state <Module>State { ... }`, where the first of them was;
# 2. every reference to one - read or written - is `<module>_st.<var>`;
# 3. each function's states - those it touches and those of everything it calls, to a fixed
# point - become its leading parameters (`mut` where it or a callee writes);
# 4. each call passes them on, and each entry point (entry, a handler, a listener, a test)
# declares them for the runtime to supply.
# It runs inside the checker (check_*.ludic call the mg_* hooks), which already knows which name is a
# local and which a global, and which function a call reaches. Plan lines, one edit each:
# VAR <file> <pos> <pos2> <state> a declaration to move into <state>
# STATE <state> <file> <pos> <export 0|1> <param> where <state> is declared (the first var)
# REF <file> <pos> <len> <text> a reference's name replaced
# INS <file> <pos> <text> text inserted (a parameter, an argument)
# ? <file>:<line> <what> what the tool cannot decide, for a person
var g_migrate: bool = false
var g_mg_runtime: bool = false # --migrate-runtime: the runtime's own vars too
var g_mg_plan: pointer = null
var g_mg_vars: []Node = new []Node # the vars that move
var g_mg_vstate: []int = new []int # the state each moves into
var g_ms_name: []pointer = new []pointer
var g_ms_mod: []pointer = new []pointer
var g_ms_param: []pointer = new []pointer
var g_ms_export: []int = new []int
# units: the functions and entry points whose states are worked out
var g_mu_node: []Node = new []Node
var g_mu_kind: []int = new []int # 0 function, 1 entry block, 2 handler, 3 test, 4 listener
var g_mu_need: []pointer = new []pointer # ",0,3," state indices it needs
var g_mu_mut: []pointer = new []pointer # the ones it changes
var g_mg_cur: int = -1
var g_mc_call: []Node = new []Node # every call to a function, and where it is
var g_mc_unit: []int = new []int
var g_mc_callee: []int = new []int
var g_mr_node: []Node = new []Node # every reference to a moving var
var g_mr_var: []int = new []int
var g_mg_report: []pointer = new []pointer
function mg_camel(s: pointer) -> pointer {
var out = ""
var up = true
var i = 0
while i < len(s) {
let c = s[i]
if c == '_' or c == '.' or c == '-' { up = true } else {
var ch = s[i .. i + 1]
if up and c >= 'a' and c <= 'z' { ch = reg_upper(ch) }
out = out + ch
up = false
}
i += 1
}
return out
}
function mg_short(m: pointer) -> pointer {
if len(m) > 6 and (m[0 .. 6] == "ludic_") { return m[6 .. len(m)] }
return m
}
function mg_state_of_mod(m: pointer) -> int {
var i = 0
while i < len(g_ms_mod) {
if (g_ms_mod[i] == m) { return i }
i += 1
}
var base = mg_short(m)
if (m == "") { base = reg_lower(g_game_name) }
push(g_ms_mod, m)
push(g_ms_name, `{mg_camel(base)}State`)
push(g_ms_param, `{base}_st`)
push(g_ms_export, 0)
return len(g_ms_mod) - 1
}
function mg_is_target(v: Node) -> bool {
if v.kind != N_VAR or v.uns != 0 or v.file == null or v.pos < 0 { return false }
if reg_find(v.s) >= 0 or is_port_var(v) { return false }
if not g_mg_runtime and has_sub(v.file, "runtime/") { return false }
return true
}
# which vars move, and the state each moves into
function mg_collect() -> void {
var i = 0
while i < g_prog_user_end and i < len(prog) {
let d = prog[i]
if d.kind == N_VAR and mg_is_target(d) {
let k = mg_state_of_mod(module_for_uses(d.file))
push(g_mg_vars, d)
push(g_mg_vstate, k)
d.mg = len(g_mg_vars) - 1
if d.vis == 1 { g_ms_export[k] = 1 }
}
i += 1
}
}
function mg_unit(n: Node, kind: int) -> int {
if n.mg >= 0 and n.kind != N_VAR { return n.mg }
push(g_mu_node, n)
push(g_mu_kind, kind)
push(g_mu_need, ",")
push(g_mu_mut, ",")
n.mg = len(g_mu_node) - 1
# an already-migrated function: its declared states are needs
if kind == 0 {
var i = 0
while i < len(n.kids) and n.kids[i].kind == N_PARAM and is_state_ty(n.kids[i].ty) {
let s = mg_state_named(n.kids[i].ty)
if s >= 0 {
mg_need(n.mg, s, n.kids[i].uns == 1)
}
i += 1
}
}
return n.mg
}
function mg_state_named(name: pointer) -> int {
var i = 0
while i < len(g_ms_name) {
if (g_ms_name[i] == name) { return i }
i += 1
}
# a state declared by hand: known by its name, its param the name in snake case
push(g_ms_mod, `={name}`)
push(g_ms_name, name)
push(g_ms_param, `{reg_lower(name)}`)
push(g_ms_export, 0)
return len(g_ms_mod) - 1
}
function mg_need(u: int, s: int, m: bool) -> bool {
var changed = false
let key = `,{itoa(s)},`
if not has_sub(g_mu_need[u], key) {
g_mu_need[u] = g_mu_need[u] + itoa(s) + ","
changed = true
}
if m and not has_sub(g_mu_mut[u], key) {
g_mu_mut[u] = g_mu_mut[u] + itoa(s) + ","
changed = true
}
return changed
}
# ---- the checker's hooks --------------------------------------------------------
function mg_enter(n: Node, kind: int) -> void {
if not g_migrate { return }
g_mg_cur = mg_unit(n, kind)
}
function mg_leave() -> void { g_mg_cur = -1 }
function mg_ref(e: Node, g: Node) -> void {
if not g_migrate or g == null or g.kind != N_VAR or g.mg < 0 { return }
push(g_mr_node, e)
push(g_mr_var, g.mg)
if g_mg_cur >= 0 { mg_need(g_mg_cur, g_mg_vstate[g.mg], false) }
else { mg_say(e, `{g.s} is read where no function is (a global's initializer); move it by hand`) }
if e.pos < 0 { mg_say(e, `a reference to {g.s} in generated code (a component, a view, a registry's lookup)`) }
}
function mg_write(t: Node, g: Node) -> void {
if not g_migrate or g == null or g.kind != N_VAR or g.mg < 0 { return }
if g_mg_cur >= 0 { mg_need(g_mg_cur, g_mg_vstate[g.mg], true) }
}
function mg_call(e: Node, f: Node) -> void {
if not g_migrate or f == null or g_mg_cur < 0 { return }
push(g_mc_call, e)
push(g_mc_unit, g_mg_cur)
push(g_mc_callee, mg_unit(f, 0))
}
function mg_say(n: Node, what: pointer) -> void {
var at = "?"
if n != null and n.file != null { at = `{n.file}:{itoa(n.line)}` }
push(g_mg_report, `? {at} {what}`)
}
# ---- the fixed point, and the plan ---------------------------------------------------
function mg_fixpoint() -> void {
var changed = true
while changed {
changed = false
var c = 0
while c < len(g_mc_call) {
let from = g_mc_unit[c]
let to = g_mc_callee[c]
let need = mg_list(g_mu_need[to])
var k = 0
while k < len(need) {
let s = need[k]
if mg_need(from, s, has_sub(g_mu_mut[to], `,{itoa(s)},`)) { changed = true }
k += 1
}
c += 1
}
}
}
function mg_list(set: pointer) -> []int {
let out = new []int
var v = 0
var any = false
var i = 1
while i < len(set) {
let ch = set[i]
if ch == ',' {
if any { push(out, v) }
v = 0
any = false
} else {
v = v * 10 + (ch - 48)
any = true
}
i += 1
}
# in the order of the states' names, so every signature and every call agree
var a = 1
while a < len(out) {
let x = out[a]
var b = a - 1
while b >= 0 and reg_str_less(g_ms_name[x], g_ms_name[out[b]]) {
out[b + 1] = out[b]
b -= 1
}
out[b + 1] = x
a += 1
}
return out
}
# a function's states that it declares already (a migration run before this one)
function mg_declared(n: Node, s: int) -> bool {
if n.kind != N_FN { return false }
var i = 0
while i < len(n.kids) and n.kids[i].kind == N_PARAM and is_state_ty(n.kids[i].ty) {
if (n.kids[i].ty == g_ms_name[s]) { return true }
i += 1
}
return false
}
function mg_param_text(u: int, s: int) -> pointer {
if has_sub(g_mu_mut[u], `,{itoa(s)},`) { return `{g_ms_param[s]}: mut {g_ms_name[s]}` }
return `{g_ms_param[s]}: {g_ms_name[s]}`
}
var g_mg_lines: []pointer = new []pointer
var g_mg_keys: pointer = ","
function mg_emit(line: pointer, key: pointer) -> void {
if not (key == "") {
if has_sub(g_mg_keys, `,{key},`) { return }
g_mg_keys = g_mg_keys + key + ","
}
push(g_mg_lines, line)
}
# text for a plan line: spaces and newlines escaped
function mg_esc(s: pointer) -> pointer {
var out = ""
var i = 0
while i < len(s) {
let c = s[i]
if c == ' ' { out = out + "\\s" } else if c == '\n' { out = out + "\\n" } else if c == '\\' { out = out + "\\\\" } else { out = out + s[i .. i + 1] }
i += 1
}
return out
}
function mg_first_positional_is_state(e: Node) -> bool {
if len(e.kids) == 0 or e.kids[0].kind != E_ID { return false }
var i = 0
while i < len(g_ms_param) {
if (g_ms_param[i] == e.kids[0].s) { return true }
i += 1
}
return false
}
function mg_nparams(n: Node) -> int {
var k = 0
var i = 0
while i < len(n.kids) {
if n.kids[i].kind == N_PARAM { k += 1 }
i += 1
}
return k
}
function mg_units() -> void {
var u = 0
while u < len(g_mu_node) {
let n = g_mu_node[u]
let need = mg_list(g_mu_need[u])
if len(need) > 0 {
var text = ""
var k = 0
while k < len(need) {
if not mg_declared(n, need[k]) {
if len(text) > 0 { text = text + ", " }
text = text + mg_param_text(u, need[k])
}
k += 1
}
if len(text) > 0 { mg_unit_edit(u, n, text) }
}
u += 1
}
}
function mg_unit_edit(u: int, n: Node, text: pointer) -> void {
let kind = g_mu_kind[u]
if kind == 0 {
if n.pos2 < 0 or n.file == null {
mg_say(n, `{n.s} needs {text} and is generated code: its caller cannot be rewritten`)
return
}
var t = text
if mg_nparams(n) > 0 { t = t + ", " }
mg_emit(`INS {n.file} {itoa(n.pos2 + 1)} {mg_esc(t)}`, `{n.file}@{itoa(n.pos2)}`)
return
}
# an entry point: a list of the states the runtime supplies
var body = n.a
if kind == 4 { body = n.a }
if body != null and len(body.kids) > 0 and body.kids[0].tps != null and (body.kids[0].tps == "state") {
mg_say(n, `this entry point declares states already; add ({text}) to them by hand`)
return
}
if kind == 1 { mg_emit(`INS {n.file} {itoa(n.pos)} {mg_esc(`({text}) `)}`, `{n.file}@{itoa(n.pos)}`) }
if kind == 2 { mg_emit(`INS {n.file} {itoa(n.pos2)} {mg_esc(`({text})`)}`, `{n.file}@{itoa(n.pos2)}`) }
if kind == 3 { mg_emit(`INS {n.file} {itoa(n.pos2)} {mg_esc(` ({text})`)}`, `{n.file}@{itoa(n.pos2)}`) }
if kind == 4 {
let b = n.a
if b == null or b.pos2 < 0 {
mg_say(n, `a listener that needs ({text}) whose place is not known`)
return
}
mg_emit(`INS {b.file} {itoa(b.pos2)} {mg_esc(`({text})`)}`, `{b.file}@{itoa(b.pos2)}`)
}
}
function mg_calls() -> void {
var c = 0
while c < len(g_mc_call) {
let e = g_mc_call[c]
let to = g_mc_callee[c]
let callee = g_mu_node[to]
let need = mg_list(g_mu_need[to])
if len(need) > 0 {
var text = ""
let passed = mg_first_positional_is_state(e)
var k = 0
while k < len(need) {
if not (mg_declared(callee, need[k]) and passed) {
if len(text) > 0 { text = text + ", " }
text = text + g_ms_param[need[k]]
}
k += 1
}
if len(text) > 0 {
if e.pos < 0 or e.file == null { mg_say(e, `a call to {callee.s} in generated code needs ({text})`) } else {
var t = text
if len(e.kids) > 0 { t = t + ", " }
mg_emit(`INS {e.file} {itoa(e.pos + 1)} {mg_esc(t)}`, `{e.file}@{itoa(e.pos)}`)
}
}
}
c += 1
}
}
function mg_refs() -> void {
var r = 0
while r < len(g_mr_node) {
let e = g_mr_node[r]
let v = g_mg_vars[g_mr_var[r]]
if e.pos >= 0 and e.file != null {
let s = g_mg_vstate[g_mr_var[r]]
mg_emit(`REF {e.file} {itoa(e.pos)} {itoa(len(v.s))} {mg_esc(`{g_ms_param[s]}.{v.s}`)}`, `{e.file}@{itoa(e.pos)}`)
}
r += 1
}
}
function mg_finish() -> void {
mg_fixpoint()
var i = 0
while i < len(g_mg_vars) {
let v = g_mg_vars[i]
let s = g_mg_vstate[i]
mg_emit(`VAR {v.file} {itoa(v.pos)} {itoa(v.pos2)} {g_ms_name[s]}`, "")
i += 1
}
var s = 0
while s < len(g_ms_name) {
# the first var of each new state is where the state is declared
var first = -1
var j = 0
while j < len(g_mg_vars) and first < 0 {
if g_mg_vstate[j] == s { first = j }
j += 1
}
if first >= 0 {
let v = g_mg_vars[first]
mg_emit(`STATE {g_ms_name[s]} {v.file} {itoa(v.pos)} {itoa(g_ms_export[s])} {g_ms_param[s]}`, "")
}
s += 1
}
mg_refs()
mg_units()
mg_calls()
let f = file_open(g_mg_plan, "wb")
if f == null { die("ludicc: cannot write the migration plan\n") }
i = 0
while i < len(g_mg_lines) {
file_write(f, g_mg_lines[i], len(g_mg_lines[i]))
file_write(f, "\n", 1)
i += 1
}
i = 0
while i < len(g_mg_report) {
file_write(f, g_mg_report[i], len(g_mg_report[i]))
file_write(f, "\n", 1)
i += 1
}
file_close(f)
exit(0)
}

View file

@ -179,14 +179,20 @@ function interp_str(e: Node) -> Node { # wrap a hole in string(
let c = node(E_CALL); let id = node(E_ID); id.s = "string"; c.a = id; push(c.kids, e); return c
}
var g_interp_line: int = 1 # the line the interpolated string sits on
var g_interp_pos: int = -1 # the backtick's place in its file
var g_hole_base: int = -1
function parse_hole(inner: pointer) -> Node { # re-lex+parse an embedded expression
let saved_toks = toks; let saved_pi = pi
let saved_base = g_lex_base
g_lex_base = g_hole_base # the hole's place in its file, or -1
lex_at(inner, g_interp_line); pi = 0; skipnl()
g_lex_base = saved_base
let e = expr()
toks = saved_toks; pi = saved_pi
return e
}
function parse_interp(raw: pointer) -> Node {
let my_pos = g_interp_pos # a hole's own templates move g_interp_pos
let n = len(raw)
var acc: Node = null
let lit = bytes(n + 1)
@ -202,6 +208,8 @@ function parse_interp(raw: pointer) -> Node {
# find the hole's closing brace: braces nest, and a brace inside a "string", a 'char' or a
# `template` in the hole is text, not structure (lex.ludic, hole_end)
i = hole_end(raw, i, n)
g_hole_base = -1
if my_pos >= 0 { g_hole_base = my_pos + 1 + hs }
acc = interp_add(acc, interp_str(parse_hole(raw[hs..i])))
i += 1 # skip the closing '}'
} else {
@ -246,7 +254,7 @@ function parse_list() -> Node {
function p_primary() -> Node {
let t = toks[pi]
if t.kind == TK_INTERP { pi += 1; g_interp_line = t.line; return parse_interp(t.text) }
if t.kind == TK_INTERP { pi += 1; g_interp_line = t.line; g_interp_pos = t.pos; return parse_interp(t.text) }
if is_op("[") { return parse_list() }
if t.kind == TK_ID and (t.text == "emit") and (toks[pi + 1].kind == TK_ID) { return parse_emit() }
if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; n.s = t.text; pi += 1; return n } # s: a long literal's digits
@ -533,6 +541,7 @@ function stmt_body() -> Node {
function parse_var() -> Node {
pi += 1; let n = node(N_VAR); n.s = eat_id(); eat_op(":"); n.ty = ptype()
if is_op("=") { pi += 1; n.a = expr() }
n.pos2 = toks[pi - 1].end # 0.S: where the declaration ends
return n
}
function parse_const() -> Node {
@ -558,10 +567,11 @@ function parse_fn() -> Node {
pi += 1; let n = node(N_FN); n.s = eat_id()
if is_op("<") { n.tps = gen_params() } # L5: function first<T>(xs: []T) -> T
gen_enter(n.tps)
n.pos2 = toks[pi].pos # 0.S: the parameter list's '('
eat_op("(")
if is_reserved_word(n.s) { perr(`'{n.s}' is a reserved word and cannot name a function`) }
while not is_op(")") {
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype()
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); parse_param_mut(p); p.ty = ptype()
if is_op("=") { pi += 1; p.a = expr() } # L11: a default, for a call that leaves it out
push(n.kids, p)
if is_op(",") { pi += 1 }
@ -573,7 +583,14 @@ function parse_fn() -> Node {
gen_enter(null)
return n
}
function parse_main() -> Node { pi += 1; let n = node(N_MAIN); n.a = block(); return n }
function parse_main() -> Node {
pi += 1
let n = node(N_MAIN)
let ps = parse_entry_params() # 0.S: entry (h: mut Hiker) - state.ludic
n.a = block()
entry_bind(n.a, ps)
return n
}
# issue #76 — rewrite calls to a namespace-block sibling (a bare short-name call)
# to the prefixed function name, so a body can call `pending()` where the function
@ -1008,7 +1025,21 @@ function parse_one_decl() -> void {
if is_id("def") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].kind == TK_ID) { parse_def(); return } # L8
if is_id("view") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_view(); return } # L11
if is_id("component") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_ui_component(); return } # L11
if is_id("var") { push(prog, parse_var()); return }
if is_id("state") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_state(); return } # 0.S
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`)
}
push(prog, parse_var())
return
}
if is_id("let") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == ":") { # 0.S: a module-level let - immutable all the way down
let v = parse_var()
v.uns = 1
if v.a == null { perr(`a module-level let {v.s} needs its value`) }
push(prog, v)
return
}
if is_id("const") { push(prog, parse_const()); return }
var is_unsafe = false
if is_id("unsafe") and (toks[pi + 1].text == "function") { # L7: an unsafe function
@ -1041,8 +1072,11 @@ function parse_test() -> Node {
let n = node(N_TEST)
n.s = toks[pi].text
n.line = ln
n.pos2 = toks[pi].end # 0.S: where state parameters would go
pi += 1 # past the name
let ps = parse_entry_params() # 0.S: test "name" (h: mut Hiker)
n.a = block()
entry_bind(n.a, ps)
return n
}

View file

@ -38,6 +38,8 @@ function parse_event() -> Node {
function parse_system() -> Node {
pi += 1; let n = node(N_SYS); n.s = eat_id(); n.ty = "Update"
n.pos2 = toks[pi - 1].end # 0.S: where state parameters would go
let ps = parse_entry_params() # 0.S: handler Draw(h: Hiker) phase Render
# postfix clauses on `handler Name …`: @anno(...) (parsed and reserved, e.g.
# @deterministic / @Reads(...) / @Writes(...)) and `phase X`. The handler's
# query lives in a prefix `@Queries(...)` annotation (see parse_one_decl), not
@ -56,6 +58,8 @@ function parse_system() -> Node {
}
skipnl()
n.a = block()
n.a.pos2 = n.pos2 # 0.S2: a listener is known by its body
entry_bind(n.a, ps)
return n
}

View file

@ -93,6 +93,7 @@ function port_bind_one(b: Node) -> void {
while i < len(rec.kids) {
let e = rec.kids[i].a
if e != null and e.kind == E_ID { port_bind_var(b, c, rec.kids[i]) }
if e != null and e.kind == E_MEMBER and e.a != null and e.a.kind == E_ID and is_state_ty(e.a.s) { port_bind_var(b, c, rec.kids[i]) }
i += 1
}
# what the bind names is seen from the bind's file: the port itself, and every `fn name`
@ -135,9 +136,17 @@ function port_defaults_only(k: int) -> void {
# `money: gm_money` for a `money: fn() -> int` member: the function `bind_<Port>_<member>` that
# returns the variable, written in the bind's own file (so it sees what the bind sees)
function port_bind_var(b: Node, c: Node, fi: Node) -> void {
let name = fi.a.s
let g = find_global(name)
if g == null { port_err(b, `bind {b.s}: {fi.s}: {name} is not a variable (a function is bound as fn {name})`) }
# 0.S: `money: Wallet.cash` - a field of a state, read through the state the runtime supplies
var sty: pointer = null
var name: pointer = null
if fi.a.kind == E_MEMBER {
sty = fi.a.a.s
name = fi.a.s
} else {
name = fi.a.s
let g = find_global(name)
if g == null { port_err(b, `bind {b.s}: {fi.s}: {name} is not a variable (a function is bound as fn {name})`) }
}
let fty = field_type(c, fi.s)
let n = len(fty)
if n < 7 or not (fty[0 .. 6] == "fn()->") {
@ -145,7 +154,8 @@ function port_bind_var(b: Node, c: Node, fi: Node) -> void {
}
let ret = fty[6 .. n]
let gen = `bind_{b.s}_{fi.s}`
let src = `function {gen}() -> {ret} {{\n return {name}\n}}\n`
var src = `function {gen}() -> {ret} {{\n return {name}\n}}\n`
if sty != null { src = `function {gen}(st: {sty}) -> {ret} {{\n return st.{name}\n}}\n` }
let saved_toks = toks
let saved_pi = pi
let saved_file = g_parse_file

View file

@ -0,0 +1,135 @@
# 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
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 {
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 }
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
push(args, id)
i += 1
}
i = 0
while i < len(e.kids) {
push(args, e.kids[i])
i += 1
}
e.kids = args
}
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
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
}