feat(lang): #76 namespace block form with export/internal visibility
`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:
parent
3eb5447f74
commit
23e232e380
12 changed files with 19192 additions and 18330 deletions
|
|
@ -689,7 +689,12 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
# #62: a package-provided namespace (declared with @Namespace(Foo)) that none
|
||||
# of the hardcoded core blocks matched — alias Foo.method to the bare function
|
||||
# foo_method (positional args), the same generic path the core aliases use.
|
||||
if (bare == null) and is_registered_namespace(ns) { bare = ns_lower(ns) + ("_") + meth }
|
||||
if (bare == null) and is_registered_namespace(ns) {
|
||||
bare = ns_lower(ns) + ("_") + meth
|
||||
# #76: a namespace declared with a block controls its public surface — an
|
||||
# `internal` method is emitted but not callable as Name.method.
|
||||
if is_ns_block(ns) and (not ns_export_has(bare)) { perr(`{ns}.{meth} is internal to namespace {ns}`) }
|
||||
}
|
||||
if (bare == null) { perr(`unknown builtin {ns}.{meth}`) }
|
||||
reorder_named(e, labels)
|
||||
let id = node(E_ID); id.s = bare; e.a = id
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
37369
selfhost/ludicc.seed.ll
37369
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue