WIP: the inspector's address root, half wired on the Emacs side
OCaml side is done and builds. Emacs side is mid-edit and INCOMPLETE — see the handoff below. `dune build --root . @check` is green. `dune test --root .` was NOT run. The .el files were not byte-compiled and flan-inspect.el will not work as it stands: the state layer still speaks the old single-expression shape while the helpers above it have been rewritten for roots and paths. WHAT WORKS (daemon, lib/, in the parent commit and unchanged here) - `(:op "inspect" :frame N :slot I :path (...))` renders one value rooted at a stopped frame's slot address. `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. - 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 field is spelled `Union.case.field`, because the payload's offset depends on the case and only the renderer knows which case the value is in. - Every step that does not fit the type in hand is refused by name with the reason: a field the type does not have, an index past a fixed array's end, `some` on something that is not an option, a union field without its case. - The frame's identity IS checked, and not by a second copy: `Dev.stopped_frame` is one function now and `locals` and `inspect` both go through it — alive, stopped, frame exists, the frame is the program's and not a thunk's, the body is one this session holds, the slot count matches, and `Emit.slot_fingerprint` matches. `inspect` additionally refuses an unbound slot, for the listing's reason: a null address would fault on the stopped game thread. - The slot travels by INDEX, not by name. Two slots can share a name (`fresh_slot` only allocates) and a refused slot is not in the listing, so neither the name nor the position identifies one. `locals` now puts the slot index as a fourth element on each `:locals` entry. - `Dev.run_render_thunk` is one function; `locals`, `globals` and `inspect` share the build/deliver/wait/read tail. - `layout`'s "union values are milestone 6" is corrected. WHAT IS HALF-BUILT, AND EXACTLY WHERE IT STOPS `emacs/flan-inspect.el`. Done: the header comment explaining the two roots; `flan-inspect-step-expr` taking a 3-element `:field` step; `flan-inspect-wire-step`; `flan-inspect--root-label`; `flan-inspect-refusal` taking an optional ROOT and allowing an option's payload under a `:slot` root. NOT done, and this is the whole of what is left: 1. `flan-inspect--expr` / `flan-inspect--stack` still hold a bare expression. They must become `flan-inspect--root` (`(:expr EXPR)` or `(:slot FRAME SLOT NAME)`) plus `flan-inspect--path`, with stack entries of `(ROOT PATH . POINT)`. 2. `flan-inspect--value` must branch on the root: `eval-expr` with `(flan-inspect--root-label root path)` for `:expr`; for `:slot`, send `(:op "inspect" :frame F :slot S :path P)` with P built by `flan-inspect-wire-step` over the path, and take `:value` from the reply. 3. `flan-inspect--show`, `-into`, `-pop`, `-refresh` rewired to (ROOT PATH). `-into` must build a `:some` step when the node's kind is `option`, and put the parent node's `:type` as the third element of a `:field` step. 4. New entry point `flan-inspect-slot (frame slot name)`, kept separate from `flan-inspect (expr)` — `emacs/flan-mode.el` autoloads and binds the latter and that file is out of this lane. 5. `emacs/flan-cnr.el`: the `flan-cnr-inspect` text property must carry `(:slot FRAME SLOT NAME)` on a local line — the slot index is `(nth 3 l)` now — and `(:expr NAME)` on a global line, with `flan-cnr-inspect` dispatching to the right entry point. 6. `emacs/test-flan-cider.el`: the fixture at "`i' on a local inspects it by name" asserts the old behaviour and must be rewritten; the locals fixtures need a fourth element. 7. `test/test_dev.ml`: no coverage of the new op yet. The discriminating test to write first is a stack whose OUTER frame has a local whose name is also a global with a different value, asserting `inspect` answers the frame's value. A new `test/programs/dev-inspect.flan` is picked up by the existing glob. 8. `BUILT.md`, `emacs/MANUAL.md`, and striking the item from `NEXT.md`'s "Decided in discussion" and `DISCUSS.md` item 1 — none done. THE THREE ANSWERS THE TASK ASKED FOR - Navigation in the new mode: the daemon supports it fully — RET extends the path, `l` shortens it, and both are a fresh request, so the view is never stale. The Emacs half of that is item 3 above and is not wired. - `l` does not cross between the modes, and that is structural rather than a rule: a stack entry carries its own root, RET only ever extends the path under the root it already has, and every new root starts with an empty stack. A mixed stack cannot be constructed, so the question does not arise — and it stays answered if a third rooting mode is added. - What each mode cannot do that the other can. The expression root works on a RUNNING program and roots at anything you can write, a call included; it cannot name a frame, so it is the bug. The slot root names one frame and one slot and is exact; it reaches an option's payload and a union case's fields, which have offsets but no accessor in the surface language; it needs a stopped program, it is refused when the frame's body was redefined since it was entered, and it cannot root at an expression at all.
This commit is contained in:
parent
122e17bd06
commit
03d4460d72
@ -33,6 +33,47 @@
|
||||
;; (lib/session.ml). A field past either bound comes back as `...' and no
|
||||
;; amount of squinting at the echo area recovers it. Re-rooting the walk at
|
||||
;; that field renders it from depth 0 — the bound moves with you.
|
||||
;;
|
||||
;;; Two ways to root a walk, and why there had to be a second
|
||||
;;
|
||||
;; Everything above describes the *expression* root, and it has one hole: an
|
||||
;; expression is evaluated where the evaluator stands. `i' on a local in the
|
||||
;; break buffer used to send that local's name, and 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, because that
|
||||
;; listing renders from each frame's slot addresses and is frame-accurate.
|
||||
;; The display was right and this buffer was not.
|
||||
;;
|
||||
;; Rooting at the slot's address alone does not fix it, and that was tried:
|
||||
;; an address is not an expression, so the first RET has nothing to build
|
||||
;; from. What the shadow stack changed is that the *step* does not have to be
|
||||
;; an expression either. The daemon has the frame's address and every slot's
|
||||
;; type, so going into a field is an address plus an offset with that field's
|
||||
;; type — the arithmetic `Render.render' already does for the listing. So
|
||||
;; there is a second rooting mode here, `flan-inspect-slot', and the daemon
|
||||
;; verb behind it is `(:op "inspect" :frame N :slot I :path (...))'.
|
||||
;;
|
||||
;; The two roots are not equally capable and the buffer says which it is on:
|
||||
;;
|
||||
;; the expression root works on a *running* program and roots at anything
|
||||
;; you can write, a call included. It cannot reach an option's payload,
|
||||
;; because Flan has no accessor form that does, and it cannot say which
|
||||
;; frame it means;
|
||||
;;
|
||||
;; the slot root names one frame and one slot, so it is exact, and it
|
||||
;; reaches an option's payload and a union case's fields, which have offsets
|
||||
;; but no accessor. It needs a stopped program, it is refused if the
|
||||
;; frame's body was redefined since it was entered — the same slot
|
||||
;; fingerprint the listing is refused by — and it cannot root at an
|
||||
;; expression, so `g' after the program resumes is refused rather than
|
||||
;; quietly answered from somewhere else.
|
||||
;;
|
||||
;; `l' never crosses between them, and that is structural rather than a rule:
|
||||
;; a stack entry carries its own root, RET only ever extends the path under
|
||||
;; the root it already has, and every new root — `flan-inspect',
|
||||
;; `flan-inspect-slot' — starts with an empty stack. So a mixed stack cannot
|
||||
;; be built, and that stays true if a third rooting mode is ever added.
|
||||
|
||||
;;; Code:
|
||||
|
||||
@ -219,27 +260,88 @@ reply without a daemon behind them, and so that this file names
|
||||
;; work without a handle to retain.
|
||||
|
||||
(defun flan-inspect-step-expr (expr step)
|
||||
"The Flan expression reaching STEP inside EXPR."
|
||||
"The Flan expression reaching STEP inside EXPR.
|
||||
A `:field' step may carry the type it was read out of, for the slot root's
|
||||
benefit; here it is ignored, because an accessor is written the same way
|
||||
whatever the value came from."
|
||||
(pcase step
|
||||
(`(:field ,name) (format "(.%s %s)" name expr))
|
||||
(`(:field ,name . ,_) (format "(.%s %s)" name expr))
|
||||
(`(:index ,i) (format "(at %s %d)" expr i))
|
||||
(_ expr)))
|
||||
|
||||
;;; Where a field is, said as an offset
|
||||
|
||||
;; The slot root's version of the same step, and it is not source: the daemon
|
||||
;; is walking a type, so a field is its name and an element is its number.
|
||||
;; Two cases need more than the name.
|
||||
;;
|
||||
;; A union's payload sits at an offset that depends on which case the value
|
||||
;; is in, and only the renderer knows which case it currently is — it wrote
|
||||
;; `(Union.case {.f …})'. So the type travels with the step and the wire
|
||||
;; spelling is `Union.case.f'. Guessing the case from a field name two
|
||||
;; cases share would read one case's layout over another's payload.
|
||||
;;
|
||||
;; An option's payload has no name at all; it is the symbol `some'.
|
||||
|
||||
(defun flan-inspect-wire-step (step)
|
||||
"STEP as the `inspect' op spells it."
|
||||
(pcase step
|
||||
(`(:field ,name ,type)
|
||||
(if (and (stringp type) (string-match-p "\\." type))
|
||||
(concat type "." name)
|
||||
name))
|
||||
(`(:field ,name) name)
|
||||
(`(:index ,i) i)
|
||||
(`(:some) 'some)
|
||||
(_ (format "%s" step))))
|
||||
|
||||
;;; A root, and the path walked from it
|
||||
|
||||
;; A root is `(:expr EXPR)' or `(:slot FRAME SLOT NAME)'. The path is a list
|
||||
;; of steps applied to it in order, and the pair is the whole of this buffer's
|
||||
;; position — which is why a stack entry carries both and `l' cannot cross
|
||||
;; between two kinds of root by accident.
|
||||
|
||||
(defun flan-inspect--root-label (root path)
|
||||
"How ROOT walked by PATH is named at the top of the buffer and in the trail."
|
||||
(pcase root
|
||||
(`(:expr ,expr)
|
||||
(seq-reduce #'flan-inspect-step-expr path expr))
|
||||
(`(:slot ,frame ,_slot ,name)
|
||||
(concat (format "%s [frame %d]" name frame)
|
||||
(mapconcat (lambda (s)
|
||||
(pcase s
|
||||
(`(:field ,f . ,_) (concat "." f))
|
||||
(`(:index ,i) (format "[%d]" i))
|
||||
(`(:some) ".some")
|
||||
(_ "")))
|
||||
path "")))
|
||||
(_ "?")))
|
||||
|
||||
;;; Why a thing cannot be entered
|
||||
|
||||
;; Every refusal is by name and carries its reason, because the alternative —
|
||||
;; RET doing nothing on some lines and something on others — is a UI that
|
||||
;; teaches you nothing about the language.
|
||||
|
||||
(defun flan-inspect-refusal (node)
|
||||
"Why NODE cannot be inspected, or nil if it can."
|
||||
(defun flan-inspect-refusal (node &optional root)
|
||||
"Why NODE cannot be inspected, or nil if it can.
|
||||
ROOT is the root the walk is on, because two of these are refusals of the
|
||||
*expression* root rather than of the value. Omitted means the expression
|
||||
root, which is the older and the more limited of the two."
|
||||
(pcase (plist-get node :kind)
|
||||
('struct (and (null (plist-get node :children))
|
||||
"a struct with no fields the renderer could reach"))
|
||||
('seq (and (null (plist-get node :children))
|
||||
"an empty sequence: there is no element to go into"))
|
||||
('option
|
||||
"an option's payload: Flan has no accessor form that reaches it, so there is no expression to send")
|
||||
;; The one place the two roots differ in the slot root's favour, and it
|
||||
;; is worth saying which it is rather than refusing flatly: the payload
|
||||
;; is field 1 and the compiler reaches it there, so an address root steps
|
||||
;; into it by offset. Nothing in the surface language does, so an
|
||||
;; expression root has nothing to send.
|
||||
(and (not (eq (car-safe root) :slot))
|
||||
"an option's payload: Flan has no accessor form that reaches it, so there is no expression to send. `i' on a local in the break buffer roots at the slot's address instead, and that root can step into it"))
|
||||
('ptr
|
||||
;; And its address is not here either. `Render.render' (lib/render.ml)
|
||||
;; writes the bare word `<ptr>' for every pointer, on purpose: it is the
|
||||
|
||||
BIN
emacs/flan-inspect.elc
Normal file
BIN
emacs/flan-inspect.elc
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user