From 6c6dfae235b4f6fbd78dc137689ac5cf83c0cbef Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 1 Sep 2026 12:31:47 +0300 Subject: [PATCH] docs(pkg): document @Namespace / @EngineSystem / @System + package hooks (#62) 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 --- changes/package-declarable-hooks.md | 3 ++ docs/PACKAGES.md | 31 +++++++++++++++++ .../annotations/annot-enginesystem.md | 33 +++++++++++++++++++ docs/language/annotations/annot-namespace.md | 24 ++++++++++++++ docs/language/annotations/annot-system.md | 24 ++++++++++++++ tools/docgen/inventory.json | 5 ++- 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 changes/package-declarable-hooks.md create mode 100644 docs/language/annotations/annot-enginesystem.md create mode 100644 docs/language/annotations/annot-namespace.md create mode 100644 docs/language/annotations/annot-system.md diff --git a/changes/package-declarable-hooks.md b/changes/package-declarable-hooks.md new file mode 100644 index 00000000..3c154f86 --- /dev/null +++ b/changes/package-declarable-hooks.md @@ -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. diff --git a/docs/PACKAGES.md b/docs/PACKAGES.md index 48d1e7e4..4bd33dca 100644 --- a/docs/PACKAGES.md +++ b/docs/PACKAGES.md @@ -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 diff --git a/docs/language/annotations/annot-enginesystem.md b/docs/language/annotations/annot-enginesystem.md new file mode 100644 index 00000000..01af8a70 --- /dev/null +++ b/docs/language/annotations/annot-enginesystem.md @@ -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 +--- + +@EngineSystem(Component, Phase) marks a function as an engine-owned system: the compiler adds it to the engine-system registry, and the frame loop calls it every Phase — after the game's own handlers — whenever the named Component is present in the program. It is the package-declarable form of the built-in engine systems (the ones that advance SpriteAnim/Motion/Light2D), 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 Input, FixedUpdate, Update, LateUpdate and Render. + +```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() + } +} +``` diff --git a/docs/language/annotations/annot-namespace.md b/docs/language/annotations/annot-namespace.md new file mode 100644 index 00000000..d05448c2 --- /dev/null +++ b/docs/language/annotations/annot-namespace.md @@ -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 +--- + +@Namespace(Name) registers Name as a namespace a package provides, so a caller can write Name.method(…) and the compiler dispatches it to the bare function name_method(…) (the namespace lowercased, an underscore, then the method). It is the package-declarable form of the built-in Foo.* stdlib namespaces (Regex.*, Grid.*, …): 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 + } +} +``` diff --git a/docs/language/annotations/annot-system.md b/docs/language/annotations/annot-system.md new file mode 100644 index 00000000..fb4fb1c3 --- /dev/null +++ b/docs/language/annotations/annot-system.md @@ -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 +--- + +@System(Phase) marks a function in a prebuilt binary module (one compiled with --emit-module) as a runtime-registered system. The module carries a load-time constructor that hands each such function to the host through ludic_register_system, and the host's frame loop calls it every Phase after its own handlers — the runtime, escape-hatch counterpart of the compile-time @EngineSystem. 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 @EngineSystem. Phases are Input, FixedUpdate, Update, LateUpdate and Render. + +```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) } + } +} +``` diff --git a/tools/docgen/inventory.json b/tools/docgen/inventory.json index 8c961daa..c32e267b 100644 --- a/tools/docgen/inventory.json +++ b/tools/docgen/inventory.json @@ -21,7 +21,10 @@ "annot-server", "annot-predicted", "annot-toserver", - "annot-toclients" + "annot-toclients", + "annot-system", + "annot-enginesystem", + "annot-namespace" ], "builtins": [ "fn-print",