diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 43a6cf3..a378fb9 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -160,6 +160,20 @@ silently omitted. **`C-c C-M-b`** is the same choice as a quick one-key prompt, when you already know which restart you want and do not need the buffer. +**The condition's fields are named and typed, and have no values.** Under the +condition you get the struct it is — `:path string`, `:tried i32` — because the +daemon compiled the program and knows what that type looks like without asking +the program anything. What is beside each field is a note saying the value is +not available, not a blank: a value lives in the stopped frame, and nothing yet +hands the break loop's condition pointer back. Knowing the shape is still worth +having — it tells you whether the field you were about to blame is a field of +this condition at all. + +If that section says it could not resolve the name, read it: a package +qualifies what it declares, so two packages' `Missing` are `a/Missing` and +`b/Missing`. The daemon refuses a bare name and says what it could have meant +rather than picking one. + After you choose, the program carries on from the restart. It never unwound, so everything it had is still there. diff --git a/emacs/flan-cnr.el b/emacs/flan-cnr.el index 9e0d8c6..b95038c 100644 --- a/emacs/flan-cnr.el +++ b/emacs/flan-cnr.el @@ -70,7 +70,7 @@ from fixtures, and so `flan-dev.el' is named in one place.") (defconst flan-cnr-unavailable '((layout - . "which struct the class name refers to [needs a daemon op: `layout'. The field names and types are in `Tast.structs', which the daemon holds because it owns the build — no running program is involved. It is the one piece of this buffer that is a day's work on one side of the socket]") + . "the daemon could not resolve this condition's name to a struct [the `layout' op answers out of `Tast.structs' and needs a *qualified* name. A package qualifies what it declares, so two packages' `Missing' are two names and neither is `Missing'; the daemon refuses a bare one and lists what it could have meant rather than picking. A name it cannot place at all is a program built from source this daemon did not compile]") (value . "the break loop is handed the condition as an opaque pointer, and `break_loop' currently discards it; nothing at run time can render a value whose type it does not know [needs an agent verb: `condition', to stash and hand back the pointer, and a daemon-built render thunk aimed at it]") (site @@ -382,20 +382,51 @@ list already says it." "What a stopped Flan program is offering." (setq buffer-read-only t)) -(defun flan-cnr-state-from-reply (reply) +(defun flan-cnr-state-from-reply (reply &optional fields) "The buffer's state, out of a `break' REPLY. +FIELDS is the condition's layout, if it was asked for and answered — a list of +(NAME TYPE VALUE), where VALUE is nil because no running program was consulted +to get it. + Everything this cannot fill in is left nil deliberately: the renderer draws a section saying why rather than leaving one out, and a section that is absent -cannot be told from one that is empty." +cannot be told from one that is empty. + +Takes the layout rather than fetching it, so this stays a function from data to +data and the fixture-driven tests can drive it without a socket." (list :condition (plist-get reply :condition) :restarts (plist-get reply :restarts) - ;; Both nil deliberately: `break' answers a class name and a list of - ;; restart names, and nothing else exists to put here yet. The renderer - ;; draws a section saying why rather than leaving one out. - :fields nil + :fields fields + ;; Still nil deliberately: nothing here is attached to the stopped + ;; program's frames, and the renderer says so by name. :stack nil :locals nil)) +(defun flan-cnr-layout (type) + "The fields of TYPE, as the renderer wants them, or nil. +`layout' is answered out of the daemon's own `Tast.structs' — it owns the +build — so this costs no round trip to the program and works while it is +stopped, which is the only moment this buffer exists in. + +TYPE is the string `break' reported as `:condition', and that string is the +qualified struct name the compiler put into the error: `Emit' takes it from +`Types.Named', the agent holds it in `condition_name', and the daemon looks it +up by the same name. So it resolves without anything here knowing what package +it came from. + +A refusal is not an error here: nil means the layout section draws the reason +it is empty, which is the degradation the buffer is already built for." + (when (and type (not (string-empty-p type))) + (let ((r (funcall flan-cnr-request-function (list :op "layout" :type type)))) + (when (equal (plist-get r :status) "ok") + (mapcar (lambda (f) + ;; The value is nil, and that is the honest half: the shape + ;; of a condition is a fact about the build, its contents + ;; are a fact about the stopped frame, and only the first is + ;; knowable today. + (list (nth 0 f) (nth 1 f) nil)) + (plist-get r :fields)))))) + ;;;###autoload (defun flan-cnr-show () "Show what the stopped program is offering, in a buffer. @@ -410,7 +441,8 @@ walk from a running program." (let ((buf (get-buffer-create flan-cnr-buffer))) (with-current-buffer buf (unless (derived-mode-p 'flan-cnr-mode) (flan-cnr-mode)) - (flan-cnr--render (flan-cnr-state-from-reply r))) + (flan-cnr--render + (flan-cnr-state-from-reply r (flan-cnr-layout (plist-get r :condition))))) (pop-to-buffer buf) buf))) diff --git a/emacs/test-flan-cider.el b/emacs/test-flan-cider.el index 7dfbfac..5f3698a 100644 --- a/emacs/test-flan-cider.el +++ b/emacs/test-flan-cider.el @@ -465,6 +465,50 @@ (test-flan--check "and n goes back to abort" (get-text-property (point) 'flan-cnr-abort))) + +;; The `layout' op, from this side: the condition's own name goes out as +;; `:type' and comes back as fields with no values. Two requests are made for +;; one `C-c C-b' — `break' then `layout' — so the stub records both. +(let ((asked nil)) + (let ((flan-cnr-request-function + (lambda (form) + (push form asked) + (pcase (plist-get form :op) + ("break" '(:status "ok" :stopped t :condition "sim/Missing" + :restarts ("retry"))) + ("layout" '(:status "ok" :type "sim/Missing" + :fields (("path" "string") ("tried" "i32")))))))) + (let ((text (with-current-buffer (save-window-excursion (flan-cnr-show)) + (buffer-string)))) + (test-flan--check "the condition's name is what `layout' is asked for" + (equal (plist-get (car (last asked)) :op) "break")) + (test-flan--check "and it is sent back verbatim, qualified as it came" + (equal (plist-get (car asked) :type) "sim/Missing")) + (test-flan--check "the fields are drawn, named and typed" + (string-match-p ":path *string" text)) + ;; Shape and contents are two different questions, and only the first is + ;; answerable without the pointer the break loop discards. + (test-flan--check "and every value still says why it is missing" + (string-match-p ":tried *i32 *value not available" text))))) + +;; A layout the daemon refuses — a bare name it will not guess between two +;; packages, or a type it cannot place — leaves the section drawing its reason +;; rather than turning `C-c C-b' into an error. The restarts are the decision +;; in front of you and they are still there. +(let ((flan-cnr-request-function + (lambda (form) + (pcase (plist-get form :op) + ("break" '(:status "ok" :stopped t :condition "Missing" + :restarts ("retry"))) + ("layout" '(:status "error" :message "Missing is not a qualified name" + :candidates ("a/Missing" "b/Missing"))))))) + (let ((text (with-current-buffer (save-window-excursion (flan-cnr-show)) + (buffer-string)))) + (test-flan--check "a refused layout is a section that says so" + (string-match-p "not available.*needs a \\*qualified\\* name" text)) + (test-flan--check "and the restarts are drawn anyway" + (string-match-p "\\[retry\\]" text)))) + (let ((flan-cnr-request-function (lambda (_) '(:status "ok" :restarts nil :stopped nil)))) (test-flan--check "a running program is refused, by name" diff --git a/emacs/test-flan-dev.el b/emacs/test-flan-dev.el index bc2dccf..7a89d58 100644 --- a/emacs/test-flan-dev.el +++ b/emacs/test-flan-dev.el @@ -105,6 +105,17 @@ is written instead — the real `message' call the real command makes." (test-flan--check "describe lists the program's globals" (member "ticks" (plist-get r :globals)))) + ;; `layout' against the real daemon, through `flan-cnr-layout', which is how + ;; the conditions buffer gets it. The reply is the first one with a list of + ;; lists in it, so `read' on this side is doing something it does nowhere + ;; else — and the daemon answers it without asking the program anything. + (require 'flan-cnr) + (let ((flan-cnr-request-function #'flan-dev--request)) + (test-flan--check "a struct's fields come back named and typed" + (equal (flan-cnr-layout "Missing") '(("id" "i32" nil)))) + (test-flan--check "and a type the daemon cannot place is nil, not an error" + (null (flan-cnr-layout "Nonesuch")))) + ;; C-c C-c on the form at point: put point inside `step' and send it. The ;; text comes from the buffer, so this exercises `beginning-of-defun' against ;; Flan's own syntax table as much as it does the wire.