A class instance refuses a key its class does not declare, read or written, through get, put, .field and [:key].

This commit is contained in:
Joseph Ferano 2026-09-26 06:02:51 +07:00
parent 1022b7c7f9
commit 4eefd25df9
14 changed files with 117 additions and 102 deletions

View File

@ -587,10 +587,11 @@ method to a running program is an ordinary redefinition.
** CANCELLED Class features deferred, each with its reason ** CANCELLED Class features deferred, each with its reason
CLOSED: [2026-09-20] CLOSED: [2026-09-20]
Inheritance, multi-argument dispatch, =:before=/=:after=/=:around= and Inheritance, multi-argument dispatch, =:before=/=:after=/=:around= and
=call-next-method=, named-slot construction, unknown-slot checking, computed =call-next-method=, named-slot construction, compile-time unknown-slot checking,
dispatch values. With single dispatch on literal values there is no specificity computed dispatch values. With single dispatch on literal values there is no
question, and inheritance or multiple dispatch would create one. Unknown-slot specificity question, and inheritance or multiple dispatch would create one.
checking needs class-typed tracking the dyn side deliberately does not have. Compile-time unknown-slot checking needs class-typed tracking the dyn side
deliberately does not have; the runtime refuses an unknown slot instead.
** DONE update-instance-for-redefined-class, the user hook ** DONE update-instance-for-redefined-class, the user hook
CLOSED: [2026-09-25] CLOSED: [2026-09-25]
@ -732,8 +733,8 @@ of !=.
** DONE A dyn value takes .field and [:key] ** DONE A dyn value takes .field and [:key]
CLOSED: [2026-09-26] CLOSED: [2026-09-26]
Assigning ~x.name~ or ~m[:k]~ is ~put~, not the stricter ~(set (get x :k) v)~: it adds Assigning ~x.name~ or ~m[:k]~ is ~put~: a plain map gains the key, and a class
a key a plain map or a class lacks. ~m[k]~ on a dyn map takes any key, as ~get~ does. instance refuses one its class does not declare, on read too, as ~get~ and ~put~ now do.
** DONE A slice from a C pointer, and a pointer cast ** DONE A slice from a C pointer, and a pointer cast
CLOSED: [2026-09-25] CLOSED: [2026-09-25]
@ -1402,9 +1403,8 @@ CLOSED: [2026-09-20]
CLHS 4.3.6. Nothing is enumerated and no heap is walked — the CLHS 4.3.6. Nothing is enumerated and no heap is walked — the
redefinition is constant time and each instance pays once, at its next touch. redefinition is constant time and each instance pays once, at its next touch.
Neither printer migrates, so a stale instance shows its old slots to the editor Neither printer migrates, so a stale instance shows its old slots to the editor
until something touches it. The registry is advisory: a key the class never until something touches it. An instance holds only declared slots — get, put and
declared is dropped by the next migration, which is data loss with no enforcement set refuse any other key — so the migration's drop loses nothing a program wrote.
behind it.
** WAIT A class registry keeps one slot list per class, not one per layout version ** WAIT A class registry keeps one slot list per class, not one per layout version
Decided 2026-09-25: waits for a case name-matching migration to the current list gets wrong. Decided 2026-09-25: waits for a case name-matching migration to the current list gets wrong.

View File

@ -326,10 +326,9 @@ The CLOS answer would need, concretely:
Flan spelling of that hook is a generic function, e.g. Flan spelling of that hook is a generic function, e.g.
`(defmethod update-for-redefined point [p added discarded] ...)`, which fits `(defmethod update-for-redefined point [p added discarded] ...)`, which fits
the dispatch mechanism that already exists. the dispatch mechanism that already exists.
- A decision on whether `put` of an unknown slot stays legal. Today it is — a - A decision on whether `put` of an unknown slot stays legal. Decided
class instance is an open map, and TODO.org, "Class features deferred, each with 2026-09-26: it does not; `get`, `put` and `set` refuse an unknown slot on an
its reason", already defers refusing an unknown slot at `(get p :z)`. If unknown slots stay legal, the registry's slot list is instance at run time, so the registry's slot list is enforced.
advisory and the whole update protocol is advisory with it.
This is a real, SBCL/CLOS-precedented design that Flan's runtime can actually This is a real, SBCL/CLOS-precedented design that Flan's runtime can actually
support. It is also a feature with no user yet, since redefinition on the dyn support. It is also a feature with no user yet, since redefinition on the dyn

View File

@ -185,9 +185,9 @@ let collect (decls : Ast.decl list) =
is what [class-of] answers and what a generic dispatches on. is what [class-of] answers and what a generic dispatches on.
Named-slot construction — the dyn twin of [(Cursor {.src s})], with an Named-slot construction — the dyn twin of [(Cursor {.src s})], with an
omitted slot meaning nil — is deferred, and so is refusing an unknown slot omitted slot meaning nil — is deferred (TODO.org, "Class features deferred,
at [(get p :z)]. Both are recorded in TODO.org, "Class features deferred, each with its reason"). An unknown slot, [(get p :z)], is refused at run
each with its reason". *) time by the runtime's [trap_no_slot]. *)
let constructor n (slots : Ast.field list) loc : Ast.decl = let constructor n (slots : Ast.field list) loc : Ast.decl =
(* Two slots of one name would write one entry and read one value, and the (* Two slots of one name would write one entry and read one value, and the
constructor would take two arguments for it. The duplicate parameter constructor would take two arguments for it. The duplicate parameter

View File

@ -1610,15 +1610,10 @@ flan_dyn flan_dyn_map_new(void) {
* *
* **What the registry constrains.** A store into a slot the class declares * **What the registry constrains.** A store into a slot the class declares
* — the constructor's, [put]'s, [set]'s — is checked against the slot's * — the constructor's, [put]'s, [set]'s — is checked against the slot's
* type. A key the class does not declare is not refused by [put]: a class * type. A key the class does not declare is refused by [get], [put] and
* instance is an open map — TODO.org, "Class features deferred, each with its * [set] alike ([trap_no_slot]), so an instance's keys are its class's slots
* reason", defers unknown-slot checking — so a key nobody declared can be * and the migration below, which keeps only those, drops nothing a program
* written to one, and the migration below will *drop* it at the next * wrote.
* redefinition, because its rule is that an instance's keys are the class's
* slots. That is real data loss and it is written down as such in TODO.org,
* "A redefined defclass migrates its instances lazily", rather than dressed
* up as enforcement. [set] does refuse an undeclared key, because a slot it
* writes has to exist.
* *
* **Where a migration happens.** [want_map], so every [get], [put] and * **Where a migration happens.** [want_map], so every [get], [put] and
* [has-key?]; [flan_dyn_len]'s map arm; and [dyn_equal]'s, so two instances * [has-key?]; [flan_dyn_len]'s map arm; and [dyn_equal]'s, so two instances
@ -3206,10 +3201,20 @@ flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k) {
} }
/* A program's (get m k) and (.k m): the same, with the site a value that is /* A program's (get m k) and (.k m): the same, with the site a value that is
* not a map is refused at. */ * not a map is refused at, and a key an instance's class does not declare
* refused rather than answered nil — [trap_no_slot]. */
static class_entry *class_sync(flan_obj *o);
static int64_t class_slot(class_entry *e, flan_dyn k);
static _Noreturn void trap_no_slot(const uint8_t *loc, int64_t loclen,
const char *op, flan_obj *o,
class_entry *e, flan_dyn k);
flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc, flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc,
int64_t loclen) { int64_t loclen) {
class_entry *e;
if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "get", "only a map answers it", m, k); if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "get", "only a map answers it", m, k);
e = class_sync(dyn_obj(m));
if (e != NULL && class_slot(e, k) < 0)
trap_no_slot(loc, loclen, "get", dyn_obj(m), e, k);
return flan_dyn_map_get(m, k); return flan_dyn_map_get(m, k);
} }
@ -3294,6 +3299,29 @@ static flan_dyn check_slot(const uint8_t *loc, int64_t loclen, int by,
static inline void map_store(flan_obj *o, flan_dyn k, flan_dyn v); static inline void map_store(flan_obj *o, flan_dyn k, flan_dyn v);
/* A key an instance's class does not declare, read or written. An instance
* has exactly its class's slots — a typo in a slot name is an error at the
* access and not a new key — so get, put and set all refuse one; a plain map
* takes any key. */
static _Noreturn void trap_no_slot(const uint8_t *loc, int64_t loclen,
const char *op, flan_obj *o,
class_entry *e, flan_dyn k) {
char sk[SAY_MAX];
kw_entry *c = o->u.v.klass;
int64_t i;
say(sk, SAY_MAX, k);
said_len = 0;
said_add("dyn %s: %.*s has no slot %s. Its slots are", op, (int)c->len,
(const char *)(c + 1), sk);
if (e == NULL || e->nslots == 0) said_add(" none");
else
for (i = 0; i < e->nslots; i++)
said_add(" :%.*s", (int)e->slots[i]->len,
(const char *)(e->slots[i] + 1));
flan_say(loc, loclen, "%s", said_buf);
flan_trap((const uint8_t *)"DynType", 7);
}
/* A constructor's stores: [flan_dyn_map_set]'s, with the refusal worded for /* A constructor's stores: [flan_dyn_map_set]'s, with the refusal worded for
* the constructor call it happened inside rather than for a [put] nobody * the constructor call it happened inside rather than for a [put] nobody
* wrote, and placed at the slot's declaration. */ * wrote, and placed at the slot's declaration. */
@ -3308,8 +3336,7 @@ void flan_dyn_slot_init(flan_dyn m, flan_dyn k, flan_dyn v,
* they are three different mistakes: the value is not a class instance at * they are three different mistakes: the value is not a class instance at
* all (a map's entries are written with [put], which is where inserting a * all (a map's entries are written with [put], which is where inserting a
* key is real); the key is not a slot the class declares; the value does not * key is real); the key is not a slot the class declares; the value does not
* fit the slot's type. The first two are why this is not [put]: a declared * fit the slot's type. The first is why this is not [put]. */
* slot always exists, so writing one is a store and never an insertion. */
void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v, void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v,
const uint8_t *loc, int64_t loclen) { const uint8_t *loc, int64_t loclen) {
flan_obj *o; flan_obj *o;
@ -3329,43 +3356,37 @@ void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v,
o = dyn_obj(m); o = dyn_obj(m);
e = class_sync(o); e = class_sync(o);
j = class_slot(e, k); j = class_slot(e, k);
if (j < 0) { if (j < 0) trap_no_slot(loc, loclen, "set", o, e, k);
char sk[SAY_MAX];
kw_entry *c = o->u.v.klass;
int64_t i;
say(sk, SAY_MAX, k);
said_len = 0;
said_add("dyn set: %.*s has no slot %s. Its slots are",
(int)c->len, (const char *)(c + 1), sk);
if (e == NULL || e->nslots == 0) said_add(" none");
else
for (i = 0; i < e->nslots; i++)
said_add(" :%.*s", (int)e->slots[i]->len,
(const char *)(e->slots[i] + 1));
said_add("; a key the class does not declare is added with put, not set");
flan_say(loc, loclen, "%s", said_buf);
flan_trap((const uint8_t *)"DynType", 7);
}
if (!slot_admit(&e->types[j], v, &out)) if (!slot_admit(&e->types[j], v, &out))
trap_slot_type(loc, loclen, BY_SET, o, e, j, m, v); trap_slot_type(loc, loclen, BY_SET, o, e, j, m, v);
map_store(o, k, out); map_store(o, k, out);
} }
void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc, static void map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc,
int64_t loclen) { int64_t loclen, int any_key) {
flan_obj *o; flan_obj *o;
class_entry *e; class_entry *e;
if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "put", "only a map answers it", m, k); if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "put", "only a map answers it", m, k);
o = dyn_obj(m); o = dyn_obj(m);
e = class_sync(o); e = class_sync(o);
if (!any_key && e != NULL && class_slot(e, k) < 0)
trap_no_slot(loc, loclen, "put", o, e, k);
/* A map with no class, and a class with no typed slot, stop at the test. */ /* A map with no class, and a class with no typed slot, stop at the test. */
if (e != NULL && e->typed) v = check_slot(loc, loclen, BY_PUT, o, e, m, k, v); if (e != NULL && e->typed) v = check_slot(loc, loclen, BY_PUT, o, e, m, k, v);
map_store(o, k, v); map_store(o, k, v);
} }
/* The same with no site: a map literal's stores, and test/dyn_ops.c. */ /* A program's put, and the store under a dyn's [.k] and [:k]. */
void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc,
int64_t loclen) {
map_put(m, k, v, loc, loclen, 0);
}
/* With no site and any key: an untagged map literal's stores, and
* test/dyn_ops.c, which builds instances' odd states by hand. No program
* reaches an instance through it. */
void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v) { void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v) {
flan_dyn_map_put(m, k, v, NULL, 0); map_put(m, k, v, NULL, 0, 1);
} }
/* The store under all three, with the instance already brought up to date. */ /* The store under all three, with the instance already brought up to date. */

View File

@ -156,10 +156,8 @@ flan_dyn flan_dyn_type_of(flan_dyn v);
* declares are dropped. The instance's identity is preserved throughout; * declares are dropped. The instance's identity is preserved throughout;
* this is CLHS 4.3.6, and [flan_dyn_class_hook] is its user hook. * this is CLHS 4.3.6, and [flan_dyn_class_hook] is its user hook.
* *
* The drop is unconditional, which is the honest cost of a class instance * No key the class never declared can be there to drop: [get], [put] and
* being an open map: a key written by a raw [put] that the class never * [set] refuse one on an instance. */
* declared is dropped by the next migration too. The registry describes the
* class's intention and does not enforce it. */
void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n); void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n);
/* The body update-instance-for-redefined-class dispatches through, as a /* The body update-instance-for-redefined-class dispatches through, as a

View File

@ -28,12 +28,9 @@
(println (get s :step)) (println (get s :step))
(set (get s :tag) [1 2]) (set (get s :tag) [1 2])
(println (get s :tag)) (println (get s :tag))
;; put reaches the same check for a declared slot, and still inserts a ;; put reaches the same check for a declared slot.
;; key the class does not declare -- an instance is an open map to put.
(put s :speed 2.5) (put s :speed 2.5)
(put s :scratch 9)
(println (get s :speed)) (println (get s :speed))
(println (get s :scratch))
(println (length s)) (println (length s))
;; A typed caller boxes into the dyn parameter as any call does. ;; A typed caller boxes into the dyn parameter as any call does.
(set (get s :step) (twelve)) (set (get s :step) (twelve))

View File

@ -64,7 +64,9 @@
(println (get p :x)) (println (get p :x))
(println (has-key? p :x)) (println (has-key? p :x))
(println (has-key? p :nothing)) (println (has-key? p :nothing))
(println (get p :nothing)) ;; A key its class does not declare is refused on an instance; a plain
;; map answers nil for one it lacks.
(println (get {:x 1} :nothing))
;; The shape tag, as a value. Every value can be asked; only an instance ;; The shape tag, as a value. Every value can be asked; only an instance
;; answers with a name. ;; answers with a name.

View File

@ -4,7 +4,7 @@
(defn as-dyn [d dyn] dyn d) (defn as-dyn [d dyn] dyn d)
(defn main [args [string]] i32 (defn main [args [str]] i32
(let [which (if (> (length args) 1) (i32 (bytes->i64 (bytes-view (at args 1)))) 0) (let [which (if (> (length args) 1) (i32 (bytes->i64 (bytes-view (at args 1)))) 0)
s (State false false) s (State false false)
n (as-dyn 3)] n (as-dyn 3)]
@ -14,5 +14,7 @@
(= which 1) (set (.paused s) 1) (= which 1) (set (.paused s) 1)
(= which 2) (set (.paused n) true) (= which 2) (set (.paused n) true)
(= which 3) (set (at s :step) 2) (= which 3) (set (at s :step) 2)
(= which 5) (set (.pasued s) true)
(= which 6) (println (.pasued s))
:else (println (at n :paused)))) :else (println (at n :paused))))
0) 0)

View File

@ -4,7 +4,7 @@ defclass(State, [paused bool step bool])
fn as-dyn(d) -> dyn = d fn as-dyn(d) -> dyn = d
fn main(args: [string]) -> i32 fn main(args: [str]) -> i32
let which = let which =
if length(args) > 1 then i32(bytes->i64(bytes-view(args[1]))) else 0 if length(args) > 1 then i32(bytes->i64(bytes-view(args[1]))) else 0
let s = State(false, false) let s = State(false, false)
@ -18,6 +18,10 @@ fn main(args: [string]) -> i32
n.paused = true n.paused = true
elif which == 3 elif which == 3
s[:step] = 2 s[:step] = 2
elif which == 5
s.pasued = true
elif which == 6
println(s.pasued)
else else
println(n[:paused]) println(n[:paused])
0 0

View File

@ -25,7 +25,7 @@
(set (at (.items b) 0) 11) (set (at (.items b) 0) 11)
(update (at (.items b) 1) + 5) (update (at (.items b) 1) + 5)
(println (.count b) (.x (.pos b)) (.y (.pos b)) (at (.items b) 0) (at (.items b) 1)) (println (.count b) (.x (.pos b)) (.y (.pos b)) (at (.items b) 0) (at (.items b) 1))
(println (.missing b) (= (.missing b) (get b :missing)))) (println (.missing {:a 1}) (= (.missing {:a 1}) (get {:a 1} :missing))))
(let [m {:hp 3}] (let [m {:hp 3}]
(set (.hp m) (- (.hp m) 1)) (set (.hp m) (- (.hp m) 1))
(set (at m :mp) 9) (set (at m :mp) 9)

View File

@ -23,7 +23,8 @@ fn main() -> i32
b.items[0] = 11 b.items[0] = 11
b.items[1] += 5 b.items[1] += 5
println(b.count, b.pos.x, b.pos.y, b.items[0], b.items[1]) println(b.count, b.pos.x, b.pos.y, b.items[0], b.items[1])
println(b.missing) let plain = {:a 1}
println(plain.missing)
let m = {:hp 3} let m = {:hp 3}
m.hp -= 1 m.hp -= 1
m[:mp] = 9 m[:mp] = 9

View File

@ -5652,13 +5652,12 @@ level "1"
(* Typed class slots and set on a slot: the stores that fit, then one (* Typed class slots and set on a slot: the stores that fit, then one
run per refusal. The constructor, put and set each check a declared run per refusal. The constructor, put and set each check a declared
slot's type, set refuses a slot the class does not declare and a slot's type, and set refuses a slot the class does not declare and a
value that is not an instance, and put still inserts an undeclared value that is not an instance. On both backends, because every one of these is a runtime call
key. On both backends, because every one of these is a runtime call
whose arguments the two emit separately. *) whose arguments the two emit separately. *)
let slots_out = let slots_out =
"#state{:pause false :step 3 :speed 1.5 :name \"sand\" :tag :x}\n\ "#state{:pause false :step 3 :speed 1.5 :name \"sand\" :tag :x}\n\
true\n-7\n[1 2]\n2.5\n9\n6\n12\n3.5\ntrue\n2\n:state\n" true\n-7\n[1 2]\n2.5\n5\n12\n3.5\ntrue\n2\n:state\n"
in in
outputs "dyn: typed class slots" "programs/dyn-class-slots.flan" slots_out; outputs "dyn: typed class slots" "programs/dyn-class-slots.flan" slots_out;
outputs ~x86:true "dyn: typed class slots, --x86" outputs ~x86:true "dyn: typed class slots, --x86"
@ -5688,8 +5687,7 @@ level "1"
is declared i32, and 5000000000 is not a value it holds \ is declared i32, and 5000000000 is not a value it holds \
exactly"); exactly");
("3", "dyn-slot-trap.flan:17:19: dyn set: state has no slot :paws. \ ("3", "dyn-slot-trap.flan:17:19: dyn set: state has no slot :paws. \
Its slots are :pause :step :tag; a key the class does not \ Its slots are :pause :step :tag");
declare is added with put, not set");
("4", "dyn-slot-trap.flan:18:19: dyn set: (get m k) is a place only \ ("4", "dyn-slot-trap.flan:18:19: dyn set: (get m k) is a place only \
on a class instance, and this is a map with no class"); on a class instance, and this is a map with no class");
("5", "dyn-slot-trap.flan:19:28: dyn construct: the slot :owner of \ ("5", "dyn-slot-trap.flan:19:28: dyn construct: the slot :owner of \
@ -5733,7 +5731,9 @@ level "1"
rows; rows;
(try Sys.remove exe with Sys_error _ -> ()) (try Sys.remove exe with Sys_error _ -> ())
in in
let field_rows (l0, l1, l2, l3, l4) = (* 5 and 6 are a misspelt slot on a class instance, written and read:
refused naming the class and its slots, never a new key or a nil. *)
let field_rows (l0, l1, l2, l3, l4, l5, l6) =
[ ("0", l0 ^ ": dyn get: int and keyword, and only a map answers it — \ [ ("0", l0 ^ ": dyn get: int and keyword, and only a map answers it — \
(get 3 :paused)"); (get 3 :paused)");
("1", l1 ^ ": dyn put: the slot :paused of State is declared bool, \ ("1", l1 ^ ": dyn put: the slot :paused of State is declared bool, \
@ -5744,19 +5744,27 @@ level "1"
("3", l3 ^ ": dyn put: the slot :step of State is declared bool, and \ ("3", l3 ^ ": dyn put: the slot :step of State is declared bool, and \
this is int"); this is int");
("4", l4 ^ ": dyn at: int and keyword, and only a text, a vec or a \ ("4", l4 ^ ": dyn at: int and keyword, and only a text, a vec or a \
map is indexed — (at 3 :paused)") ] map is indexed — (at 3 :paused)");
("5", l5 ^ ": dyn put: State has no slot :pasued. Its slots are \
:paused :step");
("6", l6 ^ ": dyn get: State has no slot :pasued. Its slots are \
:paused :step") ]
in in
let flan_rows = let flan_rows =
field_rows ("dyn-field-trap.flan:13:28", "dyn-field-trap.flan:14:19", field_rows ("dyn-field-trap.flan:13:28", "dyn-field-trap.flan:14:19",
"dyn-field-trap.flan:15:19", "dyn-field-trap.flan:16:19", "dyn-field-trap.flan:15:19", "dyn-field-trap.flan:16:19",
"dyn-field-trap.flan:17:22") "dyn-field-trap.flan:19:22", "dyn-field-trap.flan:17:19",
"dyn-field-trap.flan:18:28")
and fln_rows =
field_rows ("dyn-field-trap.fln:14:13", "dyn-field-trap.fln:16:5",
"dyn-field-trap.fln:18:5", "dyn-field-trap.fln:20:5",
"dyn-field-trap.fln:26:13", "dyn-field-trap.fln:22:5",
"dyn-field-trap.fln:24:13")
in in
field_trap "programs/dyn-field-trap.flan" flan_rows; field_trap "programs/dyn-field-trap.flan" flan_rows;
field_trap ~x86:true "programs/dyn-field-trap.flan" flan_rows; field_trap ~x86:true "programs/dyn-field-trap.flan" flan_rows;
field_trap "programs/dyn-field-trap.fln" field_trap "programs/dyn-field-trap.fln" fln_rows;
(field_rows ("dyn-field-trap.fln:14:13", "dyn-field-trap.fln:16:5", field_trap ~x86:true "programs/dyn-field-trap.fln" fln_rows;
"dyn-field-trap.fln:18:5", "dyn-field-trap.fln:20:5",
"dyn-field-trap.fln:22:13"));
(* A numeric cast opening a dyn box — TODO.org, "A numeric cast opens a (* A numeric cast opening a dyn box — TODO.org, "A numeric cast opens a
dyn box". programs/dyn-cast.flan is one program because the three dyn box". programs/dyn-cast.flan is one program because the three

View File

@ -9346,13 +9346,13 @@ let () =
(* ── A slot lost, and a second generation ── (* ── A slot lost, and a second generation ──
[:y] goes. Nothing calls [area] after this: its method reads :y, [:y] goes. Nothing calls [area] after this: its method reads :y,
which is now nil, and a generic that traps on a slot its class no which is now refused, and a generic that traps on a slot its class no
longer has is the program being wrong rather than the migration. *) longer has is the program being wrong rather than the migration. *)
let r = redefine "(defclass point [x z])" in let r = redefine "(defclass point [x z])" in
if status r <> "ok" then fail "removing a slot from a class: %s" (said r) if status r <> "ok" then fail "removing a slot from a class: %s" (said r)
else begin else begin
holds "a lost slot reads as absent" holds "a lost slot is absent"
"(if (= (get (at instances 0) :y) nil) 1 0)"; "(if (has-key? (at instances 0) :y) 0 1)";
holds "a lost slot is gone from the count" holds "a lost slot is gone from the count"
"(if (= (length (at instances 0)) 2) 1 0)"; "(if (= (length (at instances 0)) 2) 1 0)";
holds "the slots either side of it are untouched" holds "the slots either side of it are untouched"
@ -9385,31 +9385,13 @@ let () =
end; end;
(* ── A definition that did not change ── (* ── A definition that did not change ──
Every C-c C-k re-runs a file's class definitions, and a generation Every C-c C-k re-runs a file's class definitions, and re-running
bumped per registration rather than per *change* would migrate an unchanged one has to leave its instances' values alone. *)
every instance in the program on every save. Here that would be
visible: the value written below is put into a slot the class
declares, and a spurious migration would keep it — so the
discriminating half is the raw key on the line after, which a real
migration drops and an ignored re-registration leaves alone. *)
holds "a key written straight into an instance"
"(do (put (at instances 0) :scratch 7) 1)";
let r = redefine "(defclass point [x z w])" in let r = redefine "(defclass point [x z w])" in
if status r <> "ok" then fail "re-evaluating an unchanged class: %s" (said r) if status r <> "ok" then fail "re-evaluating an unchanged class: %s" (said r)
else else
holds "an unchanged definition migrates nothing" holds "an unchanged definition keeps the values"
"(if (= (get (at instances 0) :scratch) 7) 1 0)"; "(if (= (get (at instances 1) :x) 5) 1 0)"
(* And the same key after a definition that *did* change, which is
the advisory registry stated as a test rather than as a hope: a
class instance is an open map, [put] accepts any key, and the next
migration drops the ones the class does not declare. TODO.org, "A
redefined defclass migrates its instances lazily", says so in as
many words. *)
let r = redefine "(defclass point [x z w q])" in
if status r <> "ok" then fail "a fourth redefinition: %s" (said r)
else
holds "a migration drops a key the class never declared"
"(if (= (get (at instances 0) :scratch) nil) 1 0)"
end; end;
(try Unix.close c with Unix.Unix_error _ -> ()); (try Unix.close c with Unix.Unix_error _ -> ());
(try Unix.kill mpid Sys.sigkill with Unix.Unix_error _ -> ()); (try Unix.kill mpid Sys.sigkill with Unix.Unix_error _ -> ());

View File

@ -728,8 +728,9 @@ kind as a keyword — <code>:nil</code>, <code>:bool</code>, <code>:int</code>,
<code>:keyword</code> — and an instance's class name, so a class cannot be named <code>:keyword</code> — and an instance's class name, so a class cannot be named
after one of those kinds. The slots are map keys: <code>(.pause s)</code> after one of those kinds. The slots are map keys: <code>(.pause s)</code>
reads one, and <code>(set (.pause s) true)</code> writes one, checking its reads one, and <code>(set (.pause s) true)</code> writes one, checking its
type. <code>get</code> and <code>put</code> do the same, and <code>put</code> type. <code>get</code> and <code>put</code> do the same. A key the class does
is also how a key the class does not declare is added.</p> not declare is refused, so a misspelled slot stops the program at the line that
misspelled it.</p>
<p>Dispatch comes in the two styles and they are one mechanism. <p>Dispatch comes in the two styles and they are one mechanism.
<code>defgeneric</code> dispatches on the class of the first argument, which is <code>defgeneric</code> dispatches on the class of the first argument, which is