ludic/selfhost/check/check_stmt.ludic
Orkuncakilkaya 42deb76c28 schema: ludicc --emit-schema / ludic schema, --check --diagnostics=json, and editor attributes
--emit-schema FILE writes the compiler's resolved view once the program type-checks: every
record (fields, types, defaults as written, docs, places, attributes), every registry with its
entries in their final order after the open-registry merge (key, constant, index, file:line:col
of the entry and of each field value, and which file contributed which keys), every const, and
the zero-argument functions a fn value can name. Deterministic, schema_version 1; the runtime is
left out. `ludic schema [file] [-o FILE]` wraps it.

--check --diagnostics=json prints every error as one JSON array on stdout: the checker's and the
module rules' all, a parse or lowering error as the last. Tokens and nodes now carry a column.

Fields take several @attributes; @Ref(Registry), @OneOf(PREFIX_), @Range(lo, hi), @Unit("..."),
@Asset("..."), @Color on a field and @AppendOnly / @ByKey on a registry change nothing but go into
the schema, and @Ref naming no registry is an error (every one reported). Fixtures:
examples/lang/attributes.ludic, examples/rejected/ref_unknown.ludic, cases in ludic-dev test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:06:54 +03:00

345 lines
9.4 KiB
Text

# check_stmt.ludic — L4: statements, and the pass itself. check_program runs between the parse and
# the emitter over everything the program wrote (not the runtime spliced in after it): every
# function, the entry, the tests, the globals' initializers and every @On listener with its
# event's fields in scope. ECS handler, query, spawn and machine bodies bind names the checker
# does not model, so they are left to the emitter.
function ck_block(b: Node) -> void {
if b == null { return }
if b.kind != N_BLOCK {
ck_stmt(b)
return
}
let m = ck_mark()
var i = 0
while i < len(b.kids) {
ck_stmt(b.kids[i])
i += 1
}
ck_pop(m)
}
function ck_target(t: Node) -> pointer {
if t.kind == E_ID { return ck_id(t) }
return ck_expr(t)
}
function ck_let(s: Node) -> void {
var t: pointer = "?"
ck_expect = s.ty
if s.a != null { t = ck_expr(s.a) }
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)
} else { ck_let_from(s.a, s.ty) }
return
}
ck_bind(s.s, t)
# a local bound to a read-only state, or to a reference out of one, 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_ro_like(li) }
}
ck_let_from(s.a, t)
}
function ck_assign(s: Node) -> void {
# a local rebound is not a write through it
if not (s.a.kind == E_ID and ck_local(s.a.s) >= 0) { 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)
var what: pointer = "this assignment"
if s.a.kind == E_ID { what = s.a.s }
if s.a.kind == E_MEMBER { what = `field {s.a.s}` }
if (s.s == "=") {
ck_give(lt, rt, s.b, what)
return
}
let op = s.s[0..1]
let res = ck_binop(s, op, lt, rt, s.a, s.b)
ck_give(lt, res, s.b, what)
}
function ck_match(s: Node) -> void {
ck_expr(s.a)
var i = 0
while i < len(s.kids) {
let arm = s.kids[i]
let m = ck_mark()
var p = 0
while p < len(arm.kids) {
let pat = arm.kids[p]
# `Door(n) =>` binds its payload's names
if pat.kind == E_CALL {
var q = 0
while q < len(pat.kids) {
if pat.kids[q].kind == E_ID { ck_bind(pat.kids[q].s, "?") }
q += 1
}
}
p += 1
}
ck_block(arm.a)
ck_pop(m)
i += 1
}
}
function ck_stmt(s: Node) -> void {
if s == null { return }
let k = s.kind
if k == N_BLOCK { ck_block(s); return }
if k == S_LET { ck_let(s); return }
if k == S_ASSIGN { ck_assign(s); return }
if k == S_IF {
ck_cond(s.a, ck_expr(s.a))
ck_block(s.b)
ck_block(s.c)
return
}
if k == S_WHILE {
ck_cond(s.a, ck_expr(s.a))
ck_block(s.b)
return
}
if k == S_FOR {
ck_expr(s.a)
ck_expr(s.b)
let m = ck_mark()
ck_bind(s.s, "int")
ck_block(s.c)
ck_pop(m)
return
}
if k == S_RETURN {
if s.a == null { return }
ck_expect = ck_ret
let t = ck_expr(s.a)
if (ck_ret != "void") { ck_give(ck_ret, t, s.a, "the result") }
return
}
if k == S_EXPR { ck_expr(s.a); return }
if k == S_MATCH { ck_match(s); return }
if k == S_EMIT { ck_emit(s); return }
if k == S_UNSAFE {
ck_unsafe_here(s)
ck_unsafe += 1
ck_block(s.a)
ck_unsafe -= 1
return
}
# a machine's store: its states' `become` writes it
if k == S_MACHINE {
if s.a != null and not (s.a.kind == E_INT) { ck_write_check(s.a, "a machine's become") }
ck_walk(s)
return
}
# 0.S: a query's body, a spawn's fields, a machine's states - read for what they touch
if k == S_QUERY {
let m = ck_mark()
var t = 0
var i = 0
while i < len(s.kids) {
var ty: pointer = "?"
if s.c != null {
while t < len(s.c.kids) and s.c.kids[t].ival == 1 { t += 1 }
if t < len(s.c.kids) { ty = s.c.kids[t].s }
t += 1
}
ck_bind(s.kids[i].s, ty)
i += 1
}
ck_any(s.b)
ck_block(s.a)
ck_pop(m)
return
}
ck_walk(s)
}
# a name given to two parameters: the second would shadow the first, and nothing could reach it
function ck_param_twice(d: Node, i: int) -> void {
var j = 0
while j < i {
if d.kids[j].kind == N_PARAM and (d.kids[j].s == d.kids[i].s) {
ck_err("arity", d.kids[i], `{d.s} names two parameters {d.kids[i].s}`)
return
}
j += 1
}
}
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_param_twice(d, i)
ck_bind_param(d.kids[i])
}
i += 1
}
ck_ret = d.ty
if ck_ret == null { ck_ret = "void" }
if d.uns == 1 {
ck_unsafe_here(d)
ck_unsafe += 1
}
ck_block(d.a)
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()
if ev != null {
var i = 0
while i < len(ev.kids) {
ck_bind(ev.kids[i].s, ev.kids[i].ty)
i += 1
}
}
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
# named like a compiler built-in never ran at all (every call to `run` was C's system()), so too
function ck_runtime_shadows() -> void {
var u = 0
while u < g_prog_user_end {
let d = prog[u]
if d.kind == N_FN and (is_intrinsic(d.s) or is_intrinsic2(d.s)) { ck_err("shadow", d, `{d.s} is a built-in of the compiler, and a call to it would never reach this function; choose another name`) }
u += 1
}
var r = g_prog_user_end
while r < len(prog) {
let d = prog[r]
if d.kind == N_FN {
let u = ck_fn(d.s)
if u != null and u != d { ck_err("shadow", u, `{d.s} is also a function of the engine runtime ({d.file}); choose another name`) }
}
# a type the runtime declares - `property PadButton` beside its `enum PadButton` compiled,
# and the two layouts met in the IR
if ck_type_kind(d) != "" {
var k = 0
while k < g_prog_user_end and k < len(prog) {
let o = prog[k]
if ck_type_kind(o) != "" and (o.s == d.s) {
ck_err("shadow", o, `{d.s} is the runtime's {ck_type_kind(d)} ({d.file}); choose another name for this {ck_type_kind(o)}`)
}
k += 1
}
}
r += 1
}
}
function ck_type_kind(d: Node) -> pointer {
if d.kind == N_ENUM { return "enum" }
if d.kind == N_COMP and d.uns == 2 { return "state" }
if d.kind == N_COMP { return "property" }
if d.kind == N_STRUCT { return "record" }
if d.kind == N_EVENT { return "event" }
return ""
}
function ck_hooks(hs: []Node) -> void {
var i = 0
while i < len(hs) {
mg_enter(hs[i], 4)
ck_block(hs[i].a)
mg_leave()
i += 1
}
}
function ck_scene_block(b: Node) -> void {
if b == null { return }
mg_enter(b, 5)
ck_block(b)
mg_leave()
}
# a retained ui widget (N_UI, whose number is also S_IF's): its props and its children
function ck_widget(w: Node) -> void {
if w == null { return }
var i = 0
while w.b != null and i < len(w.b.kids) {
ck_expr(w.b.kids[i].a)
i += 1
}
i = 0
while i < len(w.kids) {
ck_widget(w.kids[i])
i += 1
}
}
# the pass also makes the generics real (check_gen.ludic), so it always runs
function check_program() -> void {
gen_collect()
ck_index()
ck_runtime_shadows()
ck_tynames() # 0.R4: a type written names a type
var i = 0
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 {
mg_enter(d, 1)
ck_block(d.a)
mg_leave()
}
if d.kind == N_UI and d.ival == 1 and g_migrate { # 0.S2: a ui block's props, for what they read
g_mg_cur = -3
ck_widget(d.a)
g_mg_cur = -1
}
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)
ck_give(d.ty, ck_expr(d.a), d.a, d.s)
ck_vis_file = null
ck_vis_skip = false
}
i += 1
}
var j = 0
while j < len(g_onlisten) {
ck_listener(g_onlisten[j])
j += 1
}
if g_migrate { # 0.S2: the hooks' bodies and the scenes', for what they touch
ck_hooks(g_onspawn)
ck_hooks(g_ondespawn)
ck_hooks(g_onattach)
ck_hooks(g_ondetach)
ck_hooks(g_onenable)
ck_hooks(g_ondisable)
j = 0
while j < len(g_scenes) {
ck_scene_block(g_scenes[j].a)
ck_scene_block(g_scenes[j].b)
j += 1
}
}
# 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() and not g_migrate {
if g_diag_json { diag_exit(1) }
let m = `{itoa(ck_errors)} type error(s)\n`
file_write(file_stderr(), m, len(m))
exit(1)
}
}