The rebase's four repairs, and two pins that were not pinning anything
Rebased onto dev-loop. Three conflicts were additive and both sides are kept: FIX.org's two appended sections, want_map's diagnostics argument against the class_sync inserted beside it, and test_dev.ml's agent-socket block against this lane's migration block, whose comment no longer says "the block above" now that something sits between. The fourth is the one the auto-merge hid. flan_dyn_class_def's argument check was written against the pre-diagnostics trap1 and merged clean into a tree where trap1 takes a location first, so the class name would have been read as a length. dune build does not compile flan_dyn.c, so the green build said nothing; caught by compiling a program. Two pins in test_session.ml asserted "flan_dyn_class_def" against the IR text, which every module contains because emit.ml declares every runtime entry point in all of them. Both now assert the call and the packed slot list. Checked by mutation: with the thunk suppressed the old needles pass and the new ones fail, along with the daemon's slot count. Also disclosed: say_render is the second raw reader beside render, and neither syncs, so a stale instance shows its old slots in the inspector until something touches it. That is the editor-facing consequence of keeping the printers printers, and it is now in the runtime comment and in FIX.org rather than left to be met. And the stale-caller walk says in as many words that it is a tripwire, unreachable on purpose, not a filter to be tidied away.
This commit is contained in:
parent
63fb5c629c
commit
5cfefa586b
24
FIX.org
24
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
|
||||
|
||||
@ -701,7 +701,20 @@ let eval ?(origin = "<eval>") ?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) ->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user