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

@ -500,9 +500,9 @@ function deps_case() -> void {
# --check: the module rules, the ports and the types, with nothing emitted - a broken uses line is
# refused as a full build refuses it, and a sound program writes no file
function check_build_case() -> void {
let lbl = "ludicc --check / ludic build --check: modules, uses, ports and types without emitting"
let bad = ["rejected/uses_missing", "rejected/layer_reach", "rejected/private_module", "rejected/port_unbound", "rejected/registry_hidden", "rejected/port_new", "rejected/wrong_arity"]
let want = ["fishing uses items.inv_add", "hud uses items.item_count", "add is private to module bank", "port Clock is used here but never bound", "Tools is private to module kit", "Clock is a port", "leaves out h"]
let lbl = "ludicc --check / ludic build --check: every check a build makes (binds, names, registries too), nothing written"
let bad = ["rejected/uses_missing", "rejected/layer_reach", "rejected/private_module", "rejected/port_unbound", "rejected/registry_hidden", "rejected/port_new", "rejected/wrong_arity", "rejected/bind_missing_fn", "rejected/unknown_name", "rejected/registry_count_key"]
let want = ["fishing uses items.inv_add", "hud uses items.item_count", "add is private to module bank", "port Clock is used here but never bound", "Tools is private to module kit", "Clock is a port", "leaves out h", "fn no_such_clock: no function called no_such_clock", "unknown identifier no_such_name", "its constant would be TL_COUNT, which is the registry's size"]
for i in 0 .. len(bad) {
if shq(`bin/ludicc --check examples/{bad[i]}.ludic > {tmp_dir()}/chk.err 2>&1`) { bad2(lbl, `{bad[i]} passed the check`); return }
let said = capture(`cat {tmp_dir()}/chk.err`)
@ -547,6 +547,13 @@ function expect_str_case() -> void {
let want = "ok - equal text built two ways\nexamples/library/testing_strings.ludic:11: expect_eq failed (got \"camp\", want \"lake\")\nFAIL - different text fails, and says both\nexamples/library/testing_strings.ludic:15: expect_eq failed (got \"<null>\", want \"camp\")\nFAIL - a null is not a string\n== 1 passed, 2 failed ==\n"
if s_trim(got) == s_trim(want) { ok(lbl) } else { bad2(lbl, `said [{s_trim(got)}]`) }
}
# 0.R: the drain that calls every reducer is the program's own, so the module that declares the
# actions gains no dependency on the modules that reduce them
function actions_deps_case() -> void {
let lbl = "the action drain is the program's: acts depends on no reducer's module (ludic deps)"
let got = capture(`bin/ludic deps examples/actions/modules.ludic 2>&1`)
if s_contains(got, "largest_cycle: 1") and s_contains(got, "dependencies: 0") and s_contains(got, "the widest function: reducer Bag on PickUp (") { ok(lbl) } else { bad2(lbl, s_trim(got)) }
}
# a program built headless with the toolchain and run: its first line
function headless_case(path: pointer, exp: pointer, label: pointer) -> void {
let out = `{tmp_dir()}/h_{flat(path)}`
@ -963,8 +970,11 @@ function cmd_dev_test() -> int {
reject_case("rejected/layer_reach", "hud uses items.item_count", "a layered module is still held to its uses outside the layer")
reject_case("rejected/layer_cycle", "go round in a circle: items -> layer app -> items", "a cycle through a layer and out of it is refused")
feat_case("actions/pack", "", "0 1 9 13 picked 7 picked 9 too heavy", "pack.ludic (0.R: actions, reducers - one per state, in the order of the states' names - dispatch, drain_actions, an action a reducer dispatches goes behind)")
feat_case("actions/modules", "", "2", "modules.ludic (0.R: an action in one module, its reducer in another)")
actions_deps_case()
feat_case("actions/phases", "dd a q", "1 0 2 0 2 1 1 1 1 2 1 2", "phases.ludic (0.R: the frame loop drains the queue after every phase - Input's actions are reduced before Update)")
feat_case("actions/runaway", "", "actions: Ping is still being dispatched after 64 rounds of reducers - a reducer dispatches what dispatches it", "runaway.ludic (0.R: a reducer that dispatches what dispatches it is stopped by name)")
reject_case("rejected/registry_count_key", "def Tools count: its constant would be TL_COUNT", "a registry key named count is refused")
reject_case("rejected/reducer_two_states", "a reducer takes one state, and w is a Wallet", "a reducer takes exactly one state")
reject_case("rejected/reducer_not_action", "Buy is not an action", "a reducer is on an action")
reject_case("rejected/dispatch_unknown", "dispatch Sell: Sell is not an action", "only an action is dispatched")