- 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>
215 lines
7 KiB
Text
215 lines
7 KiB
Text
# modules.ludic — L3 the module graph. `module fishing uses base, data` says which other modules
|
|
# fishing may reach at all: a reference from fishing into a module it does not name is refused
|
|
# (backend/emit_vis.ludic), exported or not, unless fishing is a friend of that module. A module
|
|
# that says no `uses` keeps the old rule (anything exported), so the rule comes in one module at a
|
|
# time. A package's module counts like any other (ludic_base, ludic_ui, ludic_render3d must be
|
|
# named); only the engine's own runtime, which is in no module, needs no naming. The declared
|
|
# graph may not have a cycle.
|
|
#
|
|
# `friend module lab` sees every module's private names; `friend module lab of fishing, data`
|
|
# sees only those modules'.
|
|
var g_mu_mod: []pointer = new []pointer # a module that said `uses`
|
|
var g_mu_list: []pointer = new []pointer # ",a,b," - what it may use (union of its lines)
|
|
var g_mu_file: []pointer = new []pointer # where it said so first
|
|
var g_mu_line: []int = new []int
|
|
var g_fr_scope: []pointer = new []pointer # per g_mod_friends entry: "" for all, else ",a,b,"
|
|
|
|
function mod_list_has(list: pointer, name: pointer) -> bool {
|
|
return has_sub(list, `,{name},`)
|
|
}
|
|
function mod_find_uses(m: pointer) -> int {
|
|
var i = 0
|
|
while i < len(g_mu_mod) {
|
|
if (g_mu_mod[i] == m) { return i }
|
|
i += 1
|
|
}
|
|
return -1
|
|
}
|
|
# `A, B, C` after `uses` / `of`, as ",A,B,C,"
|
|
function mod_parse_names() -> pointer {
|
|
var out = ","
|
|
out = out + eat_id() + ","
|
|
while is_op(",") {
|
|
pi += 1
|
|
out = out + eat_id() + ","
|
|
}
|
|
return out
|
|
}
|
|
# a module's layer, or "": `module flow in layer app` - the modules of one layer may use each other
|
|
# (and go round) freely; everything outside the layer is held to the module's `uses`
|
|
var g_ly_mod: []pointer = new []pointer
|
|
var g_ly_name: []pointer = new []pointer
|
|
function mod_layer(m: pointer) -> pointer {
|
|
var i = 0
|
|
while i < len(g_ly_mod) {
|
|
if (g_ly_mod[i] == m) { return g_ly_name[i] }
|
|
i += 1
|
|
}
|
|
return ""
|
|
}
|
|
function mod_set_layer(m: pointer, layer: pointer) -> void {
|
|
let was = mod_layer(m)
|
|
if (was == layer) { return }
|
|
if not (was == "") { perr(`module {m} is in layer {was}, and this line puts it in layer {layer}`) }
|
|
push(g_ly_mod, m)
|
|
push(g_ly_name, layer)
|
|
}
|
|
# `module NAME [in layer L] [uses A, B]`
|
|
function mod_parse_line() -> void {
|
|
pi += 1
|
|
let name = eat_id()
|
|
module_set(g_parse_file, name)
|
|
var layered = false
|
|
if is_id("in") and toks[pi + 1].text == "layer" {
|
|
pi += 2
|
|
mod_set_layer(name, eat_id())
|
|
layered = true
|
|
}
|
|
if not is_id("uses") and not layered { return }
|
|
let ln = toks[pi].line
|
|
# `uses` and nothing after it (or a layer and no uses): nothing outside the module's layer
|
|
var list = ","
|
|
if is_id("uses") {
|
|
pi += 1
|
|
if toks[pi].kind == TK_ID { list = mod_parse_names() }
|
|
}
|
|
let k = mod_find_uses(name)
|
|
if k >= 0 {
|
|
g_mu_list[k] = g_mu_list[k] + list
|
|
return
|
|
}
|
|
push(g_mu_mod, name)
|
|
push(g_mu_list, list)
|
|
push(g_mu_file, g_parse_file)
|
|
push(g_mu_line, ln)
|
|
}
|
|
# `friend module NAME [of A, B]`
|
|
function mod_parse_friend() -> void {
|
|
pi += 2
|
|
let fm = eat_id()
|
|
module_set(g_parse_file, fm)
|
|
var scope = ""
|
|
if is_id("of") {
|
|
pi += 1
|
|
scope = mod_parse_names()
|
|
}
|
|
push(g_mod_friends, fm)
|
|
push(g_fr_scope, scope)
|
|
}
|
|
# may `from` see `to`'s private names?
|
|
function module_friend_of(from: pointer, to: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(g_mod_friends) {
|
|
if (g_mod_friends[i] == from) {
|
|
if (g_fr_scope[i] == "") or mod_list_has(g_fr_scope[i], to) { return true }
|
|
}
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
# the package each of a package's files came from, as a module name: `ludic.render3d/r3d.ludic` is
|
|
# ludic_render3d. A package with no `module` line of its own is public to the export rule, but a
|
|
# module that says `uses` still has to name it - a mechanic reaching into the renderer is caught.
|
|
var g_pk_file: []pointer = new []pointer
|
|
var g_pk_name: []pointer = new []pointer
|
|
function pkg_name_of(rel: pointer) -> pointer {
|
|
var n = 0
|
|
while n < len(rel) and rel[n] != '/' { n += 1 }
|
|
let first = rel[0 .. n]
|
|
var out = ""
|
|
var i = 0
|
|
while i < len(first) {
|
|
let c = first[i]
|
|
if c == '.' or c == '-' { out = out + "_" } else { out = out + first[i .. i + 1] }
|
|
i += 1
|
|
}
|
|
return out
|
|
}
|
|
function pkg_file_set(f: pointer, name: pointer) -> void {
|
|
push(g_pk_file, f)
|
|
push(g_pk_name, name)
|
|
fm_put(g_fm_pkg, f, name)
|
|
}
|
|
function pkg_of_file(f: pointer) -> pointer { return fm_get(g_fm_pkg, f) }
|
|
# the module a declaration is in, for the uses rule: its own, else its package's
|
|
function module_for_uses(f: pointer) -> pointer {
|
|
let m = module_of(f)
|
|
if not (m == "") { return m }
|
|
return pkg_of_file(f)
|
|
}
|
|
# may module `from` reach into module `to` at all? (L3 uses; the export rule is separate)
|
|
function module_may_use(from: pointer, to: pointer) -> bool {
|
|
if (from == "") or (to == "") or (from == to) { return true }
|
|
let k = mod_find_uses(from)
|
|
if k < 0 { return true }
|
|
if not (mod_layer(from) == "") and (mod_layer(from) == mod_layer(to)) { return true }
|
|
return mod_list_has(g_mu_list[k], to)
|
|
}
|
|
# the names in ",a,b," as a slice
|
|
function mod_names(list: pointer) -> []pointer {
|
|
let out = new []pointer
|
|
var a = 1
|
|
var j = 1
|
|
while j < len(list) {
|
|
if list[j] == ',' {
|
|
if j > a { push(out, list[a .. j]) }
|
|
a = j + 1
|
|
}
|
|
j += 1
|
|
}
|
|
return out
|
|
}
|
|
# the declared uses graph has no cycle: a depth-first walk that carries its path, "a -> b -> "
|
|
var g_mu_done: pointer = "," # nodes whose every path is known to end
|
|
# the graph the cycle check walks: a layer is one node ("layer app"), since its modules may cycle
|
|
function mod_node(m: pointer) -> pointer {
|
|
let l = mod_layer(m)
|
|
if (l == "") { return m }
|
|
return `layer {l}`
|
|
}
|
|
# a node's edges, as ",a,layer b,": every uses entry of the modules in it, leaving its own layer
|
|
function mod_node_edges(nd: pointer) -> pointer {
|
|
var out = ","
|
|
var k = 0
|
|
while k < len(g_mu_mod) {
|
|
if (mod_node(g_mu_mod[k]) == nd) {
|
|
let next = mod_names(g_mu_list[k])
|
|
var n = 0
|
|
while n < len(next) {
|
|
let t = mod_node(next[n])
|
|
if not (t == nd) and not has_sub(out, `,{t},`) { out = out + t + "," }
|
|
n += 1
|
|
}
|
|
}
|
|
k += 1
|
|
}
|
|
return out
|
|
}
|
|
function mod_walk(m: pointer, path: pointer, first: pointer) -> void {
|
|
if has_sub(`-> {path}`, `-> {m} -> `) {
|
|
let k0 = mod_find_uses(first)
|
|
g_err_file = g_mu_file[k0]
|
|
g_err_line = g_mu_line[k0]
|
|
perr(`the modules' uses go round in a circle: {path}{m}; one of them has to take the other through a port, or the ones that belong together declare one layer (module m in layer L)`)
|
|
}
|
|
if has_sub(g_mu_done, `,{m},`) { return }
|
|
let edges = mod_node_edges(m)
|
|
if (edges == ",") { return }
|
|
let next = mod_names(edges)
|
|
var n = 0
|
|
while n < len(next) {
|
|
mod_walk(next[n], `{path}{m} -> `, first)
|
|
n += 1
|
|
}
|
|
g_mu_done = g_mu_done + m + ","
|
|
}
|
|
function modules_finish() -> void {
|
|
let saved = g_parsing
|
|
g_parsing = false
|
|
var i = 0
|
|
while i < len(g_mu_mod) {
|
|
mod_walk(mod_node(g_mu_mod[i]), "", g_mu_mod[i])
|
|
i += 1
|
|
}
|
|
g_parsing = saved
|
|
}
|