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
3
changes/namespace-block.md
Normal file
3
changes/namespace-block.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
Namespace block form (#76) — `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)` one at a time. Inside the block each `function short(…)` is emitted as `namelower_short`; an `export` function (the default) is callable as `Name.short(…)`, while an `internal` function is a private helper — emitted and callable by its short name from siblings in the block (calls are rewritten to the emitted name), but not part of the `Name.*` surface (`Name.internalOne()` is a compile error). It is the block sugar for the per-function `@Namespace` annotation, so a package's public API reads at a glance. A namespace declared the old per-function way is unchanged. Example: `examples/library/namespace_block.ludic`.
|
||||||
28
docs/language/structure/kw-namespace.md
Normal file
28
docs/language/structure/kw-namespace.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: kw-namespace
|
||||||
|
name: namespace
|
||||||
|
category: structure
|
||||||
|
kind: keyword
|
||||||
|
tokens: namespace
|
||||||
|
sig: namespace Name { export function foo(…) … internal function bar(…) … }
|
||||||
|
tip: Group functions into a Name.* namespace and control its public surface.
|
||||||
|
order: 16
|
||||||
|
---
|
||||||
|
|
||||||
|
A <code>namespace</code> block declares a <code>Name.*</code> namespace once and controls its public surface declaratively, instead of annotating every function with <code>@Namespace(Name)</code> one at a time. Inside the block each <code>function short(…)</code> is emitted as <code>namelower_short</code>; an <code>export</code> function (the default) is callable from anywhere as <code>Name.short(…)</code>, while an <code>internal</code> function is a private helper — emitted, and callable by its short name from other functions in the same block, but not part of the <code>Name.*</code> surface (calling <code>Name.internalOne()</code> is a compile error). It is the block sugar for the per-function <code>@Namespace</code> annotation, so a package's public API reads at a glance.
|
||||||
|
|
||||||
|
Sibling calls inside the block use the short name (<code>base()</code>), and the compiler rewrites them to the emitted name — so a namespace reads like a self-contained module.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
namespace Combat {
|
||||||
|
export function amount() -> int { return base() + 5 }
|
||||||
|
internal function base() -> int { return 10 }
|
||||||
|
export function double(n: int) -> int { return n + n }
|
||||||
|
}
|
||||||
|
entry {
|
||||||
|
print(Combat.amount()) # 15
|
||||||
|
print(Combat.double(21)) # 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
20
examples/library/namespace_block.ludic
Normal file
20
examples/library/namespace_block.ludic
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# namespace_block.ludic — the `namespace Name { ... }` block form (#76). Declares
|
||||||
|
# the namespace once and controls its public surface declaratively: `export`
|
||||||
|
# functions are callable as Name.method, `internal` ones are private helpers.
|
||||||
|
# Sibling calls inside the block use the short name.
|
||||||
|
#
|
||||||
|
# Deterministic; a full run prints: 15 42 100
|
||||||
|
program NamespaceBlock {
|
||||||
|
namespace Combat {
|
||||||
|
export function amount() -> int { return base() + 5 } # calls the internal sibling by short name
|
||||||
|
internal function base() -> int { return 10 } # a private helper — not Combat.base
|
||||||
|
export function double(n: int) -> int { return n + n }
|
||||||
|
export function via_internal() -> int { return base() * 10 }
|
||||||
|
}
|
||||||
|
|
||||||
|
entry {
|
||||||
|
print(Combat.amount()) # 15 — 10 (internal base) + 5
|
||||||
|
print(Combat.double(21)) # 42
|
||||||
|
print(Combat.via_internal()) # 100 — reaches the internal helper from inside the namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
# #62: a package-provided namespace (declared with @Namespace(Foo)) that none
|
||||||
# of the hardcoded core blocks matched — alias Foo.method to the bare function
|
# 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.
|
# 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}`) }
|
if (bare == null) { perr(`unknown builtin {ns}.{meth}`) }
|
||||||
reorder_named(e, labels)
|
reorder_named(e, labels)
|
||||||
let id = node(E_ID); id.s = bare; e.a = id
|
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 }
|
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
|
# directory part of a path, including the trailing '/', or "" if none
|
||||||
function dir_of(path: pointer) -> pointer {
|
function dir_of(path: pointer) -> pointer {
|
||||||
var last = 0 - 1
|
var last = 0 - 1
|
||||||
|
|
@ -481,6 +541,26 @@ var g_esys_fn: []pointer
|
||||||
var g_esys_phase: []pointer
|
var g_esys_phase: []pointer
|
||||||
var g_namespaces: []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>`
|
# lever 5 of the controller extensibility contract (#57): `disable system <fn>`
|
||||||
# switches off exactly one engine-owned system (an esys_* registered via
|
# 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
|
# @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
|
push(prog, h); return
|
||||||
}
|
}
|
||||||
if is_id("ui") { push(prog, parse_ui()); 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("var") { push(prog, parse_var()); return }
|
||||||
if is_id("const") { push(prog, parse_const()); return }
|
if is_id("const") { push(prog, parse_const()); return }
|
||||||
if is_id("function") {
|
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, "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")
|
push(g_esys_comp, "Light2D"); push(g_esys_fn, "esys_light2d"); push(g_esys_phase, "Render")
|
||||||
g_namespaces = new []pointer
|
g_namespaces = new []pointer
|
||||||
|
g_ns_blocks = new []pointer
|
||||||
|
g_ns_exports = new []pointer
|
||||||
loaded_paths = new []pointer
|
loaded_paths = new []pointer
|
||||||
skipnl()
|
skipnl()
|
||||||
g_game_name = "Ludic"
|
g_game_name = "Ludic"
|
||||||
|
|
|
||||||
37369
selfhost/ludicc.seed.ll
37369
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -274,7 +274,8 @@
|
||||||
"kw-import",
|
"kw-import",
|
||||||
"kw-extern",
|
"kw-extern",
|
||||||
"kw-ui",
|
"kw-ui",
|
||||||
"kw-enum"
|
"kw-enum",
|
||||||
|
"kw-namespace"
|
||||||
],
|
],
|
||||||
"system": [
|
"system": [
|
||||||
"system-run",
|
"system-run",
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ object LudicTokens {
|
||||||
*/
|
*/
|
||||||
object LudicVocabulary {
|
object LudicVocabulary {
|
||||||
val DECL = setOf(
|
val DECL = setOf(
|
||||||
"program", "import", "property", "model", "enum", "ui",
|
"program", "import", "property", "model", "enum", "ui", "namespace",
|
||||||
"const", "var", "function", "extern", "handler", "entry", "event", "scene", "test"
|
"const", "var", "function", "extern", "handler", "entry", "event", "scene", "test"
|
||||||
)
|
)
|
||||||
val CLAUSE = setOf(
|
val CLAUSE = setOf(
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
||||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|function|handler|entry|state|test)\\b" },
|
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|namespace|enum|ui|const|var|let|function|handler|entry|state|test)\\b" },
|
||||||
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
|
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
|
||||||
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
|
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
|
||||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
||||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|function|handler|entry|state|test)\\b" },
|
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|namespace|enum|ui|const|var|let|function|handler|entry|state|test)\\b" },
|
||||||
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
|
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
|
||||||
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
|
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
|
||||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ typedef struct {
|
||||||
* mirror the compiler's parser: anything parse_decl() dispatches on is a
|
* mirror the compiler's parser: anything parse_decl() dispatches on is a
|
||||||
* declaration keyword, anything stmt() dispatches on is a statement keyword. */
|
* declaration keyword, anything stmt() dispatches on is a statement keyword. */
|
||||||
static const char* LUDIC_KW_DECL[] = {
|
static const char* LUDIC_KW_DECL[] = {
|
||||||
"program","import","property","model","enum","ui",
|
"program","import","property","model","enum","ui","namespace",
|
||||||
"const","var","function","extern","handler","entry","event","scene","test", 0
|
"const","var","function","extern","handler","entry","event","scene","test", 0
|
||||||
};
|
};
|
||||||
static const char* LUDIC_KW_CLAUSE[] = {
|
static const char* LUDIC_KW_CLAUSE[] = {
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ function cmd_test() -> int {
|
||||||
feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)")
|
feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)")
|
||||||
feat_case("library/anim", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34", "anim.ludic (Anim frame/once/pingpong/cell + Tween progress/loop/yoyo/ease/number/round/point/tint)")
|
feat_case("library/anim", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34", "anim.ludic (Anim frame/once/pingpong/cell + Tween progress/loop/yoyo/ease/number/round/point/tint)")
|
||||||
feat_case("library/anim_sugar", "", "4 8 2 1 0 100 100 0 0 1 20 20 30 0 1", "anim_sugar.ludic (Anim.clip/play/on_frame/fired + Motion.to + fluent Tween.to/chain/delay/parallel handles; issue #48)")
|
feat_case("library/anim_sugar", "", "4 8 2 1 0 100 100 0 0 1 20 20 30 0 1", "anim_sugar.ludic (Anim.clip/play/on_frame/fired + Motion.to + fluent Tween.to/chain/delay/parallel handles; issue #48)")
|
||||||
|
feat_case("library/namespace_block", "", "15 42 100", "namespace_block.ludic (#76 namespace Name { export/internal function } block — declares the namespace once, controls the public surface)")
|
||||||
feat_case("library/atlas", "", "16 16 32 48 1 1 1 16 1", "atlas.ludic (#81 Sprite.sheet/cell/cell_span/define/named + Assets.image/get — namespaced spritesheet/atlas with multi-cell sprites)")
|
feat_case("library/atlas", "", "16 16 32 48 1 1 1 16 1", "atlas.ludic (#81 Sprite.sheet/cell/cell_span/define/named + Assets.image/get — namespaced spritesheet/atlas with multi-cell sprites)")
|
||||||
feat_case("library/camera_zoom", "", "1 0 0 1 1", "camera_zoom.ludic (Camera.zoom deterministic Q16.16 render-time zoom about the screen centre, verified by pixel readback; issue #78)")
|
feat_case("library/camera_zoom", "", "1 0 0 1 1", "camera_zoom.ludic (Camera.zoom deterministic Q16.16 render-time zoom about the screen centre, verified by pixel readback; issue #78)")
|
||||||
feat_case("library/clear_color", "q", "1 1", "clear_color.ludic (@ClearColor: the Render phase auto-clears to the declared colour + auto-presents, no Screen.clear/show in the handler; issue #86)")
|
feat_case("library/clear_color", "q", "1 1", "clear_color.ludic (@ClearColor: the Render phase auto-clears to the declared colour + auto-presents, no Screen.clear/show in the handler; issue #86)")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue