diff --git a/FIX.org b/FIX.org index 8bacfb5..802df2c 100644 --- a/FIX.org +++ b/FIX.org @@ -2540,11 +2540,25 @@ Without that rule every save would migrate every instance in the program. instance is read or written"; those are the three places that read or write the slot *set*. -~render~ is deliberately not one of them. It runs inside trap reporting, -where the heap is whatever the trap left, and a printer that reallocates an -object's storage is not something to have on that path. So a stale instance -prints its old slots until something touches it. CLHS's -"implementation-dependent time" allows it and the printer stays a printer. +*Neither printer is one of them*, and that has a consequence somebody will +meet. ~render~ — which ~print~ goes through, and which the editor renders +every dyn value with — and ~say_render~ — the 96-byte sentence a trap +prints — both walk the entries raw and neither syncs. ~say_render~ runs +inside trap reporting, where the heap is whatever the trap left, and a +printer that frees an object's entry block and installs another is not +something to have on that path; ~render~ is its sibling and is reached from +it for nested values, so splitting them would put the mutation one recursion +below a trap anyway. + +So: *a stale instance shows its old slots to the editor until something +touches it.* A watch expression, the value ~C-x C-e~ answers and the +inspector's render of a dyn all arrive through ~render~, so in the moment +after a ~defclass~ is redefined the inspector can show a slot the class no +longer has and omit one it has gained — while ~(get p :z)~ typed at the same +instant answers the new definition, migrates the instance, and makes the +inspector agree from then on. CLHS's "implementation-dependent time" permits +it; it is the price of the printer staying a printer; and it is disclosed +here rather than discovered. The migration rebuilds the entry block rather than compacting it in place, and writes the slots in the *class's* order. One ~malloc~ per instance per diff --git a/lib/session.ml b/lib/session.ml index 0e99ff2..0d30878 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -701,7 +701,20 @@ let eval ?(origin = "") ?pause t src : change = constructor or takes its address, minus the ones this evaluation is recompiling. [Tast.walk] rather than a match on the body's head: a constructor call can be anywhere in an - expression, and a missed one is the wild-pointer case above. *) + expression, and a missed one is the wild-pointer case above. + + **Known to be dead today, and deliberately not tightened.** + The checker refuses every caller before this runs, so [stale] + is empty on every path anyone has found; a global's [ginit] is + not walked here for the same reason it need not be — a global + initialised by calling a constructor is re-checked with + everything else, and a mismatch there is the checker's + refusal too. What this branch is for is the day that stops + being true. It is a tripwire, not a filter: if it ever fires, + the answer is a refusal naming the callers rather than a + module that loads and then reads a register as a pointer. Do + not delete it because it is unreachable — unreachable is the + property being asserted. *) let stale = List.filter_map (fun (f : Tast.fn) -> diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index 82898f0..ab93490 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -1159,12 +1159,29 @@ flan_dyn flan_dyn_map_new(void) { * later than the next time a slot is read or written" and those are the * three places that read or write the slot *set*. * - * [render] is deliberately not one of them. It runs inside trap reporting, - * where the heap is whatever the trap left, and a printer that reallocates - * an object's storage is not something to have on that path. A stale - * instance therefore prints its old slots until something touches it, which - * CLHS's "implementation-dependent time" allows and which is worth the - * printer staying a printer. */ + * **The two printers are deliberately not among them**, and the consequence + * is visible to whoever is sitting in front of the editor, so it is written + * out rather than left as a footnote. [render] — which [print] and every + * value the editor renders go through — and [say_render] — the 96-byte + * sentence a trap prints — both walk [items] raw and neither syncs. + * + * The reason is the same for both: [say_render] runs inside trap reporting, + * where the heap is whatever the trap left, and a printer that frees an + * object's entry block and installs another is not something to have on + * that path; [render] is the same function's sibling and is called from it + * for nested values, so splitting them would put a mutation one recursion + * below a trap anyway. + * + * What that costs: **a stale instance shows its OLD slots to the editor + * until something touches it.** A watch expression, the value [C-x C-e] + * answers, and the inspector's render of a dyn all reach a class instance + * through [render], so immediately after a [defclass] is redefined the + * inspector can show a slot the class no longer has, and not show one it + * has gained — while [(get p :z)] typed at the same instant answers the new + * definition and migrates it, after which the inspector agrees. CLHS's + * "implementation-dependent time" permits it and it is the price of the + * printer staying a printer; it is not a bug report waiting to happen only + * because it is written down here, in FIX.org, and nowhere else. */ /* Interning, which is under "Keywords" further down. This file includes no * header of its own — every entry point is written out in flan_dyn.h and diff --git a/test/test_session.ml b/test/test_session.ml index 5b0384f..9b202a6 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -1083,13 +1083,22 @@ let () = (String.concat " " c.Session.fns); (* The registration, and the thunk that runs it. Without the first the runtime never hears that the class changed; without the second the - module defines a function nothing calls. *) - if not (has c.Session.ir "flan_dyn_class_def") then + module defines a function nothing calls. + + [call void @] and not the bare symbol, which is the difference + between a pin and a decoration: [emit.ml]'s declare block names + every runtime entry point in every module it writes, so + "flan_dyn_class_def" on its own is in the text of a module that + registers nothing. Checked by mutation — the bare needle passes with + the thunk deleted. *) + if not (has c.Session.ir "call void @flan_dyn_class_def") then fail "a redefined class did not register its slots"; - if not (has c.Session.ir "flan_reload_call") then + if not (has c.Session.ir "define void @flan_reload_call") then fail "the class registration had nothing to run it"; - (* And the slot names, in the packed form the runtime splits. *) - if not (has c.Session.ir "x\\0Ay\\0Az") then + (* And the slot names, in the packed form the runtime splits — which is + what says the call carries *this* class's new list and not some + other module's leftovers. *) + if not (has c.Session.ir "c\"x\\0Ay\\0Az\"") then fail "the registration did not carry the new slot list" | exception Loc.Error { Loc.dmsg = m; _ } -> fail "adding a slot to a class was refused: %s" m); @@ -1106,12 +1115,24 @@ let () = fail "removing a slot from a class was refused: %s" m); (* A class that did not change registers anyway — the runtime ignores a re-registration of the same list, and something has to tell it the list - in the first place. *) + in the first place. This is the C-c C-k shape: every class in the file + arrives, whether or not any of them moved. + + The needles are the same two discriminating ones, and the slot list is + the *old* one, which is what says the registration is of this class as + it currently stands rather than a leftover from the case above. That + the runtime then declines to bump the generation is flan_dyn.c's half + and is pinned where it happens: dyn_ops.c's [classes] mode writes a + value and re-registers the same list under it, and test_dev.ml does the + same against a live program. Neither is visible in IR text, which is + why neither is asserted here. *) (let t, _ = Session.create ~file:"programs/dev-class.flan" () in match Session.eval t "(defclass point [x y])" with | c -> - if not (has c.Session.ir "flan_dyn_class_def") then - fail "an unchanged class definition registered nothing" + if not (has c.Session.ir "call void @flan_dyn_class_def") then + fail "an unchanged class definition registered nothing"; + if not (has c.Session.ir "c\"x\\0Ay\"") then + fail "an unchanged class registered some other slot list" | exception Loc.Error { Loc.dmsg = m; _ } -> fail "re-evaluating an unchanged class was refused: %s" m); (* Now the refusal that stands, which is the whole reason the relaxation