The conditions buffer names and types the fields it used to refuse

C-c C-b asks layout with the condition's own name and draws the fields under
it. The values stay refused, by name, because the shape of a condition is a
fact about the build and its contents are a fact about the stopped frame — and
only one of those is knowable today. A layout the daemon refuses is nil rather
than an error: the buffer already draws a section saying why one is empty, and
failing the whole command would take away the restarts over an annotation.
This commit is contained in:
Joseph Ferano 2026-09-12 10:39:22 +07:00
parent a8f08eda6d
commit 404b8559c5
4 changed files with 109 additions and 8 deletions

View File

@ -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.

View File

@ -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)))

View File

@ -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"

View File

@ -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.