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