fix(lang): the action drain is the program's; --check runs every check a build makes; a registry key named count is refused; name lookups are tables

- the function that calls every reducer (and the action queue) is written in the program's own
  file: in the first action's file it belonged to that module, depended on every module with a
  reducer, and joined a game's modules into one 69-module cycle (examples/actions/modules and a
  ludic deps case hold it); the state instances, the queue and the reducers make no deps edges
- ludicc --check / ludic build --check lower the program too and write nothing, so the code
  writer's refusals are in it: a bind to a function that is gone, an unknown name (and the checker
  now refuses fn <missing> itself); rejects bind_missing_fn, unknown_name, registry_count_key
- def R count is refused: its constant would be PREFIX_COUNT, the registry's size
- a file's module, package, trust and numbers-float are tables, and from the check on the lookups
  of functions, enums, records, globals and externs are too (tagged enums kept as a list): Maroon
  Lake's check-only build went from about 20 s to 7 s including lowering, its IR from about 2
  minutes to under 10 s; duplicate declarations are found by table, not a pair of loops
- threads.ludic's pool check gives each call a little work, so a busy machine cannot run them all
  on the caller before a worker wakes (it failed one run in three under load)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 23:34:34 +03:00
parent 2fdefa6040
commit 8cf4b3fed6
24 changed files with 67169 additions and 64398 deletions

View file

