feat(compiler): package-declarable engine systems + namespaces (#62)

Registry-izes the two hooks that made stdlib namespaces and engine systems
compiler-hardcoded, so a package registers them with no compiler edit — the
Phase-1 prerequisite for shipping the controller libraries (#58–#61) as real
packages rather than in-repo stdlib.

- Engine systems are a data-driven registry (component, esys-fn, phase). The
  core three (SpriteAnim/Motion — Update, Light2D — Render) are seeded in that
  exact order, so uses_engine_systems / emit_engine_systems_for_phase are now
  registry-driven with byte-identical output (verified: anim_ecs, light_ecs,
  snake IR unchanged; 87/0 golden renders; C-free fixpoint holds). A package
  appends with `@EngineSystem(Component, Phase)` on its esys function.
- Namespaces are a registry too: a package marks a provider with
  `@Namespace(Foo)`, and emit_ns_call aliases an otherwise-unknown Foo.method to
  the bare foo_method (the same generic path the core namespaces use) — after
  every hardcoded core block, so core dispatch is untouched.
- Both annotations are keyword-free (like #64's @System), so no vocabulary /
  grammar churn.

Proven end-to-end (hermetic, source path, runs everywhere): a package registers
Score + esys_score via @EngineSystem and coach_bonus via @Namespace; a consumer
game imports it and prints "4 99" — the engine system ran each Update and
Coach.bonus() dispatched, with no compiler edit for the package. Package suite
18/0.

Core stdlib namespaces stay on their optimized hardcoded blocks by design
(byte-identity + determinism); the generic path is proven to carry a namespace
and packages ride it.

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

View file

@ -23,6 +23,16 @@ function emit_expect_fail(cond1: pointer, msgsym: pointer, got: pointer, want: p
emit(`{lok}:\n`)
}
# lowercase an ASCII identifier (for #62 package-namespace aliasing: Foo -> foo)
function ns_lower(s: pointer) -> pointer {
let n = cstr_len(s)
let b = bytes(n + 1)
var i = 0
while i < n { var c = s[i]; if (c >= 65) and (c <= 90) { c = c + 32 }; b[i] = c; i = i + 1 }
b[n] = 0
return b
}
function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
# Math.* is computed inline (deterministic fixed-point), not routed through a
# bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace.
@ -576,6 +586,10 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (meth == "encode") { bare = "json_encode"; push(labels, "value") }
if (meth == "parse") { bare = "json_parse"; push(labels, "text") }
}
# #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) { perr(`unknown builtin {ns}.{meth}`) }
reorder_named(e, labels)
let id = node(E_ID); id.s = bare; e.a = id

View file

@ -26,9 +26,11 @@ function has_models() -> bool {
# same as before the feature existed. Keep this list in sync with the phase table
# in emit_engine_systems_for_phase.
function uses_engine_systems() -> bool {
if find_comp("SpriteAnim") != null { return true }
if find_comp("Motion") != null { return true }
if find_comp("Light2D") != null { return true }
# #62: any registered engine-system component present (core seeds SpriteAnim /
# Motion / Light2D; packages append via @EngineSystem), or an Occluder (which
# feeds the Light2D pass without an esys of its own).
var i = 0
while i < len(g_esys_comp) { if find_comp(g_esys_comp[i]) != null { return true }; i = i + 1 }
if find_comp("Occluder") != null { return true }
return false
}

View file

@ -60,16 +60,20 @@ function emit_one_engine_system(comp: pointer, fn: pointer) -> void {
}
}
function emit_engine_systems_for_phase(phase: pointer) -> void {
# #62: registry-driven — core entries (seeded in parse_program) keep their
# historical per-phase order, and packages that appended via @EngineSystem run
# after them in the same phase. Each call is component-gated (no-op when the
# component is absent), so an unused registration is byte-identical.
var i = 0
while i < len(g_esys_comp) {
if (g_esys_phase[i] == phase) { emit_one_engine_system(g_esys_comp[i], g_esys_fn[i]) }
i = i + 1
}
if (phase == "Update") {
emit_one_engine_system("SpriteAnim", "esys_spriteanim")
emit_one_engine_system("Motion", "esys_motion")
# Tween.* fluent handles (#48): advanced each Update tick when the game uses
# them (gated on g_uses_tween_rt rather than a declared component).
if g_uses_tween_rt and (find_fn("esys_tween") != null) { emit(" call void @fn_esys_tween()\n") }
}
if (phase == "Render") {
emit_one_engine_system("Light2D", "esys_light2d")
}
}
# Global handlers run first, then the active scene's layer handlers in