flan/test/programs/dev-classes.flan
Joseph Ferano 7f92136401 Old instances of a redefined class now follow the class
CLHS 4.3.6's update protocol, minus the user hook, on the dyn side's
defclass. Redefining a class used to be silent: a class is sugar for a
constructor defn, so the edit replaced a body and the instances already in
the program kept their old keys for ever.

Three pieces. A registry in flan_dyn.c holding each class's current slot
list and a generation, made only of interned kw_entry pointers so the
collector has nothing to trace in it and no root to push for it. A uint32
generation on the instance, fitted into the padding kind and mark leave in
front of len's alignment — sizeof(flan_obj) is 48 with it and was 48
without, and flan_dyn_obj_size is there so a later field that moves it
fails a test. And a registration thunk per reload, run by the agent
through flan_reload_call after the module's bodies are published: it has
to be a thunk, because the case this exists for is a class redefined and
not constructed.

Migration is lazy, at want_map, len's map arm and dyn_equal's. Slots kept
by name, gained slots nil, dropped slots gone, identity preserved, entries
rebuilt in the class's order so a migrated instance is indistinguishable
from a fresh one. Equality migrates both operands first, so it is over the
class as it is now.

The session had to stop refusing the constructor's signature change, and
does so only for a defclass and only when no compiled caller is left
behind. The checker gets there first in practice; the walk in eval holds
the reason locally rather than inheriting it.

The registry is advisory: a class instance is an open map, so a key a raw
put wrote that the class never declared is dropped by the next migration.
FIX.org says that plainly rather than pretending enforcement.
2026-09-20 19:46:57 +07:00

47 lines
2.1 KiB
Plaintext

;;;; A class, instances of it, and a session that changes the class.
;;;;
;;;; dev-class.flan already asks whether a *method* added to a running
;;;; program reaches a call site compiled before it existed. This asks the
;;;; harder half: whether the instances already in the program survive their
;;;; class being redefined. They are ordinary dyn maps held in a dyn global,
;;;; so the reload cannot touch them — a redefinition makes a program's
;;;; globals external and never re-initialises them, which is the whole of
;;;; "edit the code, keep the sand" — and what the editor sends is a new
;;;; constructor plus a registration of the class's new slot list. The
;;;; migration happens lazily, in the runtime, at the first touch after that.
;;;;
;;;; [instances] is a global rather than a local for exactly that reason: an
;;;; expression the editor evaluates is a thunk with a frame of its own, so
;;;; anything it is supposed to still be holding an hour later has to live
;;;; somewhere the thunk is not.
;;;;
;;;; The instances are pushed by the editor rather than by [main], and that
;;;; is not incidental. A (defclass ...) whose slots change is a constructor
;;;; whose signature changes, and the session refuses that wherever a
;;;; compiled caller of the constructor is left standing — a call site that
;;;; passes two dyn words into a three-parameter body leaves the third
;;;; holding a register, and a dyn word that is not a value is a wild
;;;; pointer. A [main] calling [(point 3 4)] would be exactly such a caller,
;;;; so this program has none and the instances arrive from thunks, which
;;;; leave nothing behind. test_session.ml pins the refusal itself.
;;;;
;;;; It keeps running rather than returning, for dev-class.flan's reason: an
;;;; expression typed at the editor is a thunk the agent runs at a frame
;;;; boundary, and a parked program has none.
(import agent "vendor:agent")
(defclass point [x y])
(defgeneric area [self] dyn)
(defmethod area point [p] (* (get p :x) (get p :y)))
(defvar instances dyn)
(defn main [] i32
(agent/start "/tmp/flan-dev-classes-fallback.sock")
(set instances (vec-new dyn))
(dotimes [i 4000]
(agent/wait 5))
0)