feat(lang): a module's private names are its own

A private function, var or const no longer collides with the same
spelling in another module, in no module or in the runtime: where two
meet, each private one is renamed for its module (seed$shop), with every
reference its module writes that no local shadows. Exported names stay
one namespace. Nothing is renamed without a clash. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 07:08:05 +03:00
parent c9dc592dcc
commit a09b622a1d
14 changed files with 49752 additions and 44752 deletions

View file

@ -107,6 +107,15 @@ harness, which has to reach inside what it tests. `friend module lab of fishing,
that to the modules named: `lab` sees their private names, and every other module's exports only. `export` is a keyword before a declaration that to the modules named: `lab` sees their private names, and every other module's exports only. `export` is a keyword before a declaration
and is not the `@export` annotation, which names a C symbol. and is not the `@export` annotation, which names a C symbol.
**A module's private names are its own.** A function, `var` or `const` a module does not export
may share its spelling with a name in another module, in a file in no module, or in the engine's
runtime: `shop` and `weather` may each have a private `seed`, and each module's code reaches its
own (a local of the same name still shadows it). There is no need to prefix a package's privates.
Exported names are one namespace across the program, so two modules that both `export function
seed` are still refused (`function 'seed' is defined twice`); and records and events - private or
not - still share one namespace. Where two names meet, the private one is compiled under its
module's name (`seed$shop`), which is the spelling a message about it may show.
To move an existing codebase onto modules, build it once with `LUDIC_VIS_REPORT=1`: every To move an existing codebase onto modules, build it once with `LUDIC_VIS_REPORT=1`: every
reference that would be refused is printed as `vis: <file>:<line>: <module>.<name> used from reference that would be refused is printed as `vis: <file>:<line>: <module>.<name> used from
<file>` and the build goes on, so a script can add the `export`s the program already relies on. <file>` and the build goes on, so a script can add the `export`s the program already relies on.

View file

@ -0,0 +1,8 @@
bump: minor
type: feature
**A module's private names are its own.** A function, var or const a module does not export no
longer collides with the same spelling in another module, in a file in no module or in the
runtime: each private one is compiled under its module's name (`seed$shop`) and its module's
references follow it. Exported names are still one namespace, and two exports of one name are
still refused. Records and events still share one namespace. A program with no such clash compiles
exactly as before.

View file

@ -0,0 +1,15 @@
# private_twins.ludic — L3: a module's private names are its own. shop and weather each have a
# private `seed`, `dice` and `LIMIT`, and the program has a `seed` of its own; none of them collide,
# and each module's code reaches its own.
#
# Running it prints: 105 41 7
import "twins/shop"
import "twins/weather"
program PrivateTwins {
function seed() -> int {
return 7
}
entry {
print(`{shop_seed()} {weather_seed()} {seed()}`)
}
}

View file

@ -0,0 +1,10 @@
# shop/index.ludic - private names a neighbour also uses: seed, dice, LIMIT
module shop
var dice: int = 100
const LIMIT: int = 5
function seed() -> int {
return dice + LIMIT
}
export function shop_seed() -> int {
return seed()
}

View file

@ -0,0 +1,13 @@
# weather/index.ludic - the same private spellings, its own; a local named dice is its own too
module weather
var dice: int = 20
const LIMIT: int = 2
function seed() -> int {
return dice * LIMIT
}
function roll(dice: int) -> int {
return dice + 1
}
export function weather_seed() -> int {
return seed() + roll(0)
}

View file

@ -0,0 +1,8 @@
# L3: two modules that both EXPORT a name clash - only private names are a module's own
import "exported_twins/a"
import "exported_twins/b"
program ExportedTwins {
entry {
print(seed())
}
}

View file

@ -0,0 +1,5 @@
# a/index.ludic - exports seed
module a
export function seed() -> int {
return 1
}

View file

@ -0,0 +1,5 @@
# b/index.ludic - exports seed too: exported names are one namespace
module b
export function seed() -> int {
return 2
}

View file

@ -0,0 +1,243 @@
# privates.ludic — L3: a module's private names are its own. A function, var or const that a module
# does not export may share its spelling with a name in another module (or in no module, or the
# engine's runtime): when two such declarations meet, each private one is renamed for its module
# (`sp_seed` in module shop becomes `sp_seed$shop`), and so is every reference to it written in that
# module that no local shadows. Exported and public names stay one namespace, so a clash between two
# of those is still the error it was. Nothing is renamed when nothing clashes, so a program with no
# clash compiles exactly as before. Records and events still share one namespace.
var g_rn_mod: []pointer = new []pointer
var g_rn_old: []pointer = new []pointer
var g_rn_new: []pointer = new []pointer
var g_rn_fn: []int = new []int # 1: a function; 0: a var or const
# which names a declaration's spelling competes with: functions with functions, values with values
function priv_group(k: int) -> int {
if k == N_FN { return 2 }
if k == N_VAR or k == N_CONST { return 1 }
return 0
}
function priv_less(a: Node, b: Node) -> bool { return reg_str_less(a.s, b.s) }
# the declarations that have a name, sorted by it (a merge sort: a program has thousands)
function priv_sort(xs: []int, lo: int, hi: int, tmp: []int) -> void {
if hi - lo < 2 { return }
let mid = (lo + hi) / 2
priv_sort(xs, lo, mid, tmp)
priv_sort(xs, mid, hi, tmp)
var i = lo
var j = mid
var k = lo
while k < hi {
if j >= hi or (i < mid and not priv_less(prog[xs[j]], prog[xs[i]])) {
tmp[k] = xs[i]
i += 1
} else {
tmp[k] = xs[j]
j += 1
}
k += 1
}
k = lo
while k < hi {
xs[k] = tmp[k]
k += 1
}
}
# the module a declaration is in; the engine's runtime (spliced after the program) is in none
function priv_mod(i: int) -> pointer {
if i >= g_prog_user_end { return "" }
return module_of(prog[i].file)
}
# one run of declarations of one name and group, [a, b): rename every private one whose module
# is not the only one the name is declared in
function priv_run(xs: []int, a: int, b: int) -> void {
var k = a
while k < b {
let i = xs[k]
let d = prog[i]
let m = priv_mod(i)
if i < g_prog_user_end and d.vis != 1 and not (m == "") {
var other = false
var j = a
while j < b {
if j != k and not (priv_mod(xs[j]) == m) { other = true }
j += 1
}
if other {
push(g_rn_mod, m)
push(g_rn_old, d.s)
push(g_rn_new, `{d.s}${m}`)
var f = 0
if d.kind == N_FN { f = 1 }
push(g_rn_fn, f)
}
}
k += 1
}
}
function privates_rename() -> void {
let xs = new []int
let tmp = new []int
var i = 0
while i < len(prog) {
if priv_group(prog[i].kind) > 0 and prog[i].s != null {
push(xs, i)
push(tmp, 0)
}
i += 1
}
priv_sort(xs, 0, len(xs), tmp)
var a = 0
while a < len(xs) {
var b = a + 1
let da = prog[xs[a]]
while b < len(xs) and (prog[xs[b]].s == da.s) { b += 1 }
if b - a > 1 {
# within the run, each group on its own
var g = 1
while g <= 2 {
let sub = new []int
var k = a
while k < b {
if priv_group(prog[xs[k]].kind) == g { push(sub, xs[k]) }
k += 1
}
if len(sub) > 1 { priv_run(sub, 0, len(sub)) }
g += 1
}
}
a = b
}
if len(g_rn_old) == 0 { return }
priv_apply()
}
# the rename for a reference to `s` written in file `f`, or null
function priv_find(s: pointer, f: pointer, want_fn: int) -> pointer {
var k = 0
while k < len(g_rn_old) {
if (g_rn_old[k] == s) and g_rn_fn[k] == want_fn {
if f != null and (module_of(f) == g_rn_mod[k]) { return g_rn_new[k] }
}
k += 1
}
return null
}
function priv_known(s: pointer) -> bool {
var k = 0
while k < len(g_rn_old) {
if (g_rn_old[k] == s) { return true }
k += 1
}
return false
}
# a name in a scope string ",a,b,": a local that shadows a module's global
function priv_shadowed(sc: pointer, s: pointer) -> bool { return has_sub(sc, `,{s},`) }
function priv_ref(n: Node, sc: pointer, want_fn: int) -> void {
if n.s == null or not priv_known(n.s) { return }
if want_fn == 0 and priv_shadowed(sc, n.s) { return }
let r = priv_find(n.s, n.file, want_fn)
if r != null { n.s = r }
}
# every reference below `n`; `sc` is the locals in scope
function priv_walk(n: Node, sc: pointer) -> void {
if n == null { return }
if n.kind == E_FNREF {
priv_ref(n, sc, 1)
return
}
if n.kind == E_ID {
priv_ref(n, sc, 0)
return
}
if n.kind == E_CALL {
if n.a != null and n.a.kind == E_ID { priv_ref(n.a, sc, 1) } else { priv_walk(n.a, sc) }
priv_kids(n, sc)
return
}
if n.kind == N_BLOCK {
var s = sc
var i = 0
while i < len(n.kids) {
priv_walk(n.kids[i], s)
if n.kids[i].kind == S_LET { s = s + n.kids[i].s + "," }
i += 1
}
priv_walk(n.a, s)
return
}
if n.kind == S_FOR {
priv_walk(n.a, sc)
priv_walk(n.b, sc)
priv_walk(n.c, sc + n.s + ",")
return
}
if n.kind == N_FN {
var s = sc
var i = 0
while i < len(n.kids) {
priv_walk(n.kids[i].a, sc)
if n.kids[i].kind == N_PARAM { s = s + n.kids[i].s + "," }
i += 1
}
priv_walk(n.a, s)
return
}
priv_walk(n.a, sc)
priv_walk(n.b, sc)
priv_walk(n.c, sc)
priv_kids(n, sc)
}
function priv_kids(n: Node, sc: pointer) -> void {
if n.kids == null { return }
var i = 0
while i < len(n.kids) {
priv_walk(n.kids[i], sc)
i += 1
}
}
function priv_walk_all(xs: []Node) -> void {
if xs == null { return }
var i = 0
while i < len(xs) {
priv_walk(xs[i], ",")
i += 1
}
}
function priv_apply() -> void {
# the declarations themselves
var i = 0
while i < g_prog_user_end and i < len(prog) {
let d = prog[i]
let g = priv_group(d.kind)
if g > 0 and d.vis != 1 and d.s != null and priv_known(d.s) {
var f = 0
if g == 2 { f = 1 }
let r = priv_find(d.s, d.file, f)
if r != null { d.s = r }
}
i += 1
}
# every reference: the program's declarations, and the bodies kept beside them
i = 0
while i < g_prog_user_end and i < len(prog) {
priv_walk(prog[i], ",")
i += 1
}
priv_walk_all(g_tests)
priv_walk_all(g_onlisten)
priv_walk_all(g_onspawn)
priv_walk_all(g_ondespawn)
priv_walk_all(g_onattach)
priv_walk_all(g_ondetach)
priv_walk_all(g_onenable)
priv_walk_all(g_ondisable)
priv_walk_all(g_computed)
priv_walk_all(g_scenes)
priv_walk_all(g_events)
# a namespace method declared in a module names its target by spelling
i = 0
while i < len(g_al_target) {
let r = priv_find(g_al_target[i], g_al_file[i], 1)
if r != null { g_al_target[i] = r }
i += 1
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -203,6 +203,7 @@ entry {
else { g_windowed = has_target and has_systems() } else { g_windowed = has_target and has_systems() }
g_parsing = false # lowering errors now locate by statement, not token g_parsing = false # lowering errors now locate by statement, not token
privates_rename() # L3: a module's private names are its own (privates.ludic)
check_duplicate_fns() # two declarations of one name: the cause, before its symptoms check_duplicate_fns() # two declarations of one name: the cause, before its symptoms
check_duplicate_decls() check_duplicate_decls()
check_program() # L4: the types agree before anything is emitted check_program() # L4: the types agree before anything is emitted

View file

@ -22,6 +22,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/frontend/aliases.ludic") push(f, "selfhost/frontend/aliases.ludic")
push(f, "selfhost/frontend/modules.ludic") push(f, "selfhost/frontend/modules.ludic")
push(f, "selfhost/frontend/ports.ludic") push(f, "selfhost/frontend/ports.ludic")
push(f, "selfhost/frontend/privates.ludic")
push(f, "selfhost/frontend/registry.ludic") push(f, "selfhost/frontend/registry.ludic")
push(f, "selfhost/frontend/registry_finish.ludic") push(f, "selfhost/frontend/registry_finish.ludic")
push(f, "selfhost/frontend/registry_open.ludic") push(f, "selfhost/frontend/registry_open.ludic")

View file

@ -795,6 +795,8 @@ function cmd_dev_test() -> int {
reject_case("rejected/port_private", "Clock is private to module clock", "whoever binds a port has to see it") reject_case("rejected/port_private", "Clock is private to module clock", "whoever binds a port has to see it")
reject_case("rejected/uses_package", "mech uses ludic_inventory.inv_count", "a package's module counts under `uses` like any other") reject_case("rejected/uses_package", "mech uses ludic_inventory.inv_count", "a package's module counts under `uses` like any other")
reject_case("rejected/uses_render3d", "mech uses ludic_render3d.PI", "a package with no module line is still named under `uses`") reject_case("rejected/uses_render3d", "mech uses ludic_render3d.PI", "a package with no module line is still named under `uses`")
feat_case("modules/private_twins", "", "105 41 7", "private_twins.ludic (L3: two modules' private seed, dice and LIMIT, and the program's seed, do not collide)")
reject_case("rejected/exported_twins", "function 'seed' is defined twice", "two modules that export one name still clash")
vis_report_case("rejected/uses_missing", "uses: examples/rejected/uses_valley/items/index.ludic:3: items.inv_add used from", "LUDIC_VIS_REPORT=1 lists a uses violation and builds") vis_report_case("rejected/uses_missing", "uses: examples/rejected/uses_valley/items/index.ludic:3: items.inv_add used from", "LUDIC_VIS_REPORT=1 lists a uses violation and builds")
feat_case("lang/checked", "", "7.5 3 12 hi 2 ok", "checked.ludic (L4: literals take their slot's kind, string(p), []string as []pointer, null, named args)") feat_case("lang/checked", "", "7.5 3 12 hi 2 ok", "checked.ludic (L4: literals take their slot's kind, string(p), []string as []pointer, null, named args)")
reject_case("rejected/wrong_arity", "this call to area leaves out h, which has no default", "a call with the wrong number of arguments is refused") reject_case("rejected/wrong_arity", "this call to area leaves out h, which has no default", "a call with the wrong number of arguments is refused")