Proposal: package/module distribution + package-declarable components & engine-systems (prerequisite for controller libs #57) #62
Labels
No labels
area:ci
area:docs
area:input
area:net
area:rendering
area:repo
area:stdlib
area:tooling
area:types
cleanup
dx
priority:high
priority:low
priority:medium
proposal
status:in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: workshopsoft/ludic#62
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Prerequisite for the builtin-controllers layer (#57). The controllers (#58–#61) are not core language — they should ship as external, versioned packages, not be welded into the compiler or
runtime/native/stdlib. This issue is the mechanism that makes that possible. Planning only — no implementation.Context & decision
The gameplay controllers (platformer / RPG / shooter / NPC AI) are genre content, not language. Baking them into the compiler or the in-repo
runtime/native/*.ludicstdlib would (a) couple game-genre opinions to the compiler's release cadence, (b) bloat every build's surface area, and (c) prevent the community from forking/extending them — which is the entire stated goal. So they ship as packages.The important sub-decision (discussed on #57): a Ludic package is distributed as source (Ludic modules), not as a precompiled OS binary (
.dylib/.so/.a). Because Ludic is AOT (ludicc → LLVM IR → native/wasm), a source package is still fully compiled — into the consumer's binary — so we get distribution and compilation without an ABI seam. Precompiled native libraries were rejected as the default for three reasons:dlopena native dylib in wasm32, and a macOS.dylibwon't serve a Linux.sobuild. Source packages compile to every target for free.property/model/query/spawnare codegen in the consumer's binary; a precompiled lib can only reach the game through the slower dynamic reflection ABI (EV7ludic_register_prop/ludic_get/setby name).Precompiled-binary distribution is kept only as an escape hatch for closed-source or other-language authors shipping a mod over the existing C-ABI + dynamic reflection — second-class by design (native-only, dynamic components, perf cost), fine for mods, wrong as the default for first-party controllers.
What has to change in the compiler
Two hooks that make stdlib namespaces work today are hardcoded, and both must become data-driven / package-declarable so a package can register a namespace, a component schema, and an engine-owned system without editing the compiler:
1. Namespace splice/import trigger (today: string literals in the parser)
The on-demand splice is a per-namespace literal in
p_postfix— e.g.if e.a.s == "Query" { g_uses_query = true }(parse.ludic ~L182) with a matchingdo_import("runtime/native/query.ludic")inmaybe_splice_runtime. A package can't add a namespace without patching these.Ns → modulefrom manifests, sop_postfixlooks the namespace up in a table (populated from imported packages) instead of matching hardcoded names.emit_ns_calldispatch likewise resolvesNs.methodvia the manifest (alias-style, preserving the runtime fn's return type — the existing path).2. Engine-owned system registration (today: a hardcoded component/phase list)
uses_engine_systems()andemit_engine_systems_for_phase(phase)carry a fixed list of(component, esys_fn, phase)(SpriteAnim/Motion/Light2D/…). A package's controller is an engine-owned system, so it needs to add to this list.(well-known-component-name, esys-fn-name, phase)— in the manifest (or via a first-classsystem P phase Update { ... }decl that lowers to the same registration). The compiler builds the splice list + the frame-loop insertion from the union of core + imported-package registrations. Keep the existing "resolve fields by name, no-op when absent" reflection-ABI contract so registration stays layout-independent and additive (unused package ⇒ byte-identical build).disable system <Name>lever from #57: with systems in a registry, disabling one is a registry flag, not a special case.Package/module distribution
Today import is raw
import "path"(include-guarded, path-relative) — enough for one repo, not for shipping. Proposed, kept deliberately small and determinism-first:import pkg "platformer"thenPlatformer { ... }— vs. today's file-path import (which stays for intra-package modules).packages/dir, monorepo-friendly), design the manifest so a URL/registry fetch can slot in later without changing the consumer-facingimport.Ns= resolution error, surfaced at compile time like every other Ludic drift check).ludicc/bin/x; the output is the same self-contained binary/wasm.Escape hatch: binary mods (not packages)
For authors who cannot or will not ship source (closed-source middleware, a controller written in C/Rust), the existing seam already works and needs no new mechanism: link a native object over the EV0–EV7 C-ABI + dynamic reflection (
ludic_register_prop/get/set,ludic_on_<E>). Document it as the explicit, second-class path: native-only, dynamic (by-name) components, no staticquery/spawn, a perf cost — appropriate for post-ship user mods, not first-party genre packages.Determinism / security / tooling notes
check-vocabulary/check-implhonest across packages).docs/language/**machinery, scoped to the package.Phasing
packages/dir,import pkg), version field, namespace-collision errors.References
p_postfixsplice triggers +maybe_splice_runtime(parse.ludic),emit_ns_calldispatch (emit_call.ludic),uses_engine_systems/emit_engine_systems_for_phase(emit_ecs/emit_game), the EV0–EV7 C-ABI + EV7 dynamic reflection (the escape hatch),do_importinclude-guarding.Implemented across this session and shipped to
main. The packaging prerequisite is met — a gameplay-controller library can now ship as a real package: declare components/models, register engine-owned systems andFoo.*namespaces, and be distributed/versioned/locked, all without editing the compiler. #58–#61 are unblocked.Delivered
Distribution / manifest / lockfile (was Phases 2–3) — done in #63:
package.ludicmanifest (name, version, provided namespaces, deps, kind);package.lock.ludiclockfile; MVS resolution; local/vendored resolution (LUDIC_PKG_PROXY,x vendor) with URL/git later; transitive deps; namespace collision = compile-time error; no build-time code execution.Phase 1 — the two hardcoded hooks are now registries (commit
7cbd5d1):@Namespace(Name)on a function opensName.method(…)→ dispatches to the barename_methodthrough the same generic path the core namespaces use (applied 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 (unused registration ⇒ byte-identical).uses_engine_systems/emit_engine_systems_for_phaseare now driven by that registry, with the core three seeded in their historical order.Score+esys_scoreandCoach.bonus(); a consumer imports it and prints4 99with no compiler edit.Binary-mod escape hatch — #62 only asked to document it; #64 built it in full:
--emit-module+ludic_register_system+@System(Phase)+x build-lib+ prebuilt consumption, over the EV0–EV7 C-ABI + dynamic reflection (native-only, second-class, by design).Docs:
docs/PACKAGES.md+ per-annotation pages for@Namespace/@EngineSystem/@System.Deferred (refinements, not blockers — happy to file a follow-up)
import pkg "name"name-based sugar — today a package is consumed by import path via theludic_modules/resolution fallback (#63); the short-name form is convenience over that.min_compilermanifest field + check.Closing as done — the prerequisite this issue exists for is delivered.