@ -151,7 +151,94 @@ function find_arch(name: pointer) -> Node {
while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and (d.s == name) { return d }; i += 1 }
return null
}
# ---- the emitter's lookups by name: tables, once the program is whole ---------------------
# find_fn / find_enum / find_comp / find_global / find_extern are asked for every name the emitter
# lowers; walking prog for each was most of a big program's build. From emit_program on, each kind
# is a table, kept up with what is appended to prog; a hit whose node has since been renamed falls
# back to the walk.
const IX_SLOTS: int = 131072
var g_ix_on: bool = false
var g_ix_tagged: []Node = new []Node
var g_ix_upto: int = 0
var g_ix_keys: [][]pointer = new [][]pointer # per kind: 0 fn, 1 enum, 2 comp, 3 global, 4 extern
var g_ix_vals: [][]pointer = new [][]pointer
function ix_start() -> void {
g_ix_keys = new [][]pointer
g_ix_vals = new [][]pointer
var t = 0
while t < 5 {
let k = new []pointer
let v = new []pointer
var i = 0
while i < IX_SLOTS {
push(k, null)
push(v, null)
i += 1
}
push(g_ix_keys, k)
push(g_ix_vals, v)
t += 1
}
g_ix_upto = 0
g_ix_tagged = new []Node
g_ix_on = true
}
function ix_kind(d: Node) -> int {
if d.kind == N_FN { return 0 }
if d.kind == N_ENUM { return 1 }
if d.kind == N_COMP { return 2 }
if d.kind == N_VAR or d.kind == N_CONST { return 3 }
if d.kind == N_EXTERN { return 4 }
return -1
}
function ix_slot(t: int, name: pointer) -> int {
var h = 5381
var i = 0
let n = len(name)
while i < n {
h = (h * 33 + name[i]) % 1000003
i += 1
}
h = h % IX_SLOTS
let ks = g_ix_keys[t]
while ks[h] != null and not (ks[h] == name) { h = (h + 1) % IX_SLOTS }
return h
}
function ix_sync() -> void {
while g_ix_upto < len(prog) {
let d = prog[g_ix_upto]
let t = ix_kind(d)
if d.kind == N_ENUM and enum_is_tagged(d) { push(g_ix_tagged, d) }
if t >= 0 and d.s != null {
let h = ix_slot(t, d.s)
if g_ix_keys[t][h] == null { # the first of a name wins, as the walk had it
g_ix_keys[t][h] = d.s
g_ix_vals[t][h] = d
}
}
g_ix_upto += 1
}
}
# the node of kind t called name, or null; -1 when the table cannot say (off, or a stale hit)
var g_ix_miss: bool = false
function ix_find(t: int, name: pointer) -> Node {
g_ix_miss = false
if not g_ix_on or name == null {
g_ix_miss = true
return null
}
ix_sync()
let h = ix_slot(t, name)
let p = g_ix_vals[t][h]
if p == null { return null }
let d: Node = p
if (d.s == name) and ix_kind(d) == t { return d }
g_ix_miss = true
return null
}
function find_comp(name: pointer) -> Node {
let x = ix_find(2, name)
if not g_ix_miss { return x }
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
@ -174,6 +261,8 @@ function field_type(s: Node, fname: pointer) -> pointer {
# find a global var/const by name
function find_global(name: pointer) -> Node {
let x = ix_find(3, name)
if not g_ix_miss { return x }
var i = 0
while i < len(prog) {
let d = prog[i]
@ -188,6 +277,14 @@ function find_global(name: pointer) -> Node {
# enum with that variant. Enum names live in `prog` like any other declaration.
function enum_ordinal(ename: pointer, vname: pointer) -> int {
var i = 0
if g_ix_on { # the table knows the one enum of that name
let d = find_enum(ename)
if d != null {
var j = 0
while j < len(d.kids) { if (d.kids[j].s == vname) { return j }; j += 1 }
}
i = len(prog)
}
while i < len(prog) {
let d = prog[i]
if d.kind == N_ENUM and (d.s == ename) {
@ -234,6 +331,8 @@ function has_countdowns() -> bool {
return false
}
function find_fn(name: pointer) -> Node {
let x = ix_find(0, name)
if not g_ix_miss { return x }
var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_FN and (d.s == name) { return d }; i += 1 }
return null
@ -248,6 +347,8 @@ function find_fn(name: pointer) -> Node {
var g_var_ord: int = 0
function find_enum(name: pointer) -> Node {
let x = ix_find(1, name)
if not g_ix_miss { return x }
var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_ENUM and (d.s == name) { return d }; i += 1 }
return null
@ -276,6 +377,16 @@ function variant_ordinal(en: Node, vname: pointer) -> int {
# Only tagged enums participate, so a plain enum's `Enum.Variant` member form and
# any like-named function are left alone.
function variant_enum(vname: pointer) -> Node {
if g_ix_on { # the tagged enums, kept as prog grows
ix_sync()
var t = 0
while t < len(g_ix_tagged) {
let ord = variant_ordinal(g_ix_tagged[t], vname)
if ord >= 0 { g_var_ord = ord; return g_ix_tagged[t] }
t += 1
}
return null
}
var i = 0
while i < len(prog) {
let d = prog[i]
@ -306,6 +417,8 @@ function enum_box_size(en: Node) -> int { return 8 * (1 + enum_max_arity(en)) }
# is the transport seam (net_send/net_poll), the windowing/socket FFI, and any
# C/Rust/Zig library binding — the same seam NETWORKING-DESIGN §5 names.
function find_extern(name: pointer) -> Node {
let x = ix_find(4, name)
if not g_ix_miss { return x }
var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_EXTERN and (d.s == name) { return d }; i += 1 }
return null

View file

@ -213,19 +213,19 @@ function emit_test_runner() -> void {
# two `function`s with one name would collide in the object file; say so in
# source terms (and name both files) instead of leaving it to the IR assembler.
function check_duplicate_fns() -> void {
let k = new []pointer # by name, in a table: a pair of loops was quadratic
let v = new []Node
ck_tab_init(k, v)
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_FN {
var j = i + 1
while j < len(prog) {
let o = prog[j]
if o.kind == N_FN and (o.s == d.s) {
g_err_file = o.file; g_err_line = o.line
perr(`function '{d.s}' is defined twice (first in {d.file}:{itoa(d.line)})`)
}
j += 1
let first = ck_tab_get(k, v, d.s)
if first != null {
g_err_file = d.file; g_err_line = d.line
perr(`function '{d.s}' is defined twice (first in {first.file}:{itoa(first.line)})`)
}
ck_tab_put(k, v, d.s, d)
}
i += 1
}
@ -240,22 +240,23 @@ function decl_group(k: int) -> int {
return 0
}
function check_duplicate_decls() -> void {
let k = new []pointer
let v = new []Node
ck_tab_init(k, v)
let n = g_prog_user_end
var i = 0
while i < n and i < len(prog) {
let d = prog[i]
let g = decl_group(d.kind)
if g > 0 {
var j = i + 1
while j < n and j < len(prog) {
let o = prog[j]
if decl_group(o.kind) == g and (o.s == d.s) {
g_err_file = o.file
g_err_line = o.line
perr(`'{d.s}' is defined twice (first in {d.file}:{itoa(d.line)})`)
}
j += 1
let key = `{itoa(g)}:{d.s}`
let first = ck_tab_get(k, v, key)
if first != null {
g_err_file = d.file
g_err_line = d.line
perr(`'{d.s}' is defined twice (first in {first.file}:{itoa(first.line)})`)
}
ck_tab_put(k, v, key, d)
}
i += 1
}
@ -264,6 +265,7 @@ function check_duplicate_decls() -> void {
function emit_program() -> void {
check_duplicate_fns()
check_duplicate_decls()
ix_start() # the lookups by name, as tables from here on
head = buf_new()
code = buf_new()
g_uses_str = false

View file

@ -29,17 +29,29 @@ function vis_plain(what: pointer) -> pointer {
function vis_say(m: pointer) -> void {
file_write(file_stderr(), m, len(m))
}
# does s start with p - without making the slice a comparison of s[0 .. n] would (asked for every
# reference)
function str_prefix(s: pointer, p: pointer) -> bool {
if s == null { return false }
let n = len(p)
if len(s) < n { return false }
var i = 0
while i < n {
if s[i] != p[i] { return false }
i += 1
}
return true
}
function vis_check(d: Node, what0: pointer) -> void {
if d == null { return }
port_check_bound(d)
# 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 }
deps_edge(d, what0) # LUDIC_DEPS: the graph ludic deps reads (emit_deps.ludic)
if g_vis_off { return }
if d.file == null { return }
# 0.S: a state's instance, supplied by the runtime (a component's or a view's glue, an entry's
# parameters): the code that names it only passes it on
if d.kind == N_VAR and d.uns == 1 and is_state_ty(d.ty) and len(d.s) > 6 and (d.s[0 .. 6] == "state$") { return }
# 0.R: the action queue and the reducers are the runtime's to call, from any module
if d.kind == N_FN and (is_action_builtin(d.s) or (len(d.s) > 14 and (d.s[0 .. 14] == "ludic_reduce__"))) { return }
let what = vis_plain(what0)
var here = g_err_file
if here == null { return }