feat(lang): #76 namespace block form with export/internal visibility
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m25s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s

`namespace Name { export function foo(...) ... internal function bar(...) ... }`
declares a Name.* namespace once and controls its public surface declaratively,
instead of annotating every function with @Namespace(Name). Inside the block
each `function short(...)` is emitted as `namelower_short`; export (the default)
makes it callable as Name.short(...), internal keeps it a private helper
(emitted, callable by short name from siblings — calls are rewritten — but
Name.internalOne() is a compile error). Block sugar for the per-function
@Namespace annotation; a package's public API reads at a glance. Namespaces
declared the old way are unchanged (gameplay_foundation still passes).

namespace added to LUDIC_KW_DECL + JetBrains/TextMate + docs page + inventory
(vocab/impl/docs checks green). Example namespace_block (export + internal +
sibling calls + internal-visibility compile error verified). Full suite 116/0,
fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-02 08:14:54 +03:00
parent 3eb5447f74
commit 23e232e380
12 changed files with 19192 additions and 18330 deletions

View file

@ -419,6 +419,66 @@ function parse_fn() -> Node {
}
function parse_main() -> Node { pi = pi + 1; let n = node(N_MAIN); n.a = block(); return n }
# issue #76 — rewrite calls to a namespace-block sibling (a bare short-name call)
# to the prefixed function name, so a body can call `pending()` where the function
# is emitted as `combat_pending`. Only E_CALL callees that are bare identifiers are
# rewritten (Ludic has no function values, and Name.method calls are E_MEMBER).
function ns_short_in(names: []pointer, s: pointer) -> bool {
var i = 0
while i < len(names) { if (names[i] == s) { return true }; i = i + 1 }
return false
}
function ns_rewrite_calls(n: Node, shorts: []pointer, prefix: pointer) -> void {
if (n == null) { return }
if (n.kind == E_CALL) and (n.a != null) and (n.a.kind == E_ID) {
if ns_short_in(shorts, n.a.s) { n.a.s = prefix + n.a.s }
}
ns_rewrite_calls(n.a, shorts, prefix)
ns_rewrite_calls(n.b, shorts, prefix)
ns_rewrite_calls(n.c, shorts, prefix)
var i = 0
while i < len(n.kids) { ns_rewrite_calls(n.kids[i], shorts, prefix); i = i + 1 }
}
# issue #76 — `namespace Name { [export|internal] function short(…) … }`. Declares
# the namespace once; each function is emitted as `<namelower>_short` and, when
# exported (the default; `internal` opts out), registered so `Name.short(…)`
# dispatches to it — the same generic path @Namespace(Name) uses, so it is the
# block sugar for it. Sibling calls inside the block are rewritten to the prefixed
# name. `internal` keeps a function as a private helper: it is emitted but not part
# of the Name.* surface.
function parse_namespace() -> void {
pi = pi + 1 # eat "namespace"
let nsname = eat_id()
eat_op("{"); skipnl()
let fns = new []Node
let shorts = new []pointer
let exps = new []int
while not is_op("}") {
var is_exp = 1 # default: exported (public)
if is_id("export") { pi = pi + 1 }
else { if is_id("internal") { pi = pi + 1; is_exp = 0 } }
if not is_id("function") { perr("a namespace body holds functions: expected `function`") }
let f = parse_fn()
push(fns, f); push(shorts, f.s); push(exps, is_exp)
skipnl()
}
eat_op("}")
let prefix = ns_lower(nsname) + ("_")
var i = 0 # prefix every function name
while i < len(fns) { fns[i].s = prefix + shorts[i]; i = i + 1 }
i = 0 # rewrite sibling calls to the prefixed name
while i < len(fns) { ns_rewrite_calls(fns[i].a, shorts, prefix); i = i + 1 }
register_namespace(nsname) # Name.method dispatch (generic path)
push(g_ns_blocks, nsname)
i = 0
while i < len(fns) {
if (exps[i] == 1) { push(g_ns_exports, fns[i].s) } # record the public surface
push(prog, fns[i])
i = i + 1
}
}
# directory part of a path, including the trailing '/', or "" if none
function dir_of(path: pointer) -> pointer {
var last = 0 - 1
@ -481,6 +541,26 @@ var g_esys_fn: []pointer
var g_esys_phase: []pointer
var g_namespaces: []pointer
# issue #76: `namespace Name { export/internal function … }` block form. A block
# declares the namespace once and controls its public surface declaratively.
# g_ns_blocks names the namespaces declared this way; g_ns_exports holds the
# prefixed function names that are *exported* (e.g. "combat_amount"), so emit_ns_call
# can reject Name.method for an `internal` helper. (A namespace declared the old
# per-function @Namespace(Name) way has no block entry, so all its methods stay
# dispatchable — back-compatible.)
var g_ns_blocks: []pointer
var g_ns_exports: []pointer
function is_ns_block(name: pointer) -> bool {
var i = 0
while i < len(g_ns_blocks) { if (g_ns_blocks[i] == name) { return true }; i = i + 1 }
return false
}
function ns_export_has(prefixed: pointer) -> bool {
var i = 0
while i < len(g_ns_exports) { if (g_ns_exports[i] == prefixed) { return true }; i = i + 1 }
return false
}
# lever 5 of the controller extensibility contract (#57): `disable system <fn>`
# switches off exactly one engine-owned system (an esys_* registered via
# @EngineSystem or the core seed) at compile time, so a game can carry a
@ -626,6 +706,7 @@ function parse_one_decl() -> void {
push(prog, h); return
}
if is_id("ui") { push(prog, parse_ui()); return }
if is_id("namespace") { parse_namespace(); return } # #76 namespace block
if is_id("var") { push(prog, parse_var()); return }
if is_id("const") { push(prog, parse_const()); return }
if is_id("function") {
@ -973,6 +1054,8 @@ function parse_program() -> void {
push(g_esys_comp, "Sprite"); push(g_esys_fn, "esys_sprite"); push(g_esys_phase, "Render")
push(g_esys_comp, "Light2D"); push(g_esys_fn, "esys_light2d"); push(g_esys_phase, "Render")
g_namespaces = new []pointer
g_ns_blocks = new []pointer
g_ns_exports = new []pointer
loaded_paths = new []pointer
skipnl()
g_game_name = "Ludic"