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.