What the second root can reach, what it needs, and why both are kept
The daemon side and the Emacs side both landed with nothing written down. Four
files owed something.
`BUILT.md` gets the whole of it: why rooting at an address alone was rejected
and why that rejection was half wrong, what a path step is and how a union's
case travels with it, why the slot goes by index and not by name, and the two
capability lists side by side — the expression root works on a running program
and cannot name a frame; the slot root names one frame and one slot and reaches
an option's payload and a union case's fields, and needs the program stopped.
Neither contains the other, which is the reason there are two.
`emacs/MANUAL.md` says the same thing in the register that file uses, under the
inspector, because the person pressing `i` is the one who needs to know which
root they got and what it cannot do. The globals section's claim that `i` works
on a global "exactly as it does on a local" was true and is now the interesting
difference, so it says what the difference is.
`NEXT.md`'s decided item is struck with what actually shipped: a frame and a
slot index rather than an address and a type, and `l` crossing between the modes
was predicted as a cost and turned out not to be one.
`DISCUSS.md` item 1 is no longer an open question. The number stays — cimport.ml
and NEXT.md cite these by number — and what stays with it is the one correction
worth keeping: an address is not an expression, but a step does not have to be
one either.
And BUILT.md's last paragraph still said `render.ml` prints `(V {:x 1.5})` and
that the printer would move when its reader did. They moved together some time
ago.
This commit is contained in:
parent
404c810958
commit
0389c2282c
76
BUILT.md
76
BUILT.md
@ -2769,3 +2769,79 @@ is a wire format — `emacs/flan-inspect.el` parses it back and hard-codes the c
|
||||
moving the printer alone would break struct inspection in the dev loop without breaking any test that says so. The
|
||||
printer moves when its reader does, in the Emacs lane. It is the one place the old spelling is still correct, and
|
||||
the reason is worth keeping: **a format with two ends only changes at both.**
|
||||
|
||||
**Since written: the printer moved, and both ends moved together.** `render.ml` prints `(V {.x 1.5 .y 0})` now and
|
||||
`emacs/flan-inspect.el` reads the dot, which is the "moves when its reader does" the paragraph above was waiting on.
|
||||
The reader tells `...` from a field label by one character of lookahead, because both begin with a dot and a field
|
||||
name never starts with a second one.
|
||||
|
||||
## Two ways to root a walk, and why neither subsumes the other
|
||||
|
||||
`i` in the break buffer sent a local's **name** to be evaluated. An expression is evaluated where the evaluator
|
||||
stands, so on the innermost frame that lands in the right frame by luck; on any other it may resolve to a global, to
|
||||
another binding of the same name, or to nothing — with the locals listing right above it showing the frame's own
|
||||
storage and nothing saying the two disagree. The display was right and the inspector was not, which is the worst
|
||||
arrangement of the two.
|
||||
|
||||
**The obvious fix was tried and rejected, and the rejection was half wrong.** Rooting the walk at the slot's address
|
||||
does not work on its own: an address is not an expression, so the first `RET` has nothing to build the next expression
|
||||
from and navigation dies at step one. What that argument assumed is that the *step* has to be an expression too, and
|
||||
the shadow stack is what stopped that being true. The daemon holds the frame's address and every slot's type, so
|
||||
stepping into a field is an address plus an offset with that field's type — which is exactly the arithmetic
|
||||
`Render.render` already does for the locals listing. So `Session.render_slot` is `render_locals` with a path applied
|
||||
to the root before the walk and one line out instead of one per slot. No second walk was written and no backend
|
||||
change was needed.
|
||||
|
||||
The verb is `(:op "inspect" :frame N :slot I :path (...))`. A path step is a string for a struct field, an integer for
|
||||
an array or slice element, and the symbol `some` for an option's payload; a union case's field is spelled
|
||||
`Union.case.field`, because the payload's offset depends on which case the value is in and only the renderer knows
|
||||
which case it currently holds — it wrote the head `(Union.case {…})`. Guessing the case from a field name two cases
|
||||
share would read one case's layout over another's payload. Every step that does not fit the type in hand is refused
|
||||
by name with its reason. A pointer is still never followed; that is the renderer's rule and not this mode's.
|
||||
|
||||
**The slot travels by index, not by name.** `check.ml`'s `fresh_slot` only ever allocates, so `(let [v 22] …)` inside
|
||||
`(let [v 11] …)` is two slots both called `v` and both are in the listing; and a refused slot is not in the listing at
|
||||
all, so its position there is not an identifier either. `locals` therefore puts the slot index on each entry as a
|
||||
fourth element, and that is what the break buffer hands back.
|
||||
|
||||
**The frame checks are the listing's, by construction.** `Dev.stopped_frame` is one function and `locals`, `globals`
|
||||
and `inspect` all go through it: alive, stopped, the frame exists, it is the program's and not a `C-x C-e` thunk's,
|
||||
its body is one this session holds, the slot count matches, and `Emit.slot_fingerprint` matches. An inspector with its
|
||||
own copy of those conditions would be free to read a frame whose body was redefined since it was entered, which is
|
||||
precisely the stale-slot answer the listing refuses. `inspect` adds one refusal of its own, for the listing's reason:
|
||||
an unbound slot is a null address and a thunk that read it would fault on the game thread of a program that is already
|
||||
stopped.
|
||||
|
||||
### What each root cannot do that the other can
|
||||
|
||||
Both are wanted and the buffer says which it is on.
|
||||
|
||||
**The expression root** works on a **running** program and starts from anything you can write, a call included. It
|
||||
cannot name a frame — that is the bug — and it cannot reach an option's payload, because the compiler gets at that as
|
||||
field 1 and nothing in the surface language does.
|
||||
|
||||
**The slot root** is exact to one frame and one slot, and it reaches an option's payload and a union case's fields,
|
||||
which have offsets but no accessor form to write. It needs a **stopped** program, it is refused when the frame's body
|
||||
was redefined since it was entered — the same fingerprint the listing is refused by — and it cannot root at an
|
||||
expression at all, so `g` after the program resumes is refused rather than quietly answered from somewhere else.
|
||||
|
||||
A refusal someone can read is the point of the second one existing. The failure being fixed was not "no answer", it
|
||||
was a confident answer from the wrong place.
|
||||
|
||||
### `l` does not cross between them, structurally
|
||||
|
||||
A stack entry in `flan-inspect.el` is `(ROOT PATH . POINT)`. `RET` only ever appends a step to the path under the root
|
||||
the buffer already has, and every new root — `flan-inspect`, `flan-inspect-slot` — starts with an empty stack. A stack
|
||||
with two kinds of root in it therefore cannot be constructed, so the question of what `l` should do when it crosses
|
||||
one does not arise. That stays true if a third rooting mode is added, which is why it is worth having as structure
|
||||
rather than as a rule in a comment.
|
||||
|
||||
The Emacs state is a root plus a path rather than a retained value for the same reason the expression stack was:
|
||||
nothing on this side can hold a Flan value. A value has no header, the thunk that rendered it is `dlclose`d the moment
|
||||
it returns, and there is no heap to retain it in. So every step and every `g` is a fresh request, which is what keeps
|
||||
the view from ever being stale — and it is also why `g` is a key someone presses rather than a timer, since an
|
||||
expression root with an effect in it would fire once a second for ever.
|
||||
|
||||
**One wire detail worth recording.** An empty `:path` is sent by omission. Emacs prints an empty list as `nil`, which
|
||||
is a symbol on the wire and would be read as a step, so there is no way for a client in that language to spell `()`.
|
||||
The daemon reads a missing `:path` — and `nil` — as the slot itself.
|
||||
|
||||
45
DISCUSS.md
45
DISCUSS.md
@ -8,42 +8,21 @@ Settled decisions live in `NEXT.md`. Reasons for what already exists live in `BU
|
||||
|
||||
---
|
||||
|
||||
## 1. `i`, the inspector, and the frame it cannot see
|
||||
## 1. `i`, the inspector, and the frame it cannot see — answered and built
|
||||
|
||||
Two things got conflated here and they should be separated.
|
||||
Option 2 was taken: the inspector has a second rooting mode, at a frame and a slot index. `BUILT.md`'s "Two ways to
|
||||
root a walk" says what each root can and cannot do and why both are kept. The number stays here because other files
|
||||
cite these by number; the question itself is no longer open.
|
||||
|
||||
**What the inspector already does.** Most of what was asked for is built. `flan-inspect` opens its own buffer, lays a
|
||||
value's fields one per line, `RET` walks into one, `l` comes back, `g` re-reads. The renderer bounds its walk at depth 4
|
||||
and span 8, and entering a field renders *that field* from depth 0 — so the elision moves with you rather than
|
||||
truncating permanently. It is CIDER's inspector adapted, and the file says what the adaptation changed.
|
||||
One thing the original entry recorded turned out to be half right, and is worth keeping here rather than deleting with it. "Root the inspector
|
||||
at the slot's address does not work, because an address is not an expression and the first `RET` has nothing to build
|
||||
from" — the premise is true and the conclusion was wrong. What changes it is that the *step* need not be an expression
|
||||
either: the daemon holds the frame's address and every slot's type, so a field is an address plus an offset with that
|
||||
field's type, which is the arithmetic the locals listing already does. The navigation objection was to rooting at an
|
||||
address while still stepping by source.
|
||||
|
||||
**What is actually missing** is detail on the leaves: a number shows in decimal only, with no hex and no binary, and a
|
||||
pointer does not show its address. Purely additive, small, and worth doing.
|
||||
|
||||
**The real problem, and it is sharper than "a bug".** The locals listing renders from each frame's own slot addresses,
|
||||
so it is frame-accurate. The inspector is built on a stack of **expressions** — going into a field means sending a
|
||||
different expression (`(.pos b)` where the last was `b`), and `l` works by popping back to the previous one. That design
|
||||
is forced: a Flan value has no header, the thunk that rendered it is `dlclose`d as soon as it returns, and there is no
|
||||
heap to retain anything in, so nothing can be held server-side the way CIDER holds a JVM object.
|
||||
|
||||
The consequence is that `i` evaluates a name wherever the evaluator stands, **not in the frame being looked at**. On the
|
||||
innermost frame that happens to be right. On any other it may resolve to a global, to a different binding, or fail —
|
||||
with nothing saying so.
|
||||
|
||||
**An earlier suggestion in this conversation — "root the inspector at the slot's address" — does not work**, and the
|
||||
reason is worth keeping: an address is not an expression, so the first `RET` has nothing to build the next expression
|
||||
from and navigation dies at step one. Recorded because it is the obvious fix and it is wrong.
|
||||
|
||||
So the options are genuinely three, and none is free:
|
||||
|
||||
1. **Teach the program to evaluate an expression relative to a frame.** The most useful and the most work: the frame's
|
||||
slots would have to be in scope for a compiled thunk, which means the daemon building a thunk whose free names bind
|
||||
to that frame's addresses. It would also fix `C-x C-e` while stopped, which has the same blindness.
|
||||
2. **Give the inspector a second rooting mode** — an address root that can still walk, by carrying a type alongside the
|
||||
address and stepping to a field's address rather than to a sub-expression. Navigation then works, but the two modes
|
||||
have different capabilities and `l` has to cross between them.
|
||||
3. **Refuse `i` outside the innermost frame**, honestly and by name. Cheapest, and it gives up the feature exactly where
|
||||
it is most wanted, since the innermost frame is the one already fully visible.
|
||||
`l` crossing between the modes was listed as a cost of option 2 and it is not one. A stack entry carries its own root
|
||||
and `RET` only extends the path under the root it already has, so a mixed stack cannot be built at all.
|
||||
|
||||
## 2. Annotating the IR and the disassembly with the source
|
||||
|
||||
|
||||
15
NEXT.md
15
NEXT.md
@ -710,16 +710,11 @@ op that compiles on first use and a cheap re-invoke per tick.
|
||||
The author also raised **ghost text** as an alternative or addition to a dedicated buffer — values shown inline at the
|
||||
code they belong to. Not designed; the buffer is the port, ghost text is a further question.
|
||||
|
||||
**The inspector gets a second way to start: an address and a type.** Closes the hole in `DISCUSS.md` item 1, where `i`
|
||||
on a local in any frame but the innermost evaluates a name wherever the evaluator stands rather than in that frame, and
|
||||
may silently inspect something else.
|
||||
|
||||
The inspector navigates by rewriting *expressions* — `(.pos b)` where the last was `b` — and `l` pops back. That is
|
||||
why the obvious fix, rooting it at the slot's address, was rejected: an address is not an expression, so the first
|
||||
`RET` has nothing to build from. **The shadow stack changed this.** The daemon now has a frame's address and every
|
||||
slot's type, so the second rooting mode is cheap: start from an address plus a type, and stepping into a field is
|
||||
address-plus-offset with the field's type. `Render.render` already does exactly that arithmetic for locals, and
|
||||
navigation keeps working, which was the objection.
|
||||
~~**The inspector gets a second way to start: an address and a type.**~~ **Built.** See `BUILT.md`, "Two ways to root
|
||||
a walk, and why neither subsumes the other". It went in as a frame and a slot *index* rather than an address and a
|
||||
type — the daemon holds both and an index is the thing the listing can hand back, while an address is not something an
|
||||
editor should be holding. The one prediction that did not survive contact: `l` crossing between the two modes was
|
||||
listed as a cost and is not one, because a stack entry carries its own root and a mixed stack cannot be built.
|
||||
|
||||
**Structural typing requires identical layout — same fields, same types, same order.** Settled by the author, and it
|
||||
makes the feature simple rather than hard: structural compatibility becomes "the same memory", which costs nothing at
|
||||
|
||||
@ -145,7 +145,7 @@ Keys in that buffer:
|
||||
| `TAB` / `n` | next restart |
|
||||
| `S-TAB` / `p` | previous |
|
||||
| `f` | fold a stack frame open or closed |
|
||||
| `i` | inspect a local variable |
|
||||
| `i` | inspect the local or global at point |
|
||||
| `a` | abort |
|
||||
| `g` | read the program again |
|
||||
| `q` | close the buffer |
|
||||
@ -198,11 +198,40 @@ Two things worth knowing, because they are unlike other inspectors.
|
||||
**The view is never stale.** Every step reads the program as it is *now*. Most
|
||||
inspectors show you the object as it was when you opened it.
|
||||
|
||||
**The root expression runs again on every step.** Going into a field sends a new
|
||||
expression — `(.pos b)` where the last one was `b`. Appending a field name is
|
||||
harmless, but the root need not be: if you inspect `(spawn-enemy)`, you spawn one
|
||||
per keystroke. That is why there is no auto-refresh and why `g` is a key you
|
||||
press rather than a timer.
|
||||
**The root runs again on every step.** Going into a field sends a new request,
|
||||
not a lookup in something remembered. Appending a field name to an expression
|
||||
is harmless, but the expression itself need not be: if you inspect
|
||||
`(spawn-enemy)`, you spawn one per keystroke. That is why there is no
|
||||
auto-refresh and why `g` is a key you press rather than a timer.
|
||||
|
||||
### Two ways to root a walk
|
||||
|
||||
There are two, they are not equally capable, and the top line of the buffer
|
||||
says which one you are on.
|
||||
|
||||
**An expression** — `C-c C-i`, and `i` on a global line in the break buffer.
|
||||
It works on a **running** program and starts from anything you can write,
|
||||
including a call. It cannot name a frame: an expression is evaluated where the
|
||||
evaluator stands, so a local's name reaches that local only when its frame is
|
||||
the innermost one. And it cannot reach an option's payload, because nothing in
|
||||
the language names it.
|
||||
|
||||
**A frame and a slot** — `i` on a local line in the break buffer. It is exact
|
||||
to one frame and one slot, so the value you get is the one the listing above it
|
||||
drew. It reaches an **option's payload** and a **union case's fields**, which
|
||||
have offsets but no accessor form to write. In exchange it needs a **stopped**
|
||||
program, and it is refused — by name, with the reason — once the program
|
||||
resumes or if the frame's body was redefined since the frame was entered. It
|
||||
cannot start from an expression at all.
|
||||
|
||||
Neither subsumes the other, which is why both are here. The one you get is
|
||||
chosen for you by the line you press `i` on.
|
||||
|
||||
**`l` never crosses between them**, and that is structural rather than a rule
|
||||
someone has to remember. Every entry on the buffer's stack carries its own
|
||||
root; `RET` only ever lengthens the path under the root already in hand; and
|
||||
starting a new root starts an empty stack. So a stack with both kinds in it
|
||||
cannot be built, and `l` has nothing to cross into.
|
||||
|
||||
---
|
||||
|
||||
@ -463,8 +492,14 @@ does" — at no cost in duplication. And the order is **by the innermost frame
|
||||
that touches it**, because a deep stack makes the union large and proximity to
|
||||
the error is what puts the likely culprit on top.
|
||||
|
||||
`i` works on a global line exactly as it does on a local: a global's name is an
|
||||
expression, so the inspector can be pointed at it with nothing new.
|
||||
`i` works on a global line, and it roots differently there than on a local —
|
||||
which is the fix rather than an inconsistency. A global really is reached by
|
||||
name: the thunk the daemon loads binds to the program's own storage through the
|
||||
dynamic linker, so the name means the same thing wherever it is evaluated. A
|
||||
local is storage in one frame, and its name evaluated anywhere else may find a
|
||||
global, another binding of the same name, or nothing. So a local goes in by
|
||||
frame and slot index — the same two facts the listing was drawn from — and a
|
||||
global by name. See "Two ways to root a walk" above for what each can reach.
|
||||
|
||||
Two things are said rather than left out. A global whose type the structural
|
||||
printer has no arm for is refused by name with the reason. And a frame the
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user