The stack section draws the frames it was written for
The web build's three artifacts are ignored alongside the native executables: a page, its loader and the module are output, not source.
This commit is contained in:
parent
bdb3f105f2
commit
81d46c342e
8
.gitignore
vendored
8
.gitignore
vendored
@ -42,6 +42,14 @@ old-ocaml/
|
||||
/calc-me
|
||||
/sand
|
||||
/conditions-play
|
||||
|
||||
# What `flan build --target=web` drops beside a source file. Anchored like the
|
||||
# native executables above, and three patterns rather than one glob because the
|
||||
# page, its loader and the module are three files with three extensions the
|
||||
# repo otherwise has legitimate uses for.
|
||||
/sand.html
|
||||
/sand.js
|
||||
/sand.wasm
|
||||
.claude/
|
||||
probe
|
||||
probe.c
|
||||
|
||||
@ -355,3 +355,37 @@ Commands with no key: `M-x flan-dev` (start a program), `M-x flan-dev-quit`
|
||||
|
||||
There is no Flan parser in any of them. The client sends text and the compiler
|
||||
answers; anything that needs to know what a form means asks.
|
||||
|
||||
## The stack, and what a frame was holding
|
||||
|
||||
`C-c C-b` opens the conditions-and-restarts buffer, and its Stack section is no
|
||||
longer empty. It lists the stopped program's frames, innermost first, each with
|
||||
where it is and how many named slots it has.
|
||||
|
||||
`TAB` on a frame opens it and shows what its locals hold — name, type and
|
||||
value, rendered the same way the inspector renders anything else. They are
|
||||
fetched the first time a frame is opened and then kept, because a stopped
|
||||
program's locals cannot change underneath you and a round trip behind a key
|
||||
that looks like folding would be a surprise.
|
||||
|
||||
Two kinds of frame are marked. A `program` frame was on the stack when the
|
||||
error happened. An `eval` frame belongs to an expression you evaluated *inside*
|
||||
the break loop, sitting on top of them. Those are shown rather than hidden, on
|
||||
the same principle the unreachable restarts are: a frame you did not write is
|
||||
better explained than silently removed.
|
||||
|
||||
Not everything can be shown, and what cannot is refused by name under the frame
|
||||
rather than left blank:
|
||||
|
||||
- a slot the compiler invented, which has no name in your source — showing it
|
||||
as `s4` would put a variable in front of you that is not in the file;
|
||||
- a slot whose binding had not run yet when the error happened, which has no
|
||||
address to read;
|
||||
- a `Vec` or a pointer, which render as `<vec>` and `<ptr>` here exactly as
|
||||
they do everywhere else.
|
||||
|
||||
**One known wrong answer.** If a function's body is redefined while the program
|
||||
is stopped inside it, and the new body happens to have the same number of slots
|
||||
of the same types, the frame will show the *new* names against the *old*
|
||||
values. The check that should catch this does not fire. There is a failing test
|
||||
pinned to it, so this is recorded rather than lurking.
|
||||
|
||||
@ -78,9 +78,9 @@ from fixtures, and so `flan-dev.el' is named in one place.")
|
||||
(params
|
||||
. "restart arguments are checked at run time against a frame that does not record its arity [needs a field in the restart frame]")
|
||||
(stack
|
||||
. "nothing here is attached to the stopped program. DWARF is not the gap and has not been for a while: `flan build --debug' emits it, `flan dev --debug' now builds the host and every redefinition module with it, and lldb walks a stack that crosses from a reloaded .so back into the host naming both sides' .flan files. But this buffer reaches the program over the daemon's socket, and a socket cannot read another process's frames — the break loop stopped itself, it is not being debugged. So this wants either an unwinder in the agent, beside `flan_rt.c', or lldb attached to the same pid and this buffer reading it [needs one of those two, not DWARF]")
|
||||
. "the program is running. A backtrace is the game thread's frame chain and it is changing while it runs, so the daemon refuses to take one [not a missing feature: ask again once it has stopped]")
|
||||
(locals
|
||||
. "same reason as the stack above it, plus a renderer aimed at an address rather than at an expression. The names and types are in the DWARF now — a let-bound local is its own name there, not `s0' — so whatever walks the frames can read them; nothing is walking the frames [needs the same attachment, and the same thunk the condition's fields need]"))
|
||||
. "this frame has no named locals the daemon can read. A slot the compiler invented is refused by name rather than shown as `s4', and a slot whose binding had not run when the error happened has no address yet [both are refusals with reasons, not gaps — the frame's own line says how many slots it has]"))
|
||||
"Why a section of this buffer is empty, by name.")
|
||||
|
||||
(defun flan-cnr--why (key)
|
||||
@ -224,8 +224,15 @@ list already says it."
|
||||
;; one level down.
|
||||
(when (memq i flan-cnr--open)
|
||||
(let ((locals (plist-get fr :locals)))
|
||||
(dolist (r (plist-get fr :refused))
|
||||
(insert (format " %s%s\n"
|
||||
(if (string-empty-p (nth 0 r)) ""
|
||||
(propertize (format "%s: " (nth 0 r))
|
||||
'face 'shadow))
|
||||
(propertize (nth 1 r) 'face 'shadow))))
|
||||
(if (null locals)
|
||||
(insert (flan-cnr--unavailable 'locals))
|
||||
(unless (plist-get fr :refused)
|
||||
(insert (flan-cnr--unavailable 'locals)))
|
||||
(dolist (l locals)
|
||||
(let ((start (point)))
|
||||
(insert (format " %s %s = %s\n"
|
||||
@ -314,6 +321,17 @@ list already says it."
|
||||
(setq flan-cnr--open
|
||||
(if (memq i flan-cnr--open) (delq i flan-cnr--open)
|
||||
(cons i flan-cnr--open)))
|
||||
;; Fetched on first open and then kept: a frame's locals cannot change
|
||||
;; while the program is stopped, and re-asking on every fold would put a
|
||||
;; round trip behind a keystroke that looks like pure redrawing.
|
||||
(when (memq i flan-cnr--open)
|
||||
(let ((fr (nth i (plist-get flan-cnr--state :stack))))
|
||||
(when (and fr (not (plist-get fr :fetched)))
|
||||
(let ((answer (flan-cnr-locals i)))
|
||||
(setcar (nthcdr i (plist-get flan-cnr--state :stack))
|
||||
(plist-put (plist-put (plist-put fr :locals (nth 0 answer))
|
||||
:refused (nth 1 answer))
|
||||
:fetched t))))))
|
||||
(let ((line (line-number-at-pos)))
|
||||
(flan-cnr--render flan-cnr--state)
|
||||
(goto-char (point-min))
|
||||
@ -382,7 +400,7 @@ list already says it."
|
||||
"What a stopped Flan program is offering."
|
||||
(setq buffer-read-only t))
|
||||
|
||||
(defun flan-cnr-state-from-reply (reply &optional fields)
|
||||
(defun flan-cnr-state-from-reply (reply &optional fields stack)
|
||||
"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
|
||||
@ -397,9 +415,11 @@ data and the fixture-driven tests can drive it without a socket."
|
||||
(list :condition (plist-get reply :condition)
|
||||
:restarts (plist-get reply :restarts)
|
||||
:fields fields
|
||||
;; Still nil deliberately: nothing here is attached to the stopped
|
||||
;; program's frames, and the renderer says so by name.
|
||||
:stack nil
|
||||
;; STACK is passed in rather than fetched, for the same reason FIELDS
|
||||
;; is: this stays a function from data to data, so the fixture tests
|
||||
;; drive it with no socket. Nil is still a legitimate answer — the
|
||||
;; renderer draws the reason rather than leaving the section out.
|
||||
:stack stack
|
||||
:locals nil))
|
||||
|
||||
(defun flan-cnr-layout (type)
|
||||
@ -427,6 +447,47 @@ it is empty, which is the degradation the buffer is already built for."
|
||||
(list (nth 0 f) (nth 1 f) nil))
|
||||
(plist-get r :fields))))))
|
||||
|
||||
(defun flan-cnr-backtrace ()
|
||||
"The stopped program's frames, innermost first, as the renderer wants them.
|
||||
|
||||
Unlike `flan-cnr-layout', this *does* reach the program: the frame chain is the
|
||||
shadow stack the dev build pushes, and only the stopped thread can read it. It
|
||||
is taken from a snapshot made when the break was entered, for the same reason
|
||||
the restart list is — a stack that is still moving cannot be numbered.
|
||||
|
||||
Frames are marked `program' or `eval'. An `eval' frame belongs to an
|
||||
expression evaluated *inside* the break loop, sitting on top of the frames that
|
||||
were there when it stopped; it is shown rather than hidden, because \"why is
|
||||
there a frame I did not write\" is a fair question and silence is the wrong
|
||||
answer to it.
|
||||
|
||||
Locals are left nil here and fetched per frame when one is opened. A stopped
|
||||
program has as many sets of locals as it has frames, and rendering all of them
|
||||
to draw one is the backtrace problem one level down."
|
||||
(let ((r (funcall flan-cnr-request-function '(:op "backtrace"))))
|
||||
(when (equal (plist-get r :status) "ok")
|
||||
(mapcar (lambda (f)
|
||||
(list :fn (nth 0 f) :loc (nth 1 f)
|
||||
:kind (nth 2 f) :nslots (nth 3 f)
|
||||
:locals nil :fetched nil))
|
||||
(plist-get r :frames)))))
|
||||
|
||||
(defun flan-cnr-locals (frame)
|
||||
"What FRAME's named locals hold, as (NAME TYPE VALUE) rows.
|
||||
|
||||
Returns two values in one list: the rows, and the refusals — a slot the daemon
|
||||
declined to show, with the reason. The refusals are shown, not dropped: a
|
||||
local that is missing because the compiler invented it and a local that is
|
||||
missing because this is broken look identical if only one of them is drawn.
|
||||
|
||||
Nothing is copied out of the program. The daemon compiles a thunk that renders
|
||||
the types it already knows at the addresses the program hands back, which is
|
||||
why this works at all while the thread is stopped."
|
||||
(let ((r (funcall flan-cnr-request-function (list :op "locals" :frame frame))))
|
||||
(if (equal (plist-get r :status) "ok")
|
||||
(list (plist-get r :locals) (plist-get r :refused))
|
||||
(list nil (list (list "" (or (plist-get r :message) "refused")))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun flan-cnr-show ()
|
||||
"Show what the stopped program is offering, in a buffer.
|
||||
@ -442,7 +503,9 @@ walk from a running program."
|
||||
(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-layout (plist-get r :condition)))))
|
||||
(flan-cnr-state-from-reply r
|
||||
(flan-cnr-layout (plist-get r :condition))
|
||||
(flan-cnr-backtrace))))
|
||||
(pop-to-buffer buf)
|
||||
buf)))
|
||||
|
||||
|
||||
@ -317,8 +317,12 @@
|
||||
;; refusals, and with nothing at all the outer one is what shows.
|
||||
(test-flan--check "the condition's fields are refused, not omitted"
|
||||
(string-match-p "Condition fields:\n not available.*Tast.structs" text))
|
||||
(test-flan--check "the stack is refused, naming DWARF"
|
||||
(string-match-p "Stack.*\n not available.*DWARF"
|
||||
;; The stack is no longer refused for want of a mechanism — the shadow stack
|
||||
;; landed and the daemon answers `backtrace'. What this fixture has is no
|
||||
;; stack *passed in*, which is the running-program case, so the reason it
|
||||
;; names is that one.
|
||||
(test-flan--check "the stack says the program is running, not that it is unbuilt"
|
||||
(string-match-p "Stack.*\n not available.*running"
|
||||
(substring text (string-match "--- Stack" text))))
|
||||
(test-flan--check "and the keys are shown" (string-match-p "TAB fold a frame" text)))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user