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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 05:36:11 +03:00
parent e8c1d54a96
commit f4062010e9
15 changed files with 47465 additions and 46516 deletions

View file

@ -2,8 +2,9 @@
# 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 (found through ludic_modules or the toolchain) is always usable, and
# the declared graph may not have a cycle.
# 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'.
@ -12,8 +13,6 @@ var g_mu_list: []pointer = new []pointer # ",a,b," - what it may use (union
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,"
var g_pkg_files: []pointer = new []pointer # files that belong to a package, not the project
var g_pkg_mods: []pointer = new []pointer # modules declared in a package's files
function mod_list_has(list: pointer, name: pointer) -> bool {
return has_sub(list, `,{name},`)
@ -41,11 +40,12 @@ function mod_parse_line() -> void {
pi += 1
let name = eat_id()
module_set(g_parse_file, name)
if is_pkg_file(g_parse_file) and not mod_is_pkg(name) { push(g_pkg_mods, name) }
if not is_id("uses") { return }
let ln = toks[pi].line
pi += 1
let list = mod_parse_names()
# `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
@ -80,26 +80,45 @@ function module_friend_of(from: pointer, to: pointer) -> bool {
}
return false
}
function is_pkg_file(f: pointer) -> bool {
# 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(g_pkg_files) {
if (g_pkg_files[i] == f) { return true }
while i < len(first) {
let c = first[i]
if c == '.' or c == '-' { out = out + "_" } else { out = out + first[i .. i + 1] }
i += 1
}
return false
return out
}
function mod_is_pkg(m: pointer) -> bool {
var i = 0
while i < len(g_pkg_mods) {
if (g_pkg_mods[i] == m) { return true }
i += 1
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 false
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 }
if mod_is_pkg(to) { return true }
let k = mod_find_uses(from)
if k < 0 { return true }
return mod_list_has(g_mu_list[k], to)

View file

@ -1158,8 +1158,9 @@ function do_import(rel: pointer) -> void {
# ludic_modules is not beside its importer and keeps its own module (or none)
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 (not beside and not is_runtime_path(rel)) or (beside and is_pkg_file(g_parse_file)) { push(g_pkg_files, 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 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 (src == null) { perr(`cannot open import {full}`) }
# the audio runtime can arrive through atlas.ludic's own import or the Assets
# splice, not only through an Audio.* call in the game; a windowed build must

View file

@ -9,7 +9,8 @@
# required) and a global `Clock` of that record, in the port's module and exported with it. The
# one `bind` is that global's initializer. When the program is read (ports_finish) every bind
# must name a port, give each required member, give no member twice-over, and see the port;
# a port used anywhere must be bound (port_check_bound, at the use). The binding's values are
# a port used anywhere must be bound (port_check_bound, at the use) - unless every member has a
# default, when an unbound port answers with its defaults. The binding's values are
# checked for visibility from the bind's file, here, so the port's module needs no `uses` of it.
var g_pt_name: []pointer = new []pointer
var g_pt_comp: []Node = new []Node
@ -108,6 +109,22 @@ function port_bind_one(b: Node) -> void {
nw.line = b.line
v.a = nw
}
# a port whose every member has a default needs no bind: left alone it answers with its defaults
function port_defaults_only(k: int) -> void {
let c = g_pt_comp[k]
var i = 0
while i < len(c.kids) {
if c.kids[i].a == null { return }
i += 1
}
let v = g_pt_var[k]
let nw = node(E_NEW)
nw.s = c.s
nw.file = v.file
nw.line = v.line
v.a = nw
g_pt_bind[k] = nw
}
function ports_finish() -> void {
let saved = g_parsing
g_parsing = false
@ -116,5 +133,10 @@ function ports_finish() -> void {
port_bind_one(g_bd[i])
i += 1
}
i = 0
while i < len(g_pt_name) {
if g_pt_bind[i] == null { port_defaults_only(i) }
i += 1
}
g_parsing = saved
}