docs(pkg): document @Namespace / @EngineSystem / @System + package hooks (#62)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m41s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 27s

Per-annotation pages for the three package-registration annotations (with
inventory entries), a "Package-declarable namespaces and engine systems" section
in docs/PACKAGES.md, and a changeset. All doc gates green (check-docs 675
fences, docs-check 726 symbols).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-01 12:31:47 +03:00
parent 7cbd5d175c
commit 6c6dfae235
6 changed files with 119 additions and 1 deletions

View file

@ -0,0 +1,3 @@
bump: minor
type: feat
Package-declarable namespaces & engine systems (#62) — the two hooks that made `Foo.*` stdlib namespaces and engine-owned systems compiler-hardcoded are now data-driven registries, so a package registers them with no compiler edit. `@Namespace(Name)` on a function opens a `Name.method(…)` namespace that dispatches to the bare `name_method` through the same generic path the built-in namespaces use (applied only after them, so it never shadows a core one). `@EngineSystem(Component, Phase)` registers an engine-owned system the frame loop runs each phase when the component is present — the package-declarable form of the built-in SpriteAnim/Motion/Light2D systems, reading components by name through the reflection ABI so an unused registration is byte-identical. Both annotations are keyword-free. The core stdlib keeps its optimized codegen (byte-identical output; the C-free bootstrap fixpoint is untouched) and packages ride the generic registry alongside it. This completes the packaging prerequisite for shipping gameplay-controller libraries (#58–#61) as real packages. See docs/PACKAGES.md.

View file

@ -102,6 +102,37 @@ still hold.
Two packages may not register the same `Foo.*` namespace — a collision is a hard
error naming both modules.
## Package-declarable namespaces and engine systems (issue #62)
A package can register two things that used to be compiler-hardcoded — a `Foo.*`
namespace and an engine-owned system — with **no compiler edit**, via two
keyword-free annotations. This is what lets gameplay-controller libraries ship as
ordinary packages instead of living in the compiler's stdlib.
- `@Namespace(Name)` on a function opens a `Name.method(…)` namespace that
dispatches to the bare `name_method(…)` (the same generic path the built-in
namespaces use, applied only after them so it never shadows a core one):
```
@Namespace(Coach) function coach_bonus() -> int { return 99 }
# a consumer then writes Coach.bonus()
```
- `@EngineSystem(Component, Phase)` registers an engine-owned system: the frame
loop calls it every `Phase` (after the game's own handlers) whenever the named
`Component` is present — the package-declarable form of the built-in
`SpriteAnim`/`Motion`/`Light2D` systems. It reads and writes components by name
through the reflection ABI, so an unused registration is byte-identical:
```
@EngineSystem(Score, Update) function esys_score() -> void { … }
```
The core stdlib namespaces and engine systems keep their own optimized codegen;
packages flow through the generic registry alongside them. (For a *prebuilt*
binary module, the runtime counterpart of `@EngineSystem` is `@System(Phase)` —
see below.)
## Prebuilt binary packages (issue #64)
A `kind prebuilt` package ships a **compiled artifact** (a native dylib per

View file

@ -0,0 +1,33 @@
---
id: annot-enginesystem
name: @EngineSystem
category: annotations
kind: annotation
tokens: @EngineSystem
sig: "@EngineSystem(Component, Phase) function esys_name() { … }"
tip: Register a package's engine-owned system so the frame loop runs it each phase.
order: 60
---
<code>@EngineSystem(Component, Phase)</code> marks a function as an engine-owned system: the compiler adds it to the engine-system registry, and the frame loop calls it every <code>Phase</code> — after the game's own handlers — whenever the named <code>Component</code> is present in the program. It is the package-declarable form of the built-in engine systems (the ones that advance <code>SpriteAnim</code>/<code>Motion</code>/<code>Light2D</code>), so a third-party controller ships a system the same way the standard library does, with no compiler edit. The function reads and writes components by name through the reflection ABI, so it stays layout-independent and additive: a build that never declares the component compiles byte-for-byte the same. Phases are <code>Input</code>, <code>FixedUpdate</code>, <code>Update</code>, <code>LateUpdate</code> and <code>Render</code>.
```ludic
program EngineSystemDemo {
property Score { value: int = 0 }
model Player { Score }
@EngineSystem(Score, Update) function esys_score() -> void { # runs every Update
let P = world_prop_id("Score")
let e = world_query_next(P, 0)
if e >= 0 { world_set(e, P, 0, world_get(e, P, 0) + 1) } # advance by name
}
@OnSpawn(Player) handler Init { }
handler Boot phase Start { let e = world_spawn(world_model_id("Player")) }
handler Run phase Update {
let P = world_prop_id("Score")
print(world_get(world_query_next(P, 0), P, 0)) # grows each frame
quit()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: annot-namespace
name: @Namespace
category: annotations
kind: annotation
tokens: @Namespace
sig: "@Namespace(Name) function name_method(…) -> T { … }"
tip: Let a package provide a Name.method(…) namespace that dispatches to name_method.
order: 61
---
<code>@Namespace(Name)</code> registers <code>Name</code> as a namespace a package provides, so a caller can write <code>Name.method(…)</code> and the compiler dispatches it to the bare function <code>name_method(…)</code> (the namespace lowercased, an underscore, then the method). It is the package-declarable form of the built-in <code>Foo.*</code> stdlib namespaces (<code>Regex.*</code>, <code>Grid.*</code>, …): a third-party package ships the same call shape without editing the compiler. The alias goes through the same generic call path the core namespaces use and preserves the target function's return type; it only applies after every built-in namespace, so it never shadows a core one. Mark each function that backs a method (the registration is idempotent — one mark is enough to open the namespace, but marking each provider documents the surface).
```ludic
program NamespaceDemo {
@Namespace(Coach) function coach_bonus() -> int { return 99 }
@Namespace(Coach) function coach_double(n: int) -> int { return n + n }
entry {
print(Coach.bonus()) # 99 — dispatches to coach_bonus
print(Coach.double(21)) # 42 — dispatches to coach_double
}
}
```

View file

@ -0,0 +1,24 @@
---
id: annot-system
name: @System
category: annotations
kind: annotation
tokens: @System
sig: "@System(Phase) function name() { … }"
tip: Register a prebuilt binary module's system with the host at load, per phase.
order: 62
---
<code>@System(Phase)</code> marks a function in a prebuilt binary module (one compiled with <code>--emit-module</code>) as a runtime-registered system. The module carries a load-time constructor that hands each such function to the host through <code>ludic_register_system</code>, and the host's frame loop calls it every <code>Phase</code> after its own handlers — the runtime, escape-hatch counterpart of the compile-time <code>@EngineSystem</code>. The compiler supplies the function's address (Ludic source cannot take one). This is how a closed-source or other-language module contributes systems over the reflection C-ABI; source packages that compile into the consumer should prefer <code>@EngineSystem</code>. Phases are <code>Input</code>, <code>FixedUpdate</code>, <code>Update</code>, <code>LateUpdate</code> and <code>Render</code>.
```ludic
program ManaModule {
function module_init() -> void { world_register_prop("Mana", 2) } # register a component at load
@System(Update) function mana_regen() -> void { # runs every Update in the host
let M = world_prop_id("Mana")
let e = world_query_next(M, 0)
if e >= 0 { world_set(e, M, 0, world_get(e, M, 0) + 1) }
}
}
```

View file

@ -21,7 +21,10 @@
"annot-server",
"annot-predicted",
"annot-toserver",
"annot-toclients"
"annot-toclients",
"annot-system",
"annot-enginesystem",
"annot-namespace"
],
"builtins": [
"fn-print",