feat(lang): friend module lab of fishing, data - a friend of the modules it names

A scoped friend sees those modules' private names and only the exports
of every other; a plain friend module still sees everything. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 04:12:47 +03:00
parent f4533331e7
commit 70d05bd504
8 changed files with 41388 additions and 41329 deletions

View file

@ -103,7 +103,8 @@ visible.ludic:5: error: add is private to module bank; mark it 'export' where it
A file in no module - the program's own file, the runtime - is public, and a package found A file in no module - the program's own file, the runtime - is public, and a package found
through `ludic_modules` or the toolchain keeps its own module rather than its importer's. A through `ludic_modules` or the toolchain keeps its own module rather than its importer's. A
program that says `friend module lab` sees every module's private names: that is for a test program that says `friend module lab` sees every module's private names: that is for a test
harness, which has to reach inside what it tests. `export` is a keyword before a declaration harness, which has to reach inside what it tests. `friend module lab of fishing, data` narrows
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.
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
@ -132,7 +133,8 @@ fishing/land.ludic:4: error: fishing uses items.inv_add (items/index.ludic:3): a
switched on one module at a time. Two module lines for one module add their lists together. switched on one module at a time. Two module lines for one module add their lists together.
- A file in no module (the program's root, the runtime) is unaffected either way, and a package's - A file in no module (the program's root, the runtime) is unaffected either way, and a package's
module (found through `ludic_modules` or the toolchain, such as `ludic_ui`) is always usable. module (found through `ludic_modules` or the toolchain, such as `ludic_ui`) is always usable.
- A friend of a module (below) is not held to its `uses` for that module. - A friend of a module (`friend module lab`, or `friend module lab of fishing`) is not held to its
`uses` for that module.
- The declared graph may not go round: `module a uses b` beside `module b uses a` is refused - The declared graph may not go round: `module a uses b` beside `module b uses a` is refused
(`the modules' uses go round in a circle: a -> b -> a`) - one of them takes the other through a (`the modules' uses go round in a circle: a -> b -> a`) - one of them takes the other through a
port instead. port instead.

5
changes/friend-of.md Normal file
View file

@ -0,0 +1,5 @@
bump: minor
type: feature
**`friend module lab of fishing, data` - a friend of some modules, not all.** A scoped friend sees
the private names of the modules it names and only the exports of every other; `friend module lab`
alone still sees everything.

View file

@ -0,0 +1,12 @@
# friend_of.ludic — L3: `friend module lab of bank` sees bank's private names and no other module's;
# a test harness reaches inside exactly what it tests. `friend module lab` alone still sees all.
#
# Running it prints: 7 7
import "bank"
program FriendOf {
friend module lab of bank
entry {
add(7)
print(`{balance} {total()}`)
}
}

View file

@ -0,0 +1,10 @@
# L3: a friend of kit is not a friend of bank - bank's private `add` stays out of reach
import "private_bank"
import "private_kit"
program FriendScope {
friend module lab of kit
entry {
add(5)
print(total())
}
}

View file

@ -61,7 +61,11 @@ function mod_parse_friend() -> void {
pi += 2 pi += 2
let fm = eat_id() let fm = eat_id()
module_set(g_parse_file, fm) module_set(g_parse_file, fm)
let scope = "" var scope = ""
if is_id("of") {
pi += 1
scope = mod_parse_names()
}
push(g_mod_friends, fm) push(g_mod_friends, fm)
push(g_fr_scope, scope) push(g_fr_scope, scope)
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -738,6 +738,8 @@ function cmd_dev_test() -> int {
feat_case("modules/uses", "", "8 3", "uses.ludic (L3: `module fishing uses base` - a module reaches only the modules it names)") feat_case("modules/uses", "", "8 3", "uses.ludic (L3: `module fishing uses base` - a module reaches only the modules it names)")
reject_case("rejected/uses_missing", "fishing uses items.inv_add", "a module that says `uses` cannot reach a module it does not name, exported or not") reject_case("rejected/uses_missing", "fishing uses items.inv_add", "a module that says `uses` cannot reach a module it does not name, exported or not")
reject_case("rejected/uses_cycle", "go round in a circle: a -> b -> a", "a cycle in the declared uses is refused") reject_case("rejected/uses_cycle", "go round in a circle: a -> b -> a", "a cycle in the declared uses is refused")
feat_case("modules/friend_of", "", "7 7", "friend_of.ludic (L3: `friend module lab of bank` sees bank's private names)")
reject_case("rejected/friend_scope", "add is private to module bank", "a friend of one module is not a friend of another")
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")