An inspector and a conditions buffer, and a restart nobody could take

Two CIDER-shaped buffers: C-c C-i navigates a value, C-c C-b shows the
condition, the restarts and the stack. C-c C-M-b keeps the old one-key prompt.

The inspector needs no protocol change at all. eval-expr already answers a
rendered string, and Session.render writes exactly seven shapes, so that string
is a grammar. Navigation is a stack of *expressions* rather than of handles:
going into :pos sends (.pos b), into element 2 sends (at (.tags b) 2) - both
ordinary Flan a person could type. CIDER keeps its stack on the server because
a JVM value can be retained; nothing here can, since a Flan value has no header
and the render thunk is dlclosed the moment it returns. The view is therefore
never stale, where CIDER's shows the object as it was when you pushed. What it
buys over C-x C-e is the depth-4 span-8 bound: a field past it comes back as
... and nothing recovers it from the echo area, and re-rooting renders it from
depth 0.

SBCL decided the order - condition, restarts, then stack - because invoke-debugger
prints the condition and show-restarts and then stops; the backtrace is a command
you type. The restarts are the decision and the stack is the explanation for it.

And SBCL found a live bug. show-restarts omits the bracket on a name already
used further in, which is not decoration: §4 takes the first frame offering a
name, so a second frame offering retry is real, is on the list, and cannot be
chosen. The old prompt showed retry twice and sent the string either way, and
the inner frame took it silently. restarts.flan's own nested function has been
that counterexample since the transfer landed. The buffer draws the shadowed row
unbracketed and refuses RET on it by name, with a test asserting nothing was
sent - which stops the lie without restoring the choice. Taking a restart by
index is the fix and is recorded as such.

Sections that cannot be filled are drawn saying why rather than omitted: a
missing section cannot be told from an empty one, and only one of those is a
fact about the program.
This commit is contained in:
Joseph Ferano 2026-09-12 04:17:41 +07:00
parent 13e8002d39
commit 6ed413a232
2 changed files with 40 additions and 1 deletions

27
NEXT.md
View File

@ -477,6 +477,17 @@ vague intention — if it is listed, someone has already established it is real.
### Bugs found and not yet fixed
- **A shadowed restart is offered and cannot be taken.** §4 says lookup takes the *first* frame offering a name, and
`flan_find_restart` does exactly that — so when two frames offer `retry`, the second one is real, is on the list the
break loop prints, and is unreachable. The old `C-c C-b` prompt showed `retry` twice and sent the string either way;
the inner frame took it, silently. `test/programs/restarts.flan`'s own `nested` function is the counterexample, and
it has been there since the transfer landed. The C&R buffer now draws the shadowed row unbracketed and refuses
`RET` on it by name, which stops the lie but does not restore the choice. The fix is to take a restart **by index**
rather than by name — SBCL identifies them positionally for precisely this reason:
- `(:op "restart-at" :index N)` on the daemon, and a matching agent verb. `flan_restart_name(i, &len)` already
walks the stack by index, so resuming by index is that loop plus `flan_break_resume`'s tail. C only, no compiler
work.
- **A restart chosen at a break inside a thunk is accepted, announced, and silently not taken.** Demonstrated.
`flan_reload_call` allocates its own `xfer` and discards it on return, so a transfer aimed at a frame below the
`flan_agent_poll` C frame unwinds only as far as the thunk. Three statements that the program will resume, none
@ -509,6 +520,22 @@ Sixty mutations, nineteen left the whole suite green. The severe cluster is clos
- And a warning: a reader mutation makes the suite **hang** rather than fail. A green run is not the only outcome to
plan for in CI.
### Asked for by the editor lanes
- **`(:op "layout" :type T)` → the struct's fields and their types.** `Tast.structs` is held by the daemon at all
times because it owns the build, and **no running program is involved** — this is the cheapest real win on the
list, and the C&R buffer already draws its result.
- **`(:op "condition")` → the stopped program's condition, rendered.** Two steps: `break_loop` currently does
`(void)condition;` and *discards the pointer*, so stash it beside `condition_name`; then the daemon builds a render
thunk aimed at that address, which is `Session.render` rooted at a `Ptr` instead of an expression.
- **One thing to get right for both:** the type must be an identity the daemon can resolve to a `Tast` type, not a
bare class name. The hook is handed a string, and two packages both declaring `Missing` leave the daemon unable to
pick a layout. A qualified name or a type id. The same wrinkle bites locals later, because DWARF also gives a name.
- **`(:op "backtrace")` is blocked** on frame metadata — unlocked by the DWARF work, then a new agent verb. Locals are
blocked twice: DWARF for the frame layout, *and* the pointer-rooted render thunk. Restart source locations and
arity are blocked too — `flan_restart` carries `prev`, `name_id`, `name` and `namelen`, so both need a new field in
the frame, which means the compiler emitting it.
### One line away
- **`match` over enums.** Fully desugarable, wanted, and blocked only by `Ast.pattern` needing a keyword case, which

View File

@ -24,6 +24,11 @@
(autoload 'flan-show-output "flan-dev" nil t)
(autoload 'flan-repl "flan-repl" nil t)
(autoload 'flan-break "flan-dev" nil t)
;; The two CIDER-shaped buffers. They reach the daemon through an indirection
;; of their own so that fixtures can drive them, so autoloading is all the
;; wiring they need.
(autoload 'flan-inspect "flan-inspect" nil t)
(autoload 'flan-cnr-show "flan-cnr" nil t)
(autoload 'flan-doc "flan-dev" nil t)
(autoload 'flan-dev "flan-dev" nil t)
(autoload 'flan-dev-quit "flan-dev" nil t)
@ -121,7 +126,14 @@ line is off screen."
(define-key map (kbd "C-c C-d") #'flan-describe)
(define-key map (kbd "C-c C-o") #'flan-show-output)
(define-key map (kbd "C-c C-r") #'flan-repl)
(define-key map (kbd "C-c C-b") #'flan-break)
;; C-c C-b opens the buffer rather than the minibuffer prompt: it shows the
;; same restarts plus the condition and the stack, and — the reason it
;; replaces rather than joins — it is the one that refuses a *shadowed*
;; restart instead of silently invoking a different frame's. flan-break is
;; still there under C-c C-M-b for the one-key path.
(define-key map (kbd "C-c C-b") #'flan-cnr-show)
(define-key map (kbd "C-c C-M-b") #'flan-break)
(define-key map (kbd "C-c C-i") #'flan-inspect)
;; Help on the name at point. C-c C-d is taken by `flan-describe', which
;; is about the session rather than about a name, and renaming a key that
;; is already documented costs more than it is worth. Not C-c C-h either: