diff --git a/emacs/flan-inspect.el b/emacs/flan-inspect.el index 0e246d6..9d2a666 100644 --- a/emacs/flan-inspect.el +++ b/emacs/flan-inspect.el @@ -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 `' for every pointer, on purpose: it is the diff --git a/emacs/flan-inspect.elc b/emacs/flan-inspect.elc new file mode 100644 index 0000000..1dde004 Binary files /dev/null and b/emacs/flan-inspect.elc differ