From 59e9392ec8857cb6147e5bc2697778ac4eb30684 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 11 Sep 2026 17:35:06 +0700 Subject: [PATCH] What the class decision changes for the next session plan.org's managed classes arrived after NEXT.md's handoff was written, so a session reading the plan cold would take them as the next task. They are not: plan.org's own last line on them says nothing until struct, Handle and reload semantics work, and that belongs where the next task is named. Three findings from reviewing it that are not in plan.org. A generic function is a cell whose body is a dispatch table - adding a method later is the same problem the indirection cells already solve, so the expensive half of classes is built. Migration has to enumerate live instances, which makes the pool behind a generational Handle the only one of the three storage options that obviously supports it, rather than a free choice. And a numbered layout has to stay resolvable for migrate to dispatch on, which is the same retention rule as nothing is ever dlclosed. Also records the open question the class facility raises for conditions: whether a condition may be a class, what the hierarchy would buy, and the three costs - allocation on the signal path being the serious one. It wants answering before handler-case, since it decides whether handler matching has one path or two. Plus three nits in the new prose: float/int are not Flan type names, the place syntax was dotted, and the migration example set a slot the class did not have. The tag-word sentence said any was the only place one is paid, which Error and now a class instance both make false. --- NEXT.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ plan.org | 10 ++++++---- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/NEXT.md b/NEXT.md index 17c62bb..b670212 100644 --- a/NEXT.md +++ b/NEXT.md @@ -37,6 +37,60 @@ and come back as *unknown name*, which is the house rule's own class of bug. Read SBCL for what restarts should *mean* and ignore how it moves control: it transfers with `block`/`return-from`, which §6 rules out. +### Managed classes are planned. Do not start them. + +plan.org grew a `class` facility beside `struct`: identity, runtime shape +metadata, an implementation-defined representation, generic-function dispatch, +and live schema change with an explicit migration at a frame boundary. Its own +last line is the rule — nothing until ordinary `struct`, `Handle` and reload +semantics are working. It is here so that a session reading plan.org cold does +not take it as the next task. Three things found while reviewing it, none of +them in plan.org yet: + +- **A generic function is a cell.** "A later module can add `(defmethod draw + ((e Enemy)) ...)` without editing the original" means every compiled call site + of `draw` has to find the new method — which is the problem the indirection + cells already solve. A generic function is a cell whose body is a dispatch + table and a reload extends the table. The expensive half of classes is + therefore already built and tested. +- **The pool is not one storage option among three.** `migrate-instances` has to + *enumerate* live instances. A pool behind generational `(Handle T)` gives that + by construction; a world arena and an owned region do not obviously. plan.org + presents the three as a free choice and they are not. +- **`Enemy@1` has to stay resolvable** for `migrate` to dispatch on it, so the + session retains every layout version's metadata for as long as any instance + holds it. Same rule as "nothing is ever `dlclose`d", and worth stating as one. + +### Open: can a condition be a class? + +Unanswered, and it wants answering before `handler-case`, because it decides +whether handler matching has one path or two. + +It would buy the thing conditions most lack: a **hierarchy**. §1 says flatly +there is none, which is why nothing can say "any condition" — no catch-all +handler and nothing for a break loop to match on. Class inheritance gives it. + +Three costs, one serious: + +- **Signalling would allocate.** A struct condition is a stack value and + `signal` takes its address; a class instance needs a pool slot at the signal + site. That is the failure path, sometimes the hot path, and sometimes the + thing that failed is allocation itself. plan.org also says no implicit + allocation anywhere in the core. +- **§5's lifetime inverts.** Today the condition dies with the signalling frame + and a handler that keeps it copies, which is free for a value struct. A class + instance survives the transfer — nicer, but now something owns and frees it. +- **Layout versions meet handler frames.** A struct condition cannot change + layout; it is refused. A class can, and then a frame pushed against + `MyError@1` is on the stack while the signaller builds `MyError@2`. + +The shape that probably wins is both: a struct condition stays exactly what it +is — no allocation, matched by name hash, dies with the frame — and a class +condition is allocated, survives, and matches by walking its class chain. +That is two matching paths, which is the same bill the struct/class split +already signs, so it is consistent rather than a new cost. Either way it is an +amendment to a **frozen** `spec-conditions.md`, not a gap in it. + **The dev loop is closed.** `C-c C-c` in Emacs recompiles the top-level form at point and installs it in a running program, at that program's next frame diff --git a/plan.org b/plan.org index cb4524e..e635d9c 100644 --- a/plan.org +++ b/plan.org @@ -139,7 +139,7 @@ identity, extensibility, and live schema changes: #+begin_src lisp (defclass Enemy - (x float) (y float) (health int)) + (x f32) (y f32) (health i32)) (defmethod update ((e Enemy) dt) ...) #+end_src @@ -169,9 +169,10 @@ is the small, eager and debuggable analogue of CLOS surprising lazy mutation on field access: #+begin_src lisp -(redefine-class Enemy ...) +(redefine-class Enemy + (x f32) (y f32) (health i32) (shield i32)) (defmethod migrate ((old Enemy@1) (new Enemy@2)) - (set new.frozen false)) + (set (.shield new) (.health old))) ; a default would not have been right (migrate-instances Enemy) #+end_src @@ -188,7 +189,8 @@ identity. collector, as Forth-lineage and refcounted dynamic languages show. It is chosen, and the reason is that dynamic typing would require paying a tag word on every value, which is exactly the header cost dropping the GC was meant to avoid. Under -static typing the tag is paid only where it is asked for, in ~any~. +static typing the tag is paid only where it is asked for: in ~any~, in ~Error~, +and on a managed ~class~ instance. An ordinary ~struct~ never carries one. - Types are mandatory; *inference* makes them feel optional. Annotate function signatures, infer locals — Odin/Zig/Rust ergonomics.