From ac8d31ee65ffc6614639374020adb11eaecac0e3 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 11 Sep 2026 17:34:13 +0700 Subject: [PATCH] Managed classes, planned and deliberately separate from structs The struct/class split, written down before anything is built on it. A struct stays a fixed-layout value with C's layout, which is what keeps the FFI, SoA and wasm stories intact; a class is a separate kind with identity, metadata and an implementation-defined representation, for the long-lived gameplay objects that want to change shape while the program is running. The tagline loses "no GC" for "no mandatory GC", because a small collector confined to class instances is now an option rather than a contradiction. CLOS goes from a flat non-goal to a bounded one: the metaclasses, method combination and arbitrary change-class are out; exact-class single dispatch and an explicit frame-boundary migration are in. Migration is eager and explicit rather than CLOS's lazy-on-access, which would put a check on every slot read. Class identity is stable and layouts are numbered, the same shape as the function versions the hot reload section grew. Nothing is frozen and nothing is to be built until struct, Handle and reload semantics are working. --- plan.org | 74 +++++++++++++++++++++++++++++++++++++++++++------- spec-memory.md | 4 ++- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/plan.org b/plan.org index 0e02b50..cb4524e 100644 --- a/plan.org +++ b/plan.org @@ -14,7 +14,7 @@ this plan that contradicts them is out of date. * What Flan is A minimal Lisp for game development. Clojure's brackets and a small slice of its -API, C's memory and value model. No GC. +API, C's memory and value model. No mandatory GC. Not a Common Lisp, not a Clojure. In one line: *Odin with a Lisp frontend and a live REPL.* @@ -43,7 +43,8 @@ live REPL.* * Non-goals - Numeric tower (no bignums, rationals, complex) -- CLOS/MOP, ~format~, pathnames, streams, sequences-over-anything, CL reader +- Full CLOS/MOP (metaclasses, method combination, ~slot-missing~, arbitrary + ~change-class~), ~format~, pathnames, streams, sequences-over-anything, CL reader - Clojure's lazy seqs, JVM interop, ~core.async~ - Persistent collections, and immutable collection types generally (see Data model) - Live-image development at SBCL's level @@ -70,8 +71,10 @@ Structure sharing destroys clear ownership, which is the only thing that forces collector. Replaced by value structs that copy on assignment — see Data model. ** The consequence that drives everything -No GC means *no object headers*. Every struct is exactly its C layout, arrays are -C arrays, so there is no marshalling layer and no wrapper allocation. +Plain ~struct~ means *no object headers*. Every struct is exactly its C layout, +arrays are C arrays, so there is no marshalling layer and no wrapper allocation. +The future managed ~class~ facility below is deliberately a separate kind of +value: it has identity, metadata, and an implementation-defined representation. FFI is still a boundary — ownership, who allocates, string and slice representation, struct padding, callbacks into Flan, error handling, and the @@ -128,6 +131,54 @@ world. - Flat and SoA arrays. - No implicit allocation anywhere in the core. +** Managed classes — planned, deliberately separate from structs +~struct~ remains Flan's default: fixed-layout values suitable for stack storage, +arrays, SIMD/SoA, FFI, and hot paths. It cannot acquire fields while live values +exist. A future ~class~ is for long-lived gameplay/editor objects that need +identity, extensibility, and live schema changes: + +#+begin_src lisp +(defclass Enemy + (x float) (y float) (health int)) + +(defmethod update ((e Enemy) dt) ...) +#+end_src + +Classes require a managed allocation strategy and runtime class/shape metadata, +but *not necessarily a tracing GC*. The initial likely choices are a world or +session arena, pool allocation behind generational ~(Handle T)~ values, or an +explicitly owned region. A small tracing GC confined to class instances remains +an option if cyclic graphs prove burdensome; it never changes ~struct~ layout or +the C ABI. + +Generic functions attach operations independently of class definitions. This +addresses the expression problem for dynamic game objects: a later module can +add either ~(defmethod draw ((e Enemy)) ...)~ or a new ~Capsule~ class plus its +~draw~ method without editing the original class or a central closed ~match~. +Start with exact-class, single-argument dispatch. Multi-argument dispatch is +the later extension for interactions such as ~(collide Player Enemy)~. Method +specificity/ambiguity rules are required before inheritance or multiple dispatch +is enabled. + +Class redefinition has a stable class identity and numbered layouts. Changing +slots leaves existing instances at their old layout until an explicit migration +at a reload/frame boundary. Added fields use declared defaults; removed fields +are discarded; renamed or type-changed fields require user migration code. This +is the small, eager and debuggable analogue of CLOS +~make-instances-obsolete~ / ~update-instance-for-redefined-class~, rather than +surprising lazy mutation on field access: + +#+begin_src lisp +(redefine-class Enemy ...) +(defmethod migrate ((old Enemy@1) (new Enemy@2)) + (set new.frozen false)) +(migrate-instances Enemy) +#+end_src + +The precise class syntax, inheritance, storage strategy, and migration API are +not frozen. Do not add classes until ordinary ~struct~, ~Handle~, and reload +semantics are working. + Immutability also serves the optimiser: a value known never to be mutated can be copied into registers and stack-allocated freely. Mutability is what forces heap identity. @@ -176,8 +227,10 @@ static typing the tag is paid only where it is asked for, in ~any~. - Tagged unions, ~distinct~ types, bit sets. ** Consequences -- Multimethods are *compile-time overload resolution*, not runtime dispatch. - Genuine runtime dispatch is on an explicit enum or tagged union. +- For ~struct~s, multimethods are *compile-time overload resolution*, not runtime + dispatch. Genuine runtime dispatch is on an explicit enum or tagged union. + Planned managed classes instead use generic-function dispatch as described in + "Managed classes"; this is an intentionally separate facility. - Conditions still work: a condition is a struct, the signal channel is a tagged union, ~handler-case~ type matching resolves at compile time. - The REPL works — the compiler knows each site's type and emits the right printer, @@ -740,10 +793,11 @@ marked. 6. Hot-reload compatibility rules. /Milestone 7./ A changed function signature creates a new version: new callers resolve it, old callers keep the old one, and the session warns at tracked stale caller sites. Still open: a changed - struct layout with live instances, a function pointer already handed to C, a - captured environment, and redefining a ~defvar~. Each needs an answer of the - form "rejected", "accepted with a migration", or "accepted and the old code - keeps running". + *struct* layout with live values is rejected; a managed class layout has an + explicit frame-boundary migration path, as described above. Still open: a + function pointer already handed to C, a captured environment, and redefining + a ~defvar~. Each needs an answer of the form "rejected", "accepted with a + migration", or "accepted and the old code keeps running". 7. Does the interpreter survive milestone 3, or is the compiled path the only backend? /Milestone 3, on measured numbers./ See Compilation. 8. ~(Option a)~ /settled:/ an ordinary stdlib union with ~Some~/~None~; the diff --git a/spec-memory.md b/spec-memory.md index b672727..8e28940 100644 --- a/spec-memory.md +++ b/spec-memory.md @@ -2,7 +2,9 @@ Status: **frozen**. Closes plan.org open decisions #6 and #10, and resolves the contradiction between "value structs copy on assignment" and owning containers. -Everything else in the design references this vocabulary. +Everything else in the design references this vocabulary. It governs plain +fixed-layout `struct` values, not the separately planned managed `class` +facility (see plan.org, "Managed classes"). ## The four container types