feat(lang): a module's private records and events are its own
A property or event a module does not export no longer collides with another module's of the same spelling: each private one is renamed for its module, with the types, new, emit and @On written in that module. Exported ones stay one namespace; two events of one spelling are now refused. Generic records, entity components and component or view records stay global. Reseed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
029a1ebba2
commit
69352babfd
17 changed files with 50199 additions and 44822 deletions
13
LANGUAGE.md
13
LANGUAGE.md
|
|
@ -112,9 +112,16 @@ may share its spelling with a name in another module, in a file in no module, or
|
|||
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.
|
||||
seed` are still refused (`function 'seed' is defined twice`). 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.
|
||||
|
||||
The same holds for a `property` and an `event`: `fishing` and `hunting` may each have a private
|
||||
`Catch` record and a private `Landed` event with different fields, and each module's types, `new`,
|
||||
`emit` and `@On` reach its own (compiled as `Catch__fishing`). Two exported ones of one spelling are
|
||||
refused (`'Catch' is defined twice`, `event 'Landed' is defined twice`). Three kinds of record stay
|
||||
one namespace, because other code names them by spelling: a generic record, a property that is an
|
||||
entity's component (a `model` names it), and the record a component or a view generates.
|
||||
|
||||
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
|
||||
|
|
|
|||
7
changes/private-kinds.md
Normal file
7
changes/private-kinds.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**A module's private records and events are its own.** A `property` or `event` a module does not
|
||||
export no longer collides with the same spelling in another module: each private one is compiled
|
||||
under its module's name and its module's types, `new`, `emit` and `@On` follow it. Exported ones are
|
||||
one namespace, and two events of one spelling are now refused (the first used to take the other's
|
||||
emits silently). Generic records, entity components and component or view records stay global.
|
||||
20
examples/modules/kinds/fishing/index.ludic
Normal file
20
examples/modules/kinds/fishing/index.ludic
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# fishing/index.ludic - a private record and a private event, spelt as hunting spells its own
|
||||
module fishing
|
||||
property Catch {
|
||||
fish: int = 0
|
||||
}
|
||||
event Landed { fish: int }
|
||||
var landed: int = 0
|
||||
var last: Catch = null
|
||||
@On(Landed) handler Count { landed += fish }
|
||||
function catch_new(n: int) -> Catch {
|
||||
let c = new Catch
|
||||
c.fish = n
|
||||
return c
|
||||
}
|
||||
export function fishing_day() -> int {
|
||||
last = catch_new(3)
|
||||
emit Landed(fish: last.fish)
|
||||
emit Landed(fish: 4)
|
||||
return landed
|
||||
}
|
||||
15
examples/modules/kinds/hunting/index.ludic
Normal file
15
examples/modules/kinds/hunting/index.ludic
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# hunting/index.ludic - a Catch and a Landed of its own, with other fields
|
||||
module hunting
|
||||
property Catch {
|
||||
name: string = ""
|
||||
weight: float = 0.0
|
||||
}
|
||||
event Landed { name: string }
|
||||
var heard: string = ""
|
||||
@On(Landed) handler Hear { heard = heard + name }
|
||||
export function hunting_day() -> string {
|
||||
let c = new Catch
|
||||
c.name = "hare"
|
||||
emit Landed(name: c.name)
|
||||
return heard
|
||||
}
|
||||
12
examples/modules/private_kinds.ludic
Normal file
12
examples/modules/private_kinds.ludic
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# private_kinds.ludic — L3: a module's private records and events are its own too. fishing and
|
||||
# hunting each have a record Catch and an event Landed, with different fields; each module's `new`,
|
||||
# types, `emit` and `@On` reach its own.
|
||||
#
|
||||
# Running it prints: 7 hare
|
||||
import "kinds/fishing"
|
||||
import "kinds/hunting"
|
||||
program PrivateKinds {
|
||||
entry {
|
||||
print(`{fishing_day()} {hunting_day()}`)
|
||||
}
|
||||
}
|
||||
8
examples/rejected/exported_events.ludic
Normal file
8
examples/rejected/exported_events.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# L3: two modules that both export an event of one name clash
|
||||
import "exported_events/a"
|
||||
import "exported_events/b"
|
||||
program ExportedEvents {
|
||||
entry {
|
||||
print(0)
|
||||
}
|
||||
}
|
||||
3
examples/rejected/exported_events/a/index.ludic
Normal file
3
examples/rejected/exported_events/a/index.ludic
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# a/index.ludic - exports Landed
|
||||
module a
|
||||
export event Landed { fish: int }
|
||||
3
examples/rejected/exported_events/b/index.ludic
Normal file
3
examples/rejected/exported_events/b/index.ludic
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# b/index.ludic - exports Landed too
|
||||
module b
|
||||
export event Landed { name: string }
|
||||
9
examples/rejected/exported_kinds.ludic
Normal file
9
examples/rejected/exported_kinds.ludic
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# L3: two modules that both export a record of one name clash
|
||||
import "exported_kinds/a"
|
||||
import "exported_kinds/b"
|
||||
program ExportedKinds {
|
||||
entry {
|
||||
let c = new Catch
|
||||
print(0)
|
||||
}
|
||||
}
|
||||
5
examples/rejected/exported_kinds/a/index.ludic
Normal file
5
examples/rejected/exported_kinds/a/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# a/index.ludic - exports Catch
|
||||
module a
|
||||
export property Catch {
|
||||
fish: int = 0
|
||||
}
|
||||
5
examples/rejected/exported_kinds/b/index.ludic
Normal file
5
examples/rejected/exported_kinds/b/index.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# b/index.ludic - exports Catch too: exported records are one namespace
|
||||
module b
|
||||
export property Catch {
|
||||
name: string = ""
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
# (`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.
|
||||
# clash compiles exactly as before. Records and events: privates_types.ludic.
|
||||
var g_rn_mod: []pointer = new []pointer
|
||||
var g_rn_old: []pointer = new []pointer
|
||||
var g_rn_new: []pointer = new []pointer
|
||||
|
|
@ -107,8 +107,8 @@ function privates_rename() -> void {
|
|||
}
|
||||
a = b
|
||||
}
|
||||
if len(g_rn_old) == 0 { return }
|
||||
priv_apply()
|
||||
if len(g_rn_old) > 0 { priv_apply() }
|
||||
privates_types() # records and events (privates_types.ludic)
|
||||
}
|
||||
# the rename for a reference to `s` written in file `f`, or null
|
||||
function priv_find(s: pointer, f: pointer, want_fn: int) -> pointer {
|
||||
|
|
|
|||
231
selfhost/frontend/privates_types.ludic
Normal file
231
selfhost/frontend/privates_types.ludic
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
# privates_types.ludic — L3: a module's private records and events are its own too. A `property`
|
||||
# or an `event` a module does not export may share its spelling with another module's (or the
|
||||
# runtime's): where two meet, each private one is renamed for its module (`Catch` in module fishing
|
||||
# becomes `Catch__fishing`), and so is every use written in that module - a type in a signature, a
|
||||
# field, a `let`, a `new`, an `emit` and an `@On`. Exported ones stay one namespace. Three kinds are
|
||||
# left global, because other code names them by spelling: a generic record, a property that is an
|
||||
# entity's component (named by a model), and a component's or a view's generated record.
|
||||
var g_rt_mod: []pointer = new []pointer # records
|
||||
var g_rt_old: []pointer = new []pointer
|
||||
var g_rt_new: []pointer = new []pointer
|
||||
var g_re_mod: []pointer = new []pointer # events
|
||||
var g_re_old: []pointer = new []pointer
|
||||
var g_re_new: []pointer = new []pointer
|
||||
|
||||
function pt_in(xs: []pointer, s: pointer) -> bool {
|
||||
if xs == null { return false }
|
||||
var i = 0
|
||||
while i < len(xs) {
|
||||
if (xs[i] == s) { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
# a record other code names by spelling: an entity's component, a generic's argument, a UI class
|
||||
function pt_pinned(d: Node) -> bool {
|
||||
if d.tps != null { return true }
|
||||
if pt_in(g_cm_name, d.s) or pt_in(g_vw_name, d.s) { return true }
|
||||
var i = 0
|
||||
while i < len(g_gi_args) {
|
||||
if has_sub(`|{g_gi_args[i]}|`, `|{d.s}|`) or has_sub(g_gi_args[i], `<{d.s}`) { return true }
|
||||
i += 1
|
||||
}
|
||||
i = 0
|
||||
while i < len(prog) {
|
||||
let a = prog[i]
|
||||
if a.kind == N_ARCH {
|
||||
var k = 0
|
||||
while k < len(a.kids) {
|
||||
if (a.kids[k].s == d.s) { return true }
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
function pt_is_record(k: int) -> bool { return k == N_COMP or k == N_STRUCT }
|
||||
# records: every private one whose spelling another module (or the runtime) also declares
|
||||
function pt_records() -> void {
|
||||
var i = 0
|
||||
while i < g_prog_user_end and i < len(prog) {
|
||||
let d = prog[i]
|
||||
let m = module_of(d.file)
|
||||
if pt_is_record(d.kind) and d.vis != 1 and not (m == "") and d.s != null {
|
||||
var other = false
|
||||
var j = 0
|
||||
while j < len(prog) and not other {
|
||||
let o = prog[j]
|
||||
if j != i and pt_is_record(o.kind) and (o.s == d.s) and not (priv_mod(j) == m) { other = true }
|
||||
j += 1
|
||||
}
|
||||
if other and not pt_pinned(d) {
|
||||
push(g_rt_mod, m)
|
||||
push(g_rt_old, d.s)
|
||||
push(g_rt_new, `{d.s}__{m}`)
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function pt_events() -> void {
|
||||
var i = 0
|
||||
while i < len(g_events) {
|
||||
let d = g_events[i]
|
||||
var m: pointer = ""
|
||||
if d.file != null { m = module_of(d.file) }
|
||||
if d.vis != 1 and not (m == "") {
|
||||
var other = false
|
||||
var j = 0
|
||||
while j < len(g_events) {
|
||||
let o = g_events[j]
|
||||
var om: pointer = ""
|
||||
if o.file != null { om = module_of(o.file) }
|
||||
if j != i and (o.s == d.s) and not (om == m) { other = true }
|
||||
j += 1
|
||||
}
|
||||
if other {
|
||||
push(g_re_mod, m)
|
||||
push(g_re_old, d.s)
|
||||
push(g_re_new, `{d.s}__{m}`)
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function pt_id_ch(c: int) -> bool {
|
||||
return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_'
|
||||
}
|
||||
# `t` with every whole name `old` in it made `new` ("[]Catch" -> "[]Catch__fishing")
|
||||
function pt_rewrite(t: pointer, old: pointer, nw: pointer) -> pointer {
|
||||
if not has_sub(t, old) { return t }
|
||||
var out: pointer = ""
|
||||
var a = 0
|
||||
var i = 0
|
||||
let n = len(t)
|
||||
while i <= n {
|
||||
if i == n or not pt_id_ch(t[i]) {
|
||||
if i > a and (t[a .. i] == old) { out = out + nw } else { out = out + t[a .. i] }
|
||||
if i < n { out = out + t[i .. i + 1] }
|
||||
a = i + 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
# a type text written in file `f`, with its module's private records renamed
|
||||
function pt_ty(t: pointer, f: pointer) -> pointer {
|
||||
if t == null or f == null { return t }
|
||||
var out = t
|
||||
var m: pointer = null
|
||||
var k = 0
|
||||
while k < len(g_rt_old) {
|
||||
if has_sub(out, g_rt_old[k]) {
|
||||
if m == null { m = module_of(f) }
|
||||
if (m == g_rt_mod[k]) { out = pt_rewrite(out, g_rt_old[k], g_rt_new[k]) }
|
||||
}
|
||||
k += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
function pt_event(s: pointer, f: pointer) -> pointer {
|
||||
if s == null or f == null { return s }
|
||||
var k = 0
|
||||
while k < len(g_re_old) {
|
||||
if (g_re_old[k] == s) and (module_of(f) == g_re_mod[k]) { return g_re_new[k] }
|
||||
k += 1
|
||||
}
|
||||
return s
|
||||
}
|
||||
function pt_walk(n: Node) -> void {
|
||||
if n == null { return }
|
||||
if n.ty != null and len(g_rt_old) > 0 { n.ty = pt_ty(n.ty, n.file) }
|
||||
if n.kind == E_NEW and n.s != null and len(g_rt_old) > 0 { n.s = pt_ty(n.s, n.file) }
|
||||
if n.kind == S_EMIT and len(g_re_old) > 0 { n.s = pt_event(n.s, n.file) }
|
||||
pt_walk(n.a)
|
||||
pt_walk(n.b)
|
||||
pt_walk(n.c)
|
||||
if n.kids != null {
|
||||
var i = 0
|
||||
while i < len(n.kids) {
|
||||
pt_walk(n.kids[i])
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
function pt_walk_all(xs: []Node) -> void {
|
||||
if xs == null { return }
|
||||
var i = 0
|
||||
while i < len(xs) {
|
||||
pt_walk(xs[i])
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function pt_apply() -> void {
|
||||
var i = 0
|
||||
while i < g_prog_user_end and i < len(prog) {
|
||||
let d = prog[i]
|
||||
if pt_is_record(d.kind) and d.vis != 1 and d.s != null {
|
||||
var k = 0
|
||||
while k < len(g_rt_old) {
|
||||
if (g_rt_old[k] == d.s) and (module_of(d.file) == g_rt_mod[k]) {
|
||||
d.s = g_rt_new[k]
|
||||
k = len(g_rt_old)
|
||||
}
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
i = 0
|
||||
while i < len(g_events) {
|
||||
let e = g_events[i]
|
||||
if e.vis != 1 { e.s = pt_event(e.s, e.file) }
|
||||
i += 1
|
||||
}
|
||||
i = 0
|
||||
while i < g_prog_user_end and i < len(prog) {
|
||||
pt_walk(prog[i])
|
||||
i += 1
|
||||
}
|
||||
pt_walk_all(g_tests)
|
||||
pt_walk_all(g_onlisten)
|
||||
pt_walk_all(g_onspawn)
|
||||
pt_walk_all(g_ondespawn)
|
||||
pt_walk_all(g_onattach)
|
||||
pt_walk_all(g_ondetach)
|
||||
pt_walk_all(g_onenable)
|
||||
pt_walk_all(g_ondisable)
|
||||
pt_walk_all(g_computed)
|
||||
pt_walk_all(g_scenes)
|
||||
pt_walk_all(g_events)
|
||||
# an @On listener names its event by spelling, in the listener's own file
|
||||
i = 0
|
||||
while i < len(g_onlisten) {
|
||||
g_onlisten[i].s = pt_event(g_onlisten[i].s, g_onlisten[i].file)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
# two events of one spelling left after the renaming are two exports (or two in no module): an error,
|
||||
# where it used to be the first one silently taking the other's emits
|
||||
function pt_events_twice() -> void {
|
||||
var i = 0
|
||||
while i < len(g_events) {
|
||||
var j = i + 1
|
||||
while j < len(g_events) {
|
||||
if (g_events[j].s == g_events[i].s) and g_events[j].file != null {
|
||||
g_err_file = g_events[j].file
|
||||
g_err_line = g_events[j].line
|
||||
perr(`event '{g_events[i].s}' is defined twice (first in {g_events[i].file}:{itoa(g_events[i].line)})`)
|
||||
}
|
||||
j += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function privates_types() -> void {
|
||||
pt_records()
|
||||
pt_events()
|
||||
if len(g_rt_old) > 0 or len(g_re_old) > 0 { pt_apply() }
|
||||
pt_events_twice()
|
||||
}
|
||||
47340
selfhost/ludicc.seed.ll
47340
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -23,6 +23,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/modules.ludic")
|
||||
push(f, "selfhost/frontend/ports.ludic")
|
||||
push(f, "selfhost/frontend/privates.ludic")
|
||||
push(f, "selfhost/frontend/privates_types.ludic")
|
||||
push(f, "selfhost/frontend/registry.ludic")
|
||||
push(f, "selfhost/frontend/registry_finish.ludic")
|
||||
push(f, "selfhost/frontend/registry_open.ludic")
|
||||
|
|
|
|||
|
|
@ -821,6 +821,9 @@ function cmd_dev_test() -> int {
|
|||
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`")
|
||||
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)")
|
||||
feat_case("modules/private_kinds", "", "7 hare", "private_kinds.ludic (L3: two modules' private record Catch and event Landed do not collide)")
|
||||
reject_case("rejected/exported_kinds", "'Catch' is defined twice", "two modules that export one record still clash")
|
||||
reject_case("rejected/exported_events", "event 'Landed' is defined twice", "two modules that export one event clash")
|
||||
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")
|
||||
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)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue