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

@ -13,7 +13,9 @@ function ck_params(f: Node, labels: []pointer, tys: []pointer) -> void {
}
}
function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
mg_call(e, f)
call_fill_defaults(e, f)
ck_state_args(e, f)
let labels = new []pointer
let tys = new []pointer
ck_params(f, labels, tys)
@ -22,6 +24,7 @@ function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
return f.ty
}
function ck_call_alias(e: Node, name: pointer, al: int, f: Node) -> pointer {
state_inject(e, f)
if g_al_labels[al] == null { call_fill_defaults(e, f) } else { call_mixed_to_named(e, ns_alias_labels(al)) }
let labels = new []pointer
let tys = new []pointer
@ -141,6 +144,7 @@ function ck_builtin(e: Node, name: pointer) -> pointer {
return "void"
}
if (name == "push") and len(e.kids) == 2 {
ck_write_check(e.kids[0], "push")
let st = ck_expr(e.kids[0])
let vt = ck_expr(e.kids[1])
if not ck_unknown(st) and is_slice_ty(st) { ck_give(slice_elem(st), vt, e.kids[1], `push onto {ck_a(st)}`) }

View file

@ -18,6 +18,7 @@ function ck_bind(name: pointer, ty: pointer) -> void {
push(ck_tys, t)
}
ck_top += 1
ck_set_ro(false)
}
function ck_mark() -> int { return ck_top }
function ck_pop(m: int) -> void { ck_top = m }

View file

@ -70,6 +70,7 @@ function ck_id(e: Node) -> pointer {
if li >= 0 { return ck_tys[li] }
let g = ck_global(e.s)
if g != null {
mg_ref(e, g)
ck_vis(g, e.s, e)
return g.ty
}

View file

@ -88,6 +88,9 @@ function gen_clone(n: Node, tps: []pointer, args: []pointer) -> Node {
c.line = n.line
c.file = n.file
c.vis = n.vis
c.uns = n.uns
c.pos = n.pos
c.pos2 = n.pos2
c.a = gen_clone(n.a, tps, args)
c.b = gen_clone(n.b, tps, args)
c.c = gen_clone(n.c, tps, args)

View file

@ -17,6 +17,7 @@ function gen_fn_instance(t: Node, iname: pointer, binds: []pointer) -> Node {
if have != null { return have }
let f = gen_clone(t, gen_split(t.tps), binds)
f.s = iname
if g_migrate { f.mg = mg_unit(t, 0) } # an instance's needs are its generic's
push(g_gen_out, f)
push(g_gen_work, f)
ck_tab_put(ck_fn_k, ck_fn_v, iname, f)
@ -44,6 +45,7 @@ function gen_arg_nodes(e: Node, labels: []pointer) -> []Node {
return out
}
function gen_call(e: Node, name: pointer, t: Node) -> pointer {
mg_call(e, t)
let expect = ck_call_expect
call_fill_defaults(e, t)
let tps = gen_split(t.tps)

View file

@ -0,0 +1,79 @@
# check_state.ludic — 0.S: what a state parameter allows. `h: mut Hiker` may change the state;
# `h: Hiker` may only read it - an assignment whose target starts at it (`h.hips = 1`,
# `h.list[i] = x`), a push onto something in it, or passing it where a `mut Hiker` is wanted is
# refused. A module-level `let` (and a const, a registry, a state's own instance) is immutable all
# the way down: the same writes starting at one are refused. What this does not follow: a reference
# read out of a read-only state into a local (`let l = h.list`) and changed there.
var ck_ro: []int = new []int # parallel to ck_names: 1 for a read-only state
function ck_set_ro(ro: bool) -> void {
let k = ck_top - 1
while len(ck_ro) < len(ck_names) { push(ck_ro, 0) }
if ro { ck_ro[k] = 1 } else { ck_ro[k] = 0 }
}
function ck_is_ro(li: int) -> bool {
if li < 0 or li >= len(ck_ro) { return false }
return ck_ro[li] == 1
}
# the name an assignment's target starts at: `a` in `a.b[c].d`
function ck_chain_root(t: Node) -> Node {
var n = t
while n != null and (n.kind == E_INDEX or n.kind == E_MEMBER) { n = n.a }
if n == null or n.kind != E_ID { return null }
return n
}
function ck_module_let(g: Node) -> bool {
if g == null { return false }
if g.kind == N_CONST { return true }
if g.kind != N_VAR { return false }
if g.uns == 1 { return true }
return reg_find(g.s) >= 0
}
# a write into `t`: refused when it starts at a read-only state or at a module-level binding
function ck_write_check(t: Node, what: pointer) -> void {
let r = ck_chain_root(t)
if r == null { return }
let li = ck_local(r.s)
if li >= 0 {
if ck_is_ro(li) {
ck_err("state", t, `{what}: {r.s} is read-only here ({r.s}: {ck_tys[li]}); take it as {r.s}: mut {ck_tys[li]} to change it`)
}
return
}
let g = ck_global(r.s)
mg_write(t, g)
if ck_module_let(g) {
var name = r.s
if is_state_ty(g.ty) and g.uns == 1 { name = `the state {g.ty}` }
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
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`)
}
ck_set_ro(is_state_ty(p.ty) and p.uns == 0)
}
# a call's arguments against the function's state parameters: a read-only state is not a mut one
function ck_state_args(e: Node, f: Node) -> void {
var k = 0
var i = 0
while i < len(f.kids) {
let p = f.kids[i]
if p.kind == N_PARAM {
if p.uns == 1 and is_state_ty(p.ty) and k < len(e.kids) {
let a = e.kids[k]
if a.kind == E_ID {
let li = ck_local(a.s)
if li >= 0 and ck_is_ro(li) {
ck_err("state", a, `{f.s} changes {p.ty} ({p.s}: mut {p.ty}), and {a.s} is read-only here`)
}
}
}
k += 1
}
i += 1
}
}

View file

@ -28,11 +28,21 @@ function ck_let(s: Node) -> void {
if s.ty != null {
if s.a != null { ck_give(s.ty, t, s.a, s.s) }
ck_bind(s.s, s.ty)
if s.tps != null and (s.tps == "state") { # an entry point's state (state.ludic)
if not is_state_ty(s.ty) { ck_err("state", s, `an entry point takes states, and {s.s}: {s.ty} is not one`) }
ck_set_ro(s.uns == 0)
}
return
}
ck_bind(s.s, t)
# a local bound to a read-only state is read-only too
if s.a != null and s.a.kind == E_ID {
let li = ck_local(s.a.s)
if li >= 0 and li < ck_top - 1 and ck_is_ro(li) { ck_set_ro(true) }
}
}
function ck_assign(s: Node) -> void {
ck_write_check(s.a, "this assignment")
let lt = ck_target(s.a)
if (s.s == "=") { ck_expect = lt }
let rt = ck_expr(s.b)
@ -116,10 +126,11 @@ function ck_stmt(s: Node) -> void {
}
}
function ck_fn_body(d: Node) -> void {
mg_enter(d, 0)
let m = ck_mark()
var i = 0
while i < len(d.kids) {
if d.kids[i].kind == N_PARAM { ck_bind(d.kids[i].s, d.kids[i].ty) }
if d.kids[i].kind == N_PARAM { ck_bind_param(d.kids[i]) }
i += 1
}
ck_ret = d.ty
@ -132,8 +143,10 @@ function ck_fn_body(d: Node) -> void {
if d.uns == 1 { ck_unsafe -= 1 }
ck_ret = "void"
ck_pop(m)
mg_leave()
}
function ck_listener(l: Node) -> void {
mg_enter(l, 4)
let ev = find_event(l.s)
ck_vis(ev, l.s, l)
let m = ck_mark()
@ -147,6 +160,7 @@ function ck_listener(l: Node) -> void {
ck_ret = "void"
ck_block(l.a)
ck_pop(m)
mg_leave()
}
# a program's function named like one of the runtime's takes every call the runtime makes to its
# own - the runtime's ui_measure started answering to a package's - so the name is refused; one
@ -177,7 +191,16 @@ function check_program() -> void {
while i < len(prog) {
let d = prog[i]
if d.kind == N_FN { ck_fn_body(d) }
if d.kind == N_MAIN or d.kind == N_TEST { ck_block(d.a) }
if d.kind == N_MAIN or d.kind == N_TEST {
mg_enter(d, 1)
ck_block(d.a)
mg_leave()
}
if d.kind == N_SYS and g_migrate { # a handler's body: only the migration reads it here
mg_enter(d, 2)
ck_block(d.a)
mg_leave()
}
if (d.kind == N_VAR or d.kind == N_CONST) and d.a != null {
ck_vis_file = d.file
ck_vis_skip = d.kind == N_VAR and is_port_var(d)
@ -195,11 +218,13 @@ function check_program() -> void {
# a test block is checked like `entry`: that is also what makes its generic calls real
j = 0
while j < len(g_tests) {
mg_enter(g_tests[j], 3)
ck_block(g_tests[j].a)
mg_leave()
j += 1
}
gen_finish()
if ck_errors > 0 and not ck_reporting() {
if ck_errors > 0 and not ck_reporting() and not g_migrate {
let m = `{itoa(ck_errors)} type error(s)\n`
file_write(file_stderr(), m, len(m))
exit(1)