ludic/selfhost/frontend/modules.ludic
Orkuncakilkaya f4062010e9 feat(lang): packages count under uses; a port of defaults may go unbound
A module that says uses must name every package module it reaches; a
package with no module line is named for its directory (ludic_render3d)
for this rule, so a mechanic reaching into the renderer is caught. Only
the engine's runtime needs no naming. 'module x uses' with nothing after
it reaches no other module. A port whose every member has a default
answers with its defaults when unbound, and 'new' of a port says to bind
it. ludic-dev test runs 'ludic test packages'. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:36:11 +03:00

169 lines
5.3 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
}
# `module NAME [uses A, B]`
function mod_parse_line() -> void {
pi += 1
let name = eat_id()
module_set(g_parse_file, name)
if not is_id("uses") { return }
let ln = toks[pi].line
pi += 1
# `uses` and nothing after it: a module that reaches no other module at all
var list = ","
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)
}
function pkg_of_file(f: pointer) -> pointer {
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
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 }
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 = "," # modules whose every path is known to end
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`)
}
if has_sub(g_mu_done, `,{m},`) { return }
let k = mod_find_uses(m)
if k < 0 { return }
let next = mod_names(g_mu_list[k])
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(g_mu_mod[i], "", g_mu_mod[i])
i += 1
}
g_parsing = saved
}