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

@ -350,7 +350,7 @@ The self-hosted `ludicc`/`ludic` (built with `bin/ludic-dev build-cli`) accept:
--windowed force a windowed (Cocoa) build --windowed force a windowed (Cocoa) build
--headless force a headless build (stdin input, out.ppm output) --headless force a headless build (stdin input, out.ppm output)
--emit-llvm stop at LLVM IR — write it and exit, no clang --emit-llvm stop at LLVM IR — write it and exit, no clang
--check parse and check only (types, modules, uses, layers, ports); write nothing --check every check a build makes (types, modules, uses, layers, ports, binds); write nothing
--fmt lex + parse only; exit 0 if it parses, 1 on a parse error --fmt lex + parse only; exit 0 if it parses, 1 on a parse error
(the check-docs gate; canonical formatting not yet restored) (the check-docs gate; canonical formatting not yet restored)
--save-temps keep the intermediate .ll --save-temps keep the intermediate .ll

View file

@ -2025,7 +2025,7 @@ ludic test # compile and run the project's `test`
ludic test tests/math.ludic --test adds # just the test named "adds" (-v: every result line) ludic test tests/math.ludic --test adds # just the test named "adds" (-v: every result line)
ludic test -j 4 # four tests at once (default: one per CPU) ludic test -j 4 # four tests at once (default: one per CPU)
ludic test packages/ludic.base # the test programs under a directory (a package's) ludic test packages/ludic.base # the test programs under a directory (a package's)
ludic build --check # only check: types, modules, uses and ports - nothing emitted ludic build --check # every check a build makes, nothing written
ludic deps # how tangled the modules are, as the compiler resolved them ludic deps # how tangled the modules are, as the compiler resolved them
ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them) ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them)
@ -2033,11 +2033,11 @@ ludicc app.ludic -o build/app # the compiler directly: a native binar
ludicc app.ludic --emit-llvm -o app.ll # stop at LLVM IR ludicc app.ludic --emit-llvm -o app.ll # stop at LLVM IR
``` ```
`ludic build [file] --check` (the compiler's `ludicc --check`) runs everything the compiler checks `ludic build [file] --check` (the compiler's `ludicc --check`) runs every check a build makes - the
before it writes code - the parse, the types, `export`, `uses` and layers, ports and binds, registries parse, the types, `export`, `uses` and layers, ports and binds, registries, and the code writer's own
- and stops: no IR, no link. On Maroon Lake it takes about three seconds where a build takes about a (a function that can reach its end without its result, a bind to a function that is not there, an
minute, for iterating on `uses` lines. Two checks belong to the code writer and only a build makes unknown name) - and writes nothing: no IR, no link. On Maroon Lake it takes about four seconds, for
them: a function that can reach its end without its result, and a local declared twice in a block. iterating on `uses` lines.
`ludic deps` compiles the program (the package's entry, or a file) with the compiler recording every `ludic deps` compiles the program (the package's entry, or a file) with the compiler recording every
reference its visibility pass resolves - from the module it is written in to the module of what it reference its visibility pass resolves - from the module it is written in to the module of what it

View file

@ -0,0 +1,10 @@
bump: patch
type: fix
**The action drain is the program's; `--check` is every check; `count` is not a registry key.**
The function that calls every reducer is written in the program's own file, so the module that
declares the actions no longer depends on every module that reduces them (a game's `base` joined a
69-module cycle). `ludic build --check` runs the code writer's checks too - a bind to a function that
is gone, an unknown name - and still writes nothing. A registry entry keyed `count` is refused
(`def Tools count: its constant would be TL_COUNT, which is the registry's size`). And a big
program builds much faster: the emitter's and the checker's lookups by name are tables, not walks of
the whole program (on Maroon Lake a check-only build, lowering included, went from about 20 s to under 10 s, its IR from about 2 minutes to under 10 s).

View file

@ -0,0 +1,4 @@
# acts/index.ludic - the actions, in a module that uses nothing: dispatching or declaring them makes
# no module depend on the modules whose reducers handle them
module acts uses
export action PickUp { item: int }

View file

@ -0,0 +1,5 @@
# bag/index.ludic - the bag's state and its reducer; it uses acts for the action's type
module bag uses acts
export state Bag { items: []int = new []int }
reducer Bag on PickUp(b: mut Bag, a: PickUp) { push(b.items, a.item) }
export function bag_count(b: Bag) -> int { return len(b.items) }

View file

@ -0,0 +1,12 @@
# modules.ludic - 0.R: an action declared in one module and reduced in another; the drain that calls
# every reducer is the program's own, so acts does not come to depend on bag (ludic deps: no cycle)
import "mods/acts"
import "mods/bag"
program ActionModules {
entry (b: Bag) {
dispatch PickUp { item: 4 }
dispatch PickUp { item: 5 }
drain_actions()
print(bag_count(b))
}
}

View file

@ -23,8 +23,13 @@ program Threads {
Sync.unlock(threads_st.lock) Sync.unlock(threads_st.lock)
} }
# remember which calls ran on a pool thread # remember which calls ran on a pool thread - each with a little work in it, so on a busy machine
function placed(i: int, out: words) -> void { if Job.is_worker() { out[i] = 1 } else { out[i] = 0 } } # the calling thread cannot claim every index before a worker wakes
function placed(i: int, out: words) -> void {
var spin = 0
for k in 0 .. 2000 { spin = spin + k % 7 }
if Job.is_worker() { out[i] = 1 + spin * 0 } else { out[i] = spin * 0 }
}
entry (threads_st: mut ThreadsState) { entry (threads_st: mut ThreadsState) {
if Sync.cpu_count() >= 1 { print(1) } if Sync.cpu_count() >= 1 { print(1) }

View file

@ -0,0 +1,6 @@
# a port bound to a function that is not there: refused by a check-only build too
program BindMissingFn {
port Clock { now: fn() -> int }
bind Clock { now: fn no_such_clock }
entry { print(Clock.now()) }
}

View file

@ -0,0 +1,8 @@
# a def keyed `count` would be TL_COUNT, the registry's own size
program RegistryCountKey {
property Tool { key: string = "", n: int = 0 }
registry Tools of Tool as TL
def Tools count { n: 1 }
def Tools axe { n: 2 }
entry { print(TL_COUNT) }
}

View file

@ -0,0 +1,4 @@
# a name that is nothing: refused by a check-only build as by a build
program UnknownName {
entry { print(no_such_name + 1) }
}

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 } while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and (d.s == name) { return d }; i += 1 }
return null 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 { function find_comp(name: pointer) -> Node {
let x = ix_find(2, name)
if not g_ix_miss { return x }
var i = 0 var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and (d.s == name) { return d }; i += 1 } while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and (d.s == name) { return d }; i += 1 }
return null return null
@ -174,6 +261,8 @@ function field_type(s: Node, fname: pointer) -> pointer {
# find a global var/const by name # find a global var/const by name
function find_global(name: pointer) -> Node { function find_global(name: pointer) -> Node {
let x = ix_find(3, name)
if not g_ix_miss { return x }
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
let d = prog[i] 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. # enum with that variant. Enum names live in `prog` like any other declaration.
function enum_ordinal(ename: pointer, vname: pointer) -> int { function enum_ordinal(ename: pointer, vname: pointer) -> int {
var i = 0 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) { while i < len(prog) {
let d = prog[i] let d = prog[i]
if d.kind == N_ENUM and (d.s == ename) { if d.kind == N_ENUM and (d.s == ename) {
@ -234,6 +331,8 @@ function has_countdowns() -> bool {
return false return false
} }
function find_fn(name: pointer) -> Node { function find_fn(name: pointer) -> Node {
let x = ix_find(0, name)
if not g_ix_miss { return x }
var i = 0 var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_FN and (d.s == name) { return d }; i += 1 } while i < len(prog) { let d = prog[i]; if d.kind == N_FN and (d.s == name) { return d }; i += 1 }
return null return null
@ -248,6 +347,8 @@ function find_fn(name: pointer) -> Node {
var g_var_ord: int = 0 var g_var_ord: int = 0
function find_enum(name: pointer) -> Node { function find_enum(name: pointer) -> Node {
let x = ix_find(1, name)
if not g_ix_miss { return x }
var i = 0 var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_ENUM and (d.s == name) { return d }; i += 1 } while i < len(prog) { let d = prog[i]; if d.kind == N_ENUM and (d.s == name) { return d }; i += 1 }
return null 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 # Only tagged enums participate, so a plain enum's `Enum.Variant` member form and
# any like-named function are left alone. # any like-named function are left alone.
function variant_enum(vname: pointer) -> Node { 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 var i = 0
while i < len(prog) { while i < len(prog) {
let d = prog[i] 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 # 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. # C/Rust/Zig library binding — the same seam NETWORKING-DESIGN §5 names.
function find_extern(name: pointer) -> Node { function find_extern(name: pointer) -> Node {
let x = ix_find(4, name)
if not g_ix_miss { return x }
var i = 0 var i = 0
while i < len(prog) { let d = prog[i]; if d.kind == N_EXTERN and (d.s == name) { return d }; i += 1 } while i < len(prog) { let d = prog[i]; if d.kind == N_EXTERN and (d.s == name) { return d }; i += 1 }
return null 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 # 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. # source terms (and name both files) instead of leaving it to the IR assembler.
function check_duplicate_fns() -> void { 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 var i = 0
while i < len(prog) { while i < len(prog) {
let d = prog[i] let d = prog[i]
if d.kind == N_FN { if d.kind == N_FN {
var j = i + 1 let first = ck_tab_get(k, v, d.s)
while j < len(prog) { if first != null {
let o = prog[j] g_err_file = d.file; g_err_line = d.line
if o.kind == N_FN and (o.s == d.s) { perr(`function '{d.s}' is defined twice (first in {first.file}:{itoa(first.line)})`)
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
} }
ck_tab_put(k, v, d.s, d)
} }
i += 1 i += 1
} }
@ -240,22 +240,23 @@ function decl_group(k: int) -> int {
return 0 return 0
} }
function check_duplicate_decls() -> void { function check_duplicate_decls() -> void {
let k = new []pointer
let v = new []Node
ck_tab_init(k, v)
let n = g_prog_user_end let n = g_prog_user_end
var i = 0 var i = 0
while i < n and i < len(prog) { while i < n and i < len(prog) {
let d = prog[i] let d = prog[i]
let g = decl_group(d.kind) let g = decl_group(d.kind)
if g > 0 { if g > 0 {
var j = i + 1 let key = `{itoa(g)}:{d.s}`
while j < n and j < len(prog) { let first = ck_tab_get(k, v, key)
let o = prog[j] if first != null {
if decl_group(o.kind) == g and (o.s == d.s) { g_err_file = d.file
g_err_file = o.file g_err_line = d.line
g_err_line = o.line perr(`'{d.s}' is defined twice (first in {first.file}:{itoa(first.line)})`)
perr(`'{d.s}' is defined twice (first in {d.file}:{itoa(d.line)})`)
}
j += 1
} }
ck_tab_put(k, v, key, d)
} }
i += 1 i += 1
} }
@ -264,6 +265,7 @@ function check_duplicate_decls() -> void {
function emit_program() -> void { function emit_program() -> void {
check_duplicate_fns() check_duplicate_fns()
check_duplicate_decls() check_duplicate_decls()
ix_start() # the lookups by name, as tables from here on
head = buf_new() head = buf_new()
code = buf_new() code = buf_new()
g_uses_str = false g_uses_str = false

View file

@ -29,17 +29,29 @@ function vis_plain(what: pointer) -> pointer {
function vis_say(m: pointer) -> void { function vis_say(m: pointer) -> void {
file_write(file_stderr(), m, len(m)) 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 { function vis_check(d: Node, what0: pointer) -> void {
if d == null { return } if d == null { return }
port_check_bound(d) 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) deps_edge(d, what0) # LUDIC_DEPS: the graph ludic deps reads (emit_deps.ludic)
if g_vis_off { return } if g_vis_off { return }
if d.file == null { 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) let what = vis_plain(what0)
var here = g_err_file var here = g_err_file
if here == null { return } if here == null { return }

View file

@ -49,6 +49,9 @@ function ck_expr(e: Node) -> pointer {
let f = ck_fn(e.s) let f = ck_fn(e.s)
ck_vis(f, e.s, e) ck_vis(f, e.s, e)
if f != null { return fn_sig_of(f) } if f != null { return fn_sig_of(f) }
# the emitter's own refusal, here too, so a check-only build finds it (a bind to a function
# that is gone)
if find_fn(e.s) == null { ck_err("fnref", e, `fn {e.s}: no function called {e.s}`) }
return "?" return "?"
} }
if k == E_NEW { return ck_new(e) } if k == E_NEW { return ck_new(e) }

View file

@ -19,6 +19,7 @@ var g_red_action: []pointer = new []pointer
var g_dsp_ids: []Node = new []Node # each dispatch's action number, filled at the end var g_dsp_ids: []Node = new []Node # each dispatch's action number, filled at the end
var g_dsp_names: []pointer = new []pointer var g_dsp_names: []pointer = new []pointer
var g_dsp_at: []Node = new []Node var g_dsp_at: []Node = new []Node
var g_act_main: pointer = null # the program's own file, where the drain is written
const ACTION_PASSES: int = 64 const ACTION_PASSES: int = 64
# action NAME { fields } - a record # action NAME { fields } - a record
@ -101,12 +102,11 @@ function actions_finish() -> void {
g_dsp_ids[i].ival = k g_dsp_ids[i].ival = k
i += 1 i += 1
} }
# the queue and the drain are the program's, written in its own file: in the file of the first
# action they would belong to its module, and it would call every module's reducers
var file = g_parse_file var file = g_parse_file
var line = 1 if g_act_main != null { file = g_act_main }
if len(g_act_nodes) > 0 { let line = 1
file = g_act_nodes[0].file
line = g_act_nodes[0].line
}
# no actions: drain_actions() is still there (a ludic.base runner calls it), and does nothing # no actions: drain_actions() is still there (a ludic.base runner calls it), and does nothing
if len(g_act_names) == 0 { if len(g_act_names) == 0 {
vw_parse("export function drain_actions() -> void {\n}\n", file, line, 1) vw_parse("export function drain_actions() -> void {\n}\n", file, line, 1)

View file

@ -128,15 +128,9 @@ function pkg_name_of(rel: pointer) -> pointer {
function pkg_file_set(f: pointer, name: pointer) -> void { function pkg_file_set(f: pointer, name: pointer) -> void {
push(g_pk_file, f) push(g_pk_file, f)
push(g_pk_name, name) push(g_pk_name, name)
fm_put(g_fm_pkg, f, name)
} }
function pkg_of_file(f: pointer) -> pointer { function pkg_of_file(f: pointer) -> pointer { return fm_get(g_fm_pkg, f) }
var i = len(g_pk_file) - 1
while i >= 0 {
if (g_pk_file[i] == f) { return g_pk_name[i] }
i -= 1
}
return ""
}
# the module a declaration is in, for the uses rule: its own, else its package's # the module a declaration is in, for the uses rule: its own, else its package's
function module_for_uses(f: pointer) -> pointer { function module_for_uses(f: pointer) -> pointer {
let m = module_of(f) let m = module_of(f)

View file

@ -25,36 +25,77 @@ var g_trusted_files: []pointer = new []pointer
var g_unsafe_all: bool = false var g_unsafe_all: bool = false
function unsafe_trusted(f: pointer) -> bool { function unsafe_trusted(f: pointer) -> bool {
if g_unsafe_all { return true } if g_unsafe_all { return true }
return not (fm_get(g_fm_trusted, f) == "")
}
# a file's facts - its module, its package, whether it is trusted - asked of every reference the
# checker and the emitter resolve: a table by file name, not a list walked each time (with a game's
# nine hundred files that walk was most of a check-only build)
const FM_SLOTS: int = 16384
property FileMap {
keys: []pointer = null
vals: []pointer = null
}
function fm_new() -> FileMap {
let m = new FileMap
m.keys = new []pointer
m.vals = new []pointer
var i = 0 var i = 0
while i < len(g_trusted_files) { while i < FM_SLOTS {
if (g_trusted_files[i] == f) { return true } push(m.keys, null)
push(m.vals, null)
i += 1 i += 1
} }
return false return m
}
function fm_slot(m: FileMap, f: pointer) -> int {
var h = 5381
var i = 0
let n = len(f)
while i < n {
h = (h * 33 + f[i]) % 1000003
i += 1
}
h = h % FM_SLOTS
while m.keys[h] != null and not (m.keys[h] == f) { h = (h + 1) % FM_SLOTS }
return h
}
# the latest value set for f wins, as the lists' walk from the end had it
function fm_put(m: FileMap, f: pointer, v: pointer) -> void {
if f == null { return }
let h = fm_slot(m, f)
m.keys[h] = f
m.vals[h] = v
}
function fm_get(m: FileMap, f: pointer) -> pointer {
if f == null { return "" }
let h = fm_slot(m, f)
if m.keys[h] == null { return "" }
return m.vals[h]
}
var g_fm_mod: FileMap = fm_new()
var g_fm_pkg: FileMap = fm_new()
var g_fm_trusted: FileMap = fm_new()
function trust_file(f: pointer) -> void {
push(g_trusted_files, f)
fm_put(g_fm_trusted, f, "1")
} }
var g_mod_file: []pointer = new []pointer var g_mod_file: []pointer = new []pointer
var g_mod_name: []pointer = new []pointer var g_mod_name: []pointer = new []pointer
var g_mod_friends: []pointer = new []pointer var g_mod_friends: []pointer = new []pointer
function module_of(f: pointer) -> pointer { function module_of(f: pointer) -> pointer { return fm_get(g_fm_mod, f) }
var i = len(g_mod_file) - 1
while i >= 0 {
if (g_mod_file[i] == f) { return g_mod_name[i] }
i -= 1
}
return ""
}
function module_set(f: pointer, name: pointer) -> void { function module_set(f: pointer, name: pointer) -> void {
push(g_mod_file, f) push(g_mod_file, f)
push(g_mod_name, name) push(g_mod_name, name)
fm_put(g_fm_mod, f, name)
} }
function is_float_file(f: pointer) -> bool { function is_float_file(f: pointer) -> bool {
if (f == null) { return false } if (f == null) { return false }
var i = 0 return not (fm_get(g_fm_float, f) == "")
while i < len(g_float_files) { }
if (g_float_files[i] == f) { return true } var g_fm_float: FileMap = fm_new()
i += 1 function float_file_add(f: pointer) -> void {
} push(g_float_files, f)
return false fm_put(g_fm_float, f, "1")
} }
var g_parsing: bool = true # false once lowering starts var g_parsing: bool = true # false once lowering starts
var g_err_file: pointer = "" # the statement being lowered var g_err_file: pointer = "" # the statement being lowered
@ -952,7 +993,7 @@ function parse_one_decl() -> void {
} }
if is_id("numbers") and (toks[pi + 1].text == "float") { if is_id("numbers") and (toks[pi + 1].text == "float") {
pi += 2 pi += 2
if not is_float_file(g_parse_file) { push(g_float_files, g_parse_file) } if not is_float_file(g_parse_file) { float_file_add(g_parse_file) }
return return
} }
if is_id("import") { pi += 1 if is_id("import") { pi += 1
@ -1180,11 +1221,11 @@ function do_import(rel: pointer) -> void {
} }
if already_loaded(full) { return } if already_loaded(full) { return }
push(loaded_paths, full) push(loaded_paths, full)
if is_float_file(g_parse_file) and not is_runtime_path(rel) and not is_float_file(full) { push(g_float_files, full) } if is_float_file(g_parse_file) and not is_runtime_path(rel) and not is_float_file(full) { float_file_add(full) }
# a module reaches as far as its own files: a package found through $LUDIC_HOME or # a module reaches as far as its own files: a package found through $LUDIC_HOME or
# ludic_modules is not beside its importer and keeps its own module (or none) # ludic_modules is not beside its importer and keeps its own module (or none)
let beside = full == join_path(cur_dir, rel) let beside = full == join_path(cur_dir, rel)
if is_runtime_path(rel) or not beside or (beside and unsafe_trusted(g_parse_file)) { push(g_trusted_files, full) } if is_runtime_path(rel) or not beside or (beside and unsafe_trusted(g_parse_file)) { trust_file(full) }
if beside and not is_runtime_path(rel) and not (module_of(g_parse_file) == "") { module_set(full, module_of(g_parse_file)) } if beside and not is_runtime_path(rel) and not (module_of(g_parse_file) == "") { module_set(full, module_of(g_parse_file)) }
if not beside and not is_runtime_path(rel) { pkg_file_set(full, pkg_name_of(rel)) } # modules.ludic if not beside and not is_runtime_path(rel) { pkg_file_set(full, pkg_name_of(rel)) } # modules.ludic
if beside and not is_runtime_path(rel) and not (pkg_of_file(g_parse_file) == "") { pkg_file_set(full, pkg_of_file(g_parse_file)) } if beside and not is_runtime_path(rel) and not (pkg_of_file(g_parse_file) == "") { pkg_file_set(full, pkg_of_file(g_parse_file)) }

View file

@ -44,6 +44,12 @@ function reg_fill(r: int) -> void {
j += 1 j += 1
} }
push(seen, key) push(seen, key)
if (reg_upper(key) == "COUNT") {
g_err_file = rec.file
g_err_line = rec.line
g_parsing = false
perr(`def {g_rg_name[r]} {key}: its constant would be {g_rg_prefix[r]}_COUNT, which is the registry's size; give the entry another key`)
}
if keyed and not reg_rec_has(rec, "key") { if keyed and not reg_rec_has(rec, "key") {
let fi = node(E_FINIT) let fi = node(E_FINIT)
fi.s = "key" fi.s = "key"

View file

@ -29,7 +29,7 @@ function res_read_into(reg: pointer, from: pointer, who: pointer) -> void {
let saved_file = g_parse_file let saved_file = g_parse_file
let saved_parsing = g_parsing let saved_parsing = g_parsing
if not (module_of(saved_file) == "") { module_set(path, module_of(saved_file)) } if not (module_of(saved_file) == "") { module_set(path, module_of(saved_file)) }
if is_float_file(saved_file) and not is_float_file(path) { push(g_float_files, path) } if is_float_file(saved_file) and not is_float_file(path) { float_file_add(path) }
g_parse_file = path g_parse_file = path
g_parsing = true g_parsing = true
g_res_mode = true g_res_mode = true

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -188,6 +188,7 @@ entry {
cur_dir = dir_of(path) cur_dir = dir_of(path)
g_src_name = base_name(path) # for panic/expect file:line messages g_src_name = base_name(path) # for panic/expect file:line messages
g_parse_file = path # for the compiler's own file:line diagnostics g_parse_file = path # for the compiler's own file:line diagnostics
g_act_main = path # 0.R: where the action drain is written
lex(src) lex(src)
parse_program() parse_program()
g_prog_user_end = len(prog) g_prog_user_end = len(prog)
@ -217,9 +218,15 @@ entry {
check_duplicate_decls() check_duplicate_decls()
g_mg_entry = path g_mg_entry = path
if g_migrate { mg_collect() } # 0.S2: the vars that move (migrate.ludic) if g_migrate { mg_collect() } # 0.S2: the vars that move (migrate.ludic)
ix_start() # the lookups by name, as tables (emit_core.ludic)
check_program() # L4: the types agree before anything is emitted check_program() # L4: the types agree before anything is emitted
if g_migrate { mg_finish() } if g_migrate { mg_finish() }
if g_check_only { exit(0) } # --check: the checks are all there is # --check: every check a build makes - the emitter refuses things too (a bind to a function that is
# gone, an unknown name) - and nothing written
if g_check_only {
emit_program()
exit(0)
}
emit_program() emit_program()
deps_flush() # LUDIC_DEPS=<file>: the module graph (ludic deps) deps_flush() # LUDIC_DEPS=<file>: the module graph (ludic deps)

View file

@ -91,7 +91,7 @@ function dp_load(path: pointer) -> bool {
if len(w) >= 6 and w[0] == "alias" { push(dp_aliases, lines[i]) } if len(w) >= 6 and w[0] == "alias" { push(dp_aliases, lines[i]) }
if len(w) >= 5 and w[0] == "width" and w[4] == "0" and s_to_int(w[1]) > dp_wide_n { if len(w) >= 5 and w[0] == "width" and w[4] == "0" and s_to_int(w[1]) > dp_wide_n {
dp_wide_n = s_to_int(w[1]) dp_wide_n = s_to_int(w[1])
dp_wide_at = `{w[2]} ({w[3]})` dp_wide_at = `{dp_fn_name(w[2])} ({w[3]})`
} }
if len(w) >= 5 and w[0] == "write" { if len(w) >= 5 and w[0] == "write" {
push(dp_wowner, w[1]) push(dp_wowner, w[1])
@ -102,6 +102,15 @@ function dp_load(path: pointer) -> bool {
} }
return true return true
} }
# a function as its source names it: a reducer is `reducer Bag on PickUp`, not its symbol
function dp_fn_name(n: pointer) -> pointer {
if not s_starts(n, "ludic_reduce__") { return n }
let rest = n[14 .. slen(n)]
for i in 0 .. slen(rest) - 1 {
if rest[i] == '_' and rest[i + 1] == '_' { return `reducer {rest[i + 2 .. slen(rest)]} on {rest[0 .. i]}` }
}
return n
}
function dp_own(k: int) -> bool { return dp_pkg[k] == 0 } function dp_own(k: int) -> bool { return dp_pkg[k] == 0 }
# an edge the numbers count: between two of the program's own modules # an edge the numbers count: between two of the program's own modules
function dp_counted(e: int) -> bool { return dp_own(dp_ef[e]) and dp_own(dp_et[e]) } function dp_counted(e: int) -> bool { return dp_own(dp_ef[e]) and dp_own(dp_et[e]) }

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 # --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 # refused as a full build refuses it, and a sound program writes no file
function check_build_case() -> void { function check_build_case() -> void {
let lbl = "ludicc --check / ludic build --check: modules, uses, ports and types without emitting" 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"] 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"] 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) { 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 } 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`) 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" 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)}]`) } 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 # a program built headless with the toolchain and run: its first line
function headless_case(path: pointer, exp: pointer, label: pointer) -> void { function headless_case(path: pointer, exp: pointer, label: pointer) -> void {
let out = `{tmp_dir()}/h_{flat(path)}` 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_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") 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/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/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)") 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_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/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") reject_case("rejected/dispatch_unknown", "dispatch Sell: Sell is not an action", "only an action is dispatched")