Merge branch 'worktree-agent-a684a973597966330' into dev-loop

# Conflicts:
#	FIX.org
This commit is contained in:
Joseph Ferano 2026-09-20 21:16:48 +07:00
commit 70b999f8f4
7 changed files with 410 additions and 84 deletions

35
FIX.org
View File

@ -3789,3 +3789,38 @@ spec-memory.md's Generics section gained the three rules this lane decided —
the literal under ~numeric?~, the widening boundary, and dyn — because the
spike banner names that section and plan.org's Types as the current account,
and all three are observable from a program.
* The Emacs buffer story, consolidated, 2026-09-20
The decision: two streams, one tool list, and the rest untouched.
- Two streams. ~*flan*~ (renamed from ~*flan-dev*~) is the daemon's log —
compilation-minor-mode, jump-to-error, and now a mirror of the program's
println output, so output lands somewhere before any REPL interaction has
happened. ~*flan-repl*~ is the working stream: eval results, the program's
output inserted above the prompt (output first, then the value), and a
one-line summary when a compile fails — "1 error — see ~*flan-diagnostics*~".
- One tool list. ~*flan-diagnostics*~ holds everything the compiler reports:
the errors as today, popped up (shown, not selected) when one lands, and
below them the memory-allocation sites ~flan-check-memory~ asks the
~(:op "memory")~ op for — one section, replaced whole on every ask, each
line in its kind's faint face (memory/gc, memory/native). The buffer got a
major mode: read-only, ~n~/~p~/~RET~ throughout both sections.
- ~*flan-output*~ is removed entirely, its ~C-c C-o~ with it. The key now
clears the REPL's last send; ~C-c M-o~ clears the transcript whole
(CIDER's pair), both bound in flan-mode and flan-repl-mode.
- Inspector and break/conditions buffers unchanged.
No daemon changes: the program's output already rides every reply's
~:output~, so both destinations are editor-side routing in
~flan--append-output~. A REPL rejection is distinguished from a connection
failure (~:client~ on the synthetic reply); only the compiler's messages
reach the diagnostics list.
** What was run
~dune test --root .~ green, test_emacs and test_cider included; the three
changed .el files byte-compile clean with warnings as errors. New checks in
test-flan.el: output reaches the daemon buffer with no REPL open, output
lands above the value at the REPL and is mirrored, the rejection summary and
its full message in the diagnostics list, both clears, and the two-section
layout (errors above, memory below, replace-whole, clear takes both).

View File

@ -1540,7 +1540,7 @@ request and `read` reads a reply.
| `C-c C-k` | the whole buffer, as **one** module |
| `C-x C-e` | the expression before point, evaluated *in the running program* |
| `C-c C-z` / `C-c C-q` | connect (finds `.flan-dev.sock` upward) / disconnect |
| `C-c C-o` | the running program's own output, in `*flan-output*` |
| `C-c C-o` | clear what the REPL's last send produced (`C-c M-o`: the whole transcript) |
| `C-c C-r` | a prompt on the running program (`*flan-repl*`) |
| `C-c C-b` | what a **stopped** program is offering, and which to take |
| `C-c C-d` | what the running program currently defines |
@ -1617,7 +1617,7 @@ same call the command loop makes — and checks the hook is installed in that bu
is Emacs' contract and a test claiming to check it would be checking nothing.
**The program's stdout is a pipe into the daemon**, and whatever it printed since the last reply rides along with the
next one into `*flan-output*`. Having it arrive *with* a reply rather than by a separate request is the point: the
next one into `*flan*` — and into `*flan-repl*` when a prompt is open. Having it arrive *with* a reply rather than by a separate request is the point: the
output an evaluation itself caused is the output anyone wants to see. Draining that pipe is a liveness requirement and
not a nicety — a pipe nobody reads fills at 64K and the next write blocks the program forever — so it is read from the
accept loop's `select`, not only when an editor asks, and the buffer is capped so a program printing every frame cannot
@ -1731,8 +1731,8 @@ because the file says which package it belongs to; a prompt has no file and noth
paren inside a string does not count.
- **A value and the program's output are different things and arrive by different routes.** The value is the result of
the request and appears at the prompt; anything the program printed while evaluating it rides along on the same reply
and goes to `*flan-output*`. Showing them in one place would be convenient and wrong, so there is a test for the
separation.
and is inserted above the value — output first, then the value, the way a terminal REPL reads — with `*flan*` mirroring
the output. There is a test for the ordering.
That test is what caught a real bug: the renderer's unit case emitted `()` without evaluating the expression, so
`(println "x")` — the most ordinary thing anyone types at a prompt — answered `()` while nothing happened. A `()`

View File

@ -54,7 +54,7 @@ You also need the `flan` binary on your `PATH`. If it is somewhere else, set
Two settings worth knowing about before you need them. `flan-start-timeout`
(60s) bounds the wait for a daemon to come up, and `flan-reply-timeout`
(30s) bounds one request once it has. The second is the one a long first
compile can exhaust: the message says so and names `*flan-dev*`, where the
compile can exhaust: the message says so and names `*flan*`, where the
daemon's own build log is, so you can see whether it is still working before
you raise it.
@ -132,7 +132,7 @@ socket reaches `flan dev` from Emacs:
(setq flan-daemon-args '("--debug")) ; breakpoints, through flan-dape
```
The first line of `*flan-dev*` is the command that actually ran, so you can
The first line of `*flan*` is the command that actually ran, so you can
always check what a session was started with.
There is no prefix argument for this and that is deliberate: `C-u M-x flan`
@ -292,9 +292,17 @@ One thing to know: it is **program-scoped**, not buffer-scoped. Names are the
running program's names. In sand you write `sim/settle`, not `settle`, because
that is what the program calls it.
**`C-c C-o`** shows `*flan-output*` — whatever the program itself has printed.
That is separate from the REPL, because the program's stdout belongs to the
program.
**Output lands here too.** Whatever the program printed while evaluating your
form rides along on the reply and is inserted above the prompt — output
first, then the value, the way a terminal REPL reads. When a compile fails
the prompt gets one line, `1 error — see *flan-diagnostics*`, and the message
itself is in that buffer, which pops up. The daemon's buffer `*flan*` mirrors
all program output, so a println is never lost when no prompt is open.
**Two clears.** `C-c C-o` removes what the last send produced — output, value
or error line — and leaves the transcript. `C-c M-o` erases the transcript
whole and leaves a fresh prompt. Both work from a `.flan` buffer as well as
from the prompt, and neither touches the input history.
**History.** `<up>` and `<down>` walk it while you are on the line you are
typing, filtered by whatever you have typed so far — `(sim` then `<up>` reaches
@ -597,6 +605,12 @@ They last until you edit the buffer — they are a reading of the source, not
feedback about an evaluation, so moving point through them leaves them alone.
Asking again while they are up takes them down.
**The full list goes to `*flan-diagnostics*`** as its own section, below
whatever errors are logged there — every site, the ones in files you have
open and the ones elsewhere, one `file:line:col: message` line each in the
kind's face. `n`, `p` and `RET` navigate it like the errors above it. Asking
again replaces the section; the errors above it stay.
#### The same thing from the command line, and flycheck
`flan check FILE --warn-memory` prints the same list in the standard
@ -992,16 +1006,22 @@ the thing rather than when you dismiss it.
**And it is kept.** Every compiler message the editor is handed is also appended
to `*flan-diagnostics*`, one entry per message with the time it arrived above
it: `M-x flan-show-diagnostics`. The overlay is feedback about one command and
it, and the buffer pops up when one lands — shown, not selected, so your
typing stays where it was. The overlay is feedback about one command and
is gone at the next keystroke; this is the copy you can still quote an hour
later, without going through `*Messages*` for it. Nothing clears it — not
connecting, not a successful evaluation, not quitting the program — until you
say `M-x flan-clear-diagnostics`. Each entry's message line is left in the shape
the compiler wrote it, `file:line:col: message`, so `RET` and `M-g M-n` on one
go to the code and a line yanked out of the buffer reads the same as one pasted
from a terminal.
the compiler wrote it, `file:line:col: message`, so `RET`, `n` and `p` on one
go to or between the code lines it names, and a line yanked out of the buffer
reads the same as one pasted from a terminal. `M-x flan-show-diagnostics`
brings it back by hand.
**A build that failed is in `*flan-dev*`**, and it jumps too: the buffer is in
**It is the one list for everything the compiler reports.** The errors are
the log above; the allocation sites `M-x flan-check-memory` asks for are one
section below them, replaced whole on every ask, navigable the same way.
**A build that failed is in `*flan*`**, and it jumps too: the buffer is in
`compilation-minor-mode`, so `M-g M-n`, `M-g M-p` and `RET` on a diagnostic take
you to the line it names. That is the buffer to read when `M-x flan` reports a
daemon that exited before it was ready — a `main` that does not compile means no
@ -1035,7 +1055,8 @@ Use `C-c C-g` if you need frames.
| `C-u C-x C-e` | ...and stop at it instead of showing its value |
| `C-c C-z` | connect (finds `.flan-dev.sock` upward) |
| `C-c C-q` | disconnect |
| `C-c C-o` | the running program's own output |
| `C-c C-o` | clear what the REPL's last send produced |
| `C-c M-o` | clear the REPL transcript whole |
| `C-c C-r` | a prompt on the running program |
| `C-c C-b` | a stopped program: condition, restarts, stack |
| `C-c C-M-b` | the same restarts, as a one-key prompt |
@ -1074,10 +1095,9 @@ in the buffer).
| `flan-echo-result` | `t` | report an accepted evaluation in the echo area |
| `flan-inline-result` | `t` | also show an expression's value at the end of its line |
| `flan-names-shown` | `4` | how many names to list before summarising |
| `flan-output-buffer` | `"*flan-output*"` | where the program's output goes |
| `flan-poll-interval` | `1.0` | seconds between checks for whether it stopped |
| `flan-daemon-buffer` | `"*flan-dev*"` | the daemon's own log |
| `flan-diagnostics-buffer` | `"*flan-diagnostics*"` | every compiler message, kept |
| `flan-daemon-buffer` | `"*flan*"` | the daemon's own log; mirrors program output |
| `flan-diagnostics-buffer` | `"*flan-diagnostics*"` | everything the compiler reports, kept |
| `flan-start-timeout` | `60` | seconds to wait for a program to come up |
| `flan-lower-buffer` | `"*flan-lowering*"` | where `C-c C-l` writes |
| `flan-lower-program` | `"flan"` | the compiler `C-c C-l` shells out to |

View File

@ -78,8 +78,9 @@
(autoload 'flan-connect "flan" nil t)
(autoload 'flan-disconnect "flan" nil t)
(autoload 'flan-describe "flan" nil t)
(autoload 'flan-show-output "flan" nil t)
(autoload 'flan-repl "flan-repl" nil t)
(autoload 'flan-repl-clear "flan-repl" nil t)
(autoload 'flan-repl-clear-output "flan-repl" nil t)
(autoload 'flan-break "flan" 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
@ -317,7 +318,12 @@ For `syntax-propertize-function'."
(define-key map (kbd "C-c C-z") #'flan-connect)
(define-key map (kbd "C-c C-q") #'flan-disconnect)
(define-key map (kbd "C-c C-d") #'flan-describe)
(define-key map (kbd "C-c C-o") #'flan-show-output)
;; The REPL's two clears, reachable from the file being edited: results
;; land in *flan-repl* whichever buffer the send came from, so the keys
;; that take them down belong here too. CIDER's pair: C-c C-o for the
;; last send's output, C-c M-o for the whole transcript.
(define-key map (kbd "C-c C-o") #'flan-repl-clear-output)
(define-key map (kbd "C-c M-o") #'flan-repl-clear)
(define-key map (kbd "C-c C-r") #'flan-repl)
;; 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

View File

@ -37,11 +37,17 @@
;; for it, because the file says which package it belongs to; a prompt has no
;; file and nothing to derive it from.
;;
;; - **A value and the program's output are different things** and arrive by
;; different routes. The value of the expression appears at the prompt;
;; anything the program printed while evaluating it goes to *flan-output*,
;; riding along on the same reply. Showing them in one place would be
;; convenient and wrong.
;; - **A value and the program's output are different things**, and both are
;; here. The value of the expression appears at the prompt; anything the
;; program printed while evaluating it — riding along on the same reply —
;; is inserted above it, output first, then the value, the way a terminal
;; REPL reads. The daemon's buffer (*flan*) mirrors the output, so it is
;; still somewhere when no prompt is open.
;;
;; When a compile fails, the prompt gets one line — "1 error — see
;; *flan-diagnostics*" — and the message itself goes to the diagnostics
;; list, which pops up. The full Elm-style error is worth a buffer with
;; navigation; the prompt is not that buffer.
;;; Code:
@ -83,7 +89,10 @@ a buffer-local value is not visible.")
(defvar flan-repl-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-o") #'flan-show-output)
;; The two clears, on CIDER's keys: C-c C-o takes down what the last send
;; printed, C-c M-o takes the whole transcript.
(define-key map (kbd "C-c C-o") #'flan-repl-clear-output)
(define-key map (kbd "C-c M-o") #'flan-repl-clear)
(define-key map (kbd "C-c C-d") #'flan-describe)
(define-key map (kbd "C-c C-q") #'flan-disconnect)
(define-key map (kbd "<up>") #'flan-repl-previous-input)
@ -243,6 +252,70 @@ should open a line, not send something the reader will reject."
(let ((proc (get-buffer-process (current-buffer))))
(comint-output-filter proc (concat text "\n" flan-repl-prompt))))
(defun flan-repl--insert-output (text)
"Insert TEXT, the program's own output, before the prompt.
Two moments call this and the same rule serves both. During a send there
is no prompt yet the process mark sits at the end of the input just sent
so the text goes at the mark, before the value and the prompt that follow.
Between sends the mark sits after the prompt, and the text goes above the
prompt's line, so the prompt and whatever is being typed at it do not move."
(let ((proc (get-buffer-process (current-buffer))))
(when (process-live-p proc)
(let ((mark (process-mark proc))
(text (if (string-suffix-p "\n" text) text (concat text "\n")))
(inhibit-read-only t))
(save-excursion
(goto-char mark)
(forward-line 0)
(let ((at (point)))
(insert text)
;; Inserting at the mark itself leaves the mark *before* the
;; text — a plain marker does not advance — and the value the
;; reply carries would then land above the output it caused.
(when (>= at (marker-position mark))
(set-marker mark (point)))))))))
(defun flan-repl--buffer ()
"The REPL buffer, for the two clear commands, from wherever they are run."
(let ((buf (if (derived-mode-p 'flan-repl-mode)
(current-buffer)
(get-buffer flan-repl-buffer))))
(unless (buffer-live-p buf)
(user-error "flan: no REPL buffer; C-c C-r opens one"))
buf))
;;;###autoload
(defun flan-repl-clear ()
"Erase the whole REPL transcript and leave a fresh prompt.
The input history is untouched: it is the screen that goes, not the ring."
(interactive)
(with-current-buffer (flan-repl--buffer)
(let ((proc (get-buffer-process (current-buffer)))
(inhibit-read-only t))
(erase-buffer)
(when (process-live-p proc)
(set-marker (process-mark proc) (point-max))
(comint-output-filter proc flan-repl-prompt)))))
;;;###autoload
(defun flan-repl-clear-output ()
"Erase what the last send produced — output, value or error line.
The input that produced it stays, and so does the prompt; this is for a
send whose output buried the transcript, not for starting over that is
`flan-repl-clear'."
(interactive)
(with-current-buffer (flan-repl--buffer)
(let* ((proc (get-buffer-process (current-buffer)))
(mark (and (process-live-p proc) (process-mark proc)))
(start (and (marker-position comint-last-input-end)
(marker-position comint-last-input-end)))
(end (and mark (save-excursion (goto-char mark)
(forward-line 0) (point))))
(inhibit-read-only t))
(if (not (and start end (< start end)))
(message "flan: nothing to clear")
(delete-region start end)))))
(defun flan-repl--send (_proc text)
"Evaluate TEXT in the running program and show what it was."
(let ((code (string-trim text)))
@ -252,14 +325,27 @@ should open a line, not send something the reader will reject."
(let ((reply (condition-case err
(flan--request
(list :op "eval-expr" :code code :file "<repl>"))
(error (list :status "error"
;; :client marks a failure of the *connection* — no
;; daemon, no reply — which is not a compiler message
;; and has no place in the diagnostics list.
(error (list :status "error" :client t
:message (error-message-string err))))))
(flan-repl--output
(if (equal (plist-get reply :status) "ok")
(or (plist-get reply :value) "")
(concat "error: " (or (plist-get reply :message) "rejected")
(let ((loc (plist-get reply :loc)))
(if loc (concat " (" loc ")") ""))))))))))
(cond
((equal (plist-get reply :status) "ok")
(flan-repl--output (or (plist-get reply :value) "")))
((plist-get reply :client)
(flan-repl--output
(concat "error: " (or (plist-get reply :message) "rejected"))))
(t
;; A rejection. The message goes where every compiler message
;; goes — the diagnostics list, which `flan--record-diagnostic'
;; shows — and the prompt gets the one-line pointer.
(ignore-errors
(flan--record-diagnostic (plist-get reply :loc)
(or (plist-get reply :message)
"rejected")))
(flan-repl--output
(format "1 error — see %s" flan-diagnostics-buffer)))))))))
(defun flan-repl-return ()
"Send the input if it is a whole form, otherwise open a line."

View File

@ -116,17 +116,17 @@ could be drawn at all, as at the REPL."
"How many installed names to name before falling back to counting them."
:type 'integer)
(defcustom flan-output-buffer "*flan-output*"
"Buffer the running program's own output is appended to."
:type 'string)
(defcustom flan-daemon-buffer "*flan-dev*"
(defcustom flan-daemon-buffer "*flan*"
"Buffer the daemon's own output goes to.
This is where a build that failed says so: the daemon compiles the program
before it binds its socket, so a program that does not compile produces no
socket at all and this buffer is the only account of why. It is also where a
build that is merely slow can be watched, which is what the reply timeout
below points at."
below points at.
The program's own output lands here too, mirrored from wherever else it
goes: a REPL shows it when one is open, and this buffer is where it is
regardless, so a println is never lost for want of a prompt."
:type 'string)
(defcustom flan-diagnostics-buffer "*flan-diagnostics*"
@ -138,7 +138,11 @@ want to keep: to quote one somewhere else you had to find it again in
*Messages*, among everything else Emacs had said since. This is the other
half one entry per message, in the order they arrived, never cleared by
anything but you. `M-x flan-clear-diagnostics' when it has grown past being
useful."
useful.
Everything the compiler reports is in here: the errors, and below them the
allocation sites `flan-check-memory' asks for, as one section replaced on
every ask."
:type 'string)
(defcustom flan-reply-timeout 30
@ -329,17 +333,36 @@ Reconnecting happens before a send, never after one."
(erase-buffer)
(flan--no-reply proc))))))
;; Written by `flan--append-output' when a REPL is open; defined in
;; flan-repl.el, which requires this file, so the reference here has to be a
;; declaration and a guard rather than a `require'.
(declare-function flan-repl--insert-output "flan-repl" (text))
(defun flan--repl-buffer ()
"The live REPL buffer, or nil when none has been opened."
(and (featurep 'flan-repl)
(bound-and-true-p flan-repl-buffer)
(get-buffer flan-repl-buffer)))
(defun flan--append-output (text)
"Append TEXT, the running program's own output, to its buffer."
"Append TEXT, the running program's own output, where it can be read.
Two places. The daemon's log always gets it, so output lands somewhere
before any REPL interaction has happened; the REPL shows it too when one is
open, above its prompt, which is where whoever is typing there is looking."
(when (and text (> (length text) 0))
(with-current-buffer (get-buffer-create flan-output-buffer)
(let ((at-end (= (point) (point-max))))
(with-current-buffer (get-buffer-create flan-daemon-buffer)
(let ((at-end (= (point) (point-max)))
(inhibit-read-only t))
(save-excursion
(goto-char (point-max))
(insert text))
;; Follow the tail only for someone who was already at it; a reader
;; scrolled back is reading something.
(when at-end (goto-char (point-max)))))))
(when at-end (goto-char (point-max)))))
(let ((repl (flan--repl-buffer)))
(when repl
(with-current-buffer repl
(flan-repl--insert-output text))))))
;; The break buffer, which this file shows but does not draw. An autoload
;; rather than a `require': flan-cnr.el reaches the daemon through this file,
@ -1228,13 +1251,6 @@ than being told so."
((null index) (user-error "flan: %s is not on the list" choice))
(t (flan-restart-at index (nth index restarts)))))))
;;;###autoload
(defun flan-show-output ()
"Show the running program's output, after collecting anything pending."
(interactive)
(ignore-errors (flan--request '(:op "describe")))
(display-buffer (get-buffer-create flan-output-buffer)))
(defun flan-describe ()
"Report what the running program currently defines."
(interactive)
@ -1383,28 +1399,67 @@ no longer wrong."
;; the compiler wrote it — `file:line:col: text' — which is what makes
;; `compilation-minor-mode' below able to jump from it, and also what makes a
;; line yanked out of here read the same as one pasted from a terminal.
;;
;; It is the one list for everything the compiler reports. Errors are the
;; log above, appended as they arrive; the allocation sites `flan-check-memory'
;; asks for are one section below them, replaced whole on every ask — a
;; reading of the source has no arrival order worth keeping, where the errors
;; do. The section boundary is `flan--diagnostics-memory-start', a marker
;; that advances past every error inserted at it, so the errors stay above
;; and the memory notes stay below whichever order the two arrive in.
(defvar flan-diagnostics-mode-map
(let ((map (make-sparse-keymap)))
;; The bare keys grep-mode and compilation-mode readers reach for. The
;; minor mode below provides RET, M-g M-n and M-g M-p; these two are the
;; major-mode half, possible because the buffer is read-only.
(define-key map (kbd "n") #'compilation-next-error)
(define-key map (kbd "p") #'compilation-previous-error)
map)
"Keymap for `flan-diagnostics-mode'.")
(define-derived-mode flan-diagnostics-mode special-mode "Flan-Diagnostics"
"Every message the compiler handed the editor, one navigable list.
`n' and `p' move between entries, RET goes to the line one names.
\\{flan-diagnostics-mode-map}"
;; Every entry names a file, a line and a column, so `next-error' and RET
;; should go there. The minor mode rather than deriving from
;; `compilation-mode', because this buffer is not the output of a command
;; that ran once.
(compilation-minor-mode 1))
(defvar-local flan--diagnostics-memory-start nil
"Marker at the start of the memory section, or nil while there is none.
Insertion type t, so an error inserted at it lands above the section and
the marker keeps naming the section's own first character.")
(defun flan--diagnostics-buffer ()
"The diagnostics buffer, made and set up if this is the first message."
(or (get-buffer flan-diagnostics-buffer)
(with-current-buffer (get-buffer-create flan-diagnostics-buffer)
;; The same mode the daemon's buffer gets, for the same reason: every
;; entry names a file, a line and a column, so `next-error' and RET
;; should go there. A minor mode rather than `compilation-mode',
;; because this buffer is not the output of a command that ran once.
(compilation-minor-mode 1)
(flan-diagnostics-mode)
(current-buffer))))
(defun flan--record-diagnostic (loc msg)
"Append MSG, which the daemon reported at LOC, to the diagnostics log.
LOC may be nil: a refusal the daemon could not place is still a message
somebody may want to keep, and it is written without one."
somebody may want to keep, and it is written without one.
The buffer is also shown. A rejection is the moment the list is for, and
it is shown rather than selected the fix happens in the buffer the error
came from, not here."
(when (and (stringp msg) (> (length msg) 0))
(with-current-buffer (flan--diagnostics-buffer)
(let ((at-end (= (point) (point-max)))
(inhibit-read-only t))
(save-excursion
(goto-char (point-max))
;; Above the memory section when there is one: errors are the log,
;; the memory notes are a standing reading, and the reading stays
;; at the bottom.
(goto-char (or (and flan--diagnostics-memory-start
(marker-position flan--diagnostics-memory-start))
(point-max)))
(unless (bolp) (insert "\n"))
(insert (format-time-string "── %H:%M:%S ──────────────────────\n"))
(insert (if (stringp loc) (format "%s: %s\n" loc msg)
@ -1413,7 +1468,53 @@ somebody may want to keep, and it is written without one."
;; `flan--append-output's rule, and the same one: follow the tail only
;; for somebody who was already at it, since a reader scrolled back is
;; reading something.
(when at-end (goto-char (point-max)))))))
(when at-end (goto-char (point-max))))
(display-buffer (current-buffer)))))
(defun flan--render-memory-sites (sites)
"Write SITES, the rows of an (:op \"memory\") reply, below the errors.
One section at the end of the diagnostics list, replaced whole every time:
the sites are a reading of the source as it stands, so a second reading
supersedes the first rather than joining it. Each line keeps the
`file:line:col: message' shape, so RET and `n'/`p' work on these exactly as
they do on the errors above, and the kind's face the same faint pair the
in-buffer underlines use is what tells the two heaps apart."
(with-current-buffer (flan--diagnostics-buffer)
(let ((inhibit-read-only t))
(save-excursion
(if (and flan--diagnostics-memory-start
(marker-position flan--diagnostics-memory-start))
(progn (goto-char flan--diagnostics-memory-start)
(delete-region (point) (point-max)))
(goto-char (point-max))
(unless (bolp) (insert "\n")))
(let ((start (point)))
(insert (format-time-string
"── memory, %H:%M:%S ──────────────────\n"))
(if (null sites)
(insert "nothing in this program allocates\n")
(dolist (row sites)
(let ((face (or (flan--memory-face (nth 1 row)) 'shadow)))
;; Both properties: `face' for a buffer font-lock has not
;; taken over, `font-lock-face' so refontification under
;; `compilation-minor-mode' does not strip the colour.
(insert (propertize (format "%s: %s" (nth 0 row) (nth 2 row))
'face face 'font-lock-face face)
"\n"))))
;; After the text, so the marker can advance past errors inserted
;; at it and still name this section's first character.
(setq flan--diagnostics-memory-start (copy-marker start t)))))
(display-buffer (current-buffer))))
(defun flan--repl-note-failure ()
"One line at the REPL saying the evaluation failed, when a REPL is open.
The full message is in the diagnostics list, which `flan--record-diagnostic'
has already shown; the prompt gets the pointer and nothing else."
(let ((repl (flan--repl-buffer)))
(when repl
(with-current-buffer repl
(flan-repl--insert-output
(format "1 error — see %s" flan-diagnostics-buffer))))))
;;;###autoload
(defun flan-show-diagnostics ()
@ -1423,13 +1524,16 @@ somebody may want to keep, and it is written without one."
;;;###autoload
(defun flan-clear-diagnostics ()
"Empty the diagnostics log.
"Empty the diagnostics log, the memory section with it.
Nothing else empties it not connecting, not an accepted evaluation, not
quitting the program which is the point of it."
(interactive)
(with-current-buffer (flan--diagnostics-buffer)
(let ((inhibit-read-only t))
(erase-buffer))))
(erase-buffer))
(when flan--diagnostics-memory-start
(set-marker flan--diagnostics-memory-start nil)
(setq flan--diagnostics-memory-start nil))))
(defun flan--show-error (loc msg)
"Mark MSG at LOC, if LOC names a file some buffer is visiting.
@ -2012,6 +2116,10 @@ has nothing to sit beside."
;; overlay needs a location and a buffer visiting the file, and the
;; message is worth keeping in either case.
(ignore-errors (flan--record-diagnostic loc (or msg "rejected")))
;; And said at the REPL, when one is open: whoever is working there
;; sees that the compile failed without turning to the echo area, and
;; the line names the buffer the message itself is in.
(ignore-errors (flan--repl-note-failure))
;; A refusal is not a value. `flan--show-error' clears the buffer it
;; marks, but a rejection the client cannot place — no `:loc', or a file
;; nobody is visiting — marks nothing, and leaving the last value up
@ -2990,7 +3098,11 @@ something is put in it; the block arrives at the first push, which is the
line that is marked.
The annotations last until you edit the buffer. Asking again while they are
up takes them down."
up takes them down.
The full list of sites the ones in files you have open and the ones
elsewhere goes to the diagnostics buffer as its own section, below
whatever errors are logged there, navigable like them."
(interactive)
(if (flan--marked-buffers)
(progn (mapc #'flan-clear-memory (flan--marked-buffers))
@ -3003,6 +3115,11 @@ up takes them down."
;; run — but another one may be, and painting over it would stack a
;; second copy with a second `help-echo'.
(mapc #'flan-clear-memory (flan--marked-buffers))
;; The whole list, into the diagnostics buffer, before the underlines
;; are drawn: the underlines reach only the files that are open, and
;; the list is where the rest of the answer is readable — and
;; navigable, since every line is a location RET can take.
(flan--render-memory-sites (plist-get r :sites))
(dolist (row (plist-get r :sites))
(if (flan--show-memory (nth 0 row) (nth 1 row) (nth 2 row))
(setq here (1+ here))

View File

@ -456,15 +456,16 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; The session is not poisoned by that: a good form still lands.
(flan--eval "(defn step [] i64 (set ticks (+ ticks 100)) ticks)" "form")
;; The program's own output arrives on replies and lands in its buffer, so
;; a long-running program is not writing into a terminal nobody is watching.
;; The program's own output arrives on replies and lands in the daemon's
;; buffer — no REPL is open yet, and the log is the fallback that makes a
;; println never depend on one.
(flan--eval "(defn step [] i64 (do (println \"HELLO\") ticks))" "form")
(let ((seen nil) (deadline (+ (float-time) 10)))
(while (and (not seen) (< (float-time) deadline))
(ignore-errors (flan--request '(:op "describe")))
(setq seen (with-current-buffer (get-buffer-create flan-output-buffer)
(setq seen (with-current-buffer (get-buffer-create flan-daemon-buffer)
(string-match-p "HELLO" (buffer-string)))))
(test-flan--check "the program's output reaches its buffer" seen))
(test-flan--check "the program's output reaches the daemon's buffer" seen))
;; ── Which evaluator C-x C-e reaches ──────────────────────────────────
;;
@ -804,9 +805,11 @@ already rely on it — so nothing here is a stand-in for the real thing."
(and said (string-match-p "\\_<ticks\\_>" said)
(not (string-match-p "=>" said)))))
;; The REPL buffer: typed input goes through the same eval-expr request, and
;; the value lands at the prompt while the program's own output goes to
;; *flan-output*. Conflating those two is the bug worth testing for.
;; The REPL buffer: typed input goes through the same eval-expr request.
;; The value lands at the prompt, the program's own output is inserted
;; above it — output first, then the value, the way a terminal REPL reads —
;; and a rejection leaves one line pointing at *flan-diagnostics*, where
;; the message itself is.
(test-flan--check "an incomplete form is not sent"
(not (flan-repl--complete-p "(+ 1")))
(test-flan--check "a whole form is sent" (flan-repl--complete-p "(+ 1 2)"))
@ -832,30 +835,89 @@ already rely on it — so nothing here is a stand-in for the real thing."
(insert "no-such-thing")
(flan-repl-return)
(let ((deadline (+ (float-time) 15)))
(while (and (not (string-match-p "unknown name" (buffer-string)))
(while (and (not (string-match-p "1 error" (buffer-string)))
(< (float-time) deadline))
(accept-process-output nil 0.05)))
(test-flan--check "the REPL shows an error"
(string-match-p "unknown name" (buffer-string)))
;; A value and the program's output travel by different routes: the value
;; is the result of the request, the output rides along with the reply.
;; Showing them in one place would be convenient and wrong.
;; A rejection at the prompt is a compiler message like any other: the
;; message itself goes to the diagnostics list and the prompt gets the
;; one-line pointer at it.
(test-flan--check "the REPL shows the one-line error summary"
(and (string-match-p "1 error" (buffer-string))
(string-match-p (regexp-quote
flan-diagnostics-buffer)
(buffer-string))))
(test-flan--check "and the message itself is in the diagnostics list"
(with-current-buffer (flan--diagnostics-buffer)
(string-match-p "unknown name" (buffer-string))))
;; A value and the program's output travel by different routes — the
;; value is the result of the request, the output rides along with the
;; reply — and both land here, output above value, so the prompt reads
;; the way a terminal REPL does. The daemon's buffer mirrors the output.
(goto-char (point-max))
(insert "(println \"PRINTED\")")
(flan-repl-return)
;; "^PRINTED$" throughout, because the input echo just inserted contains
;; the word too: the bare line is the program's output and nothing else
;; is.
(let ((deadline (+ (float-time) 15)))
(while (and (not (with-current-buffer flan-output-buffer
(string-match-p "PRINTED" (buffer-string))))
(while (and (not (string-match-p "^PRINTED$" (buffer-string)))
(< (float-time) deadline))
(ignore-errors (flan--request '(:op "describe")))
(accept-process-output nil 0.05)))
(test-flan--check "printed text goes to the output buffer"
(with-current-buffer flan-output-buffer
(string-match-p "PRINTED" (buffer-string))))
;; ...and the prompt got the *value*, which for a call made for its effect
;; is Unit. The text it printed is not the value and does not belong here.
(test-flan--check "and the prompt got the value, not the text"
(string-match-p "()" (buffer-string))))
(test-flan--check "printed text lands at the REPL"
(string-match-p "^PRINTED$" (buffer-string)))
;; ...and the prompt got the *value* as well, which for a call made for
;; its effect is Unit — after the output it caused, not before it.
(test-flan--check "below it the value, in that order"
(let ((s (buffer-string)))
(and (string-match "^PRINTED$" s)
(string-match-p "()" (substring s (match-end 0))))))
(test-flan--check "and the daemon's buffer mirrors the output"
(with-current-buffer flan-daemon-buffer
(string-match-p "^PRINTED$" (buffer-string))))
;; The two clears. C-c C-o takes down what the last send produced and
;; nothing else; C-c M-o takes the transcript whole and leaves a prompt.
(flan-repl-clear-output)
(test-flan--check "clear-output removes the last send's output"
(not (string-match-p "^PRINTED$" (buffer-string))))
(test-flan--check "but not the transcript above it"
(string-match-p "23" (buffer-string)))
(flan-repl-clear)
(test-flan--check "clear empties the transcript to a fresh prompt"
(and (not (string-match-p "23" (buffer-string)))
(string-match-p (regexp-quote flan-repl-prompt)
(buffer-string)))))
;; ── One diagnostics list, two sections ─────────────────────────────────
;;
;; The memory sites render at the end of the diagnostics buffer as one
;; section, replaced whole on every ask, and an error that arrives after a
;; render still lands *above* the section: the errors are the log, the
;; sites are a standing reading. Driven directly rather than through the
;; daemon — the op's rows are test_dev.ml's business, the layout is this
;; file's.
(flan--render-memory-sites '(("f.flan:1:1" "memory/gc" "MEMNOTE-A")))
(flan--record-diagnostic "f.flan:2:2" "ERR-AFTER")
(with-current-buffer (flan--diagnostics-buffer)
(let ((s (buffer-string)))
(test-flan--check "an error recorded after a render sits above it"
(and (string-match-p "ERR-AFTER" s)
(string-match-p "MEMNOTE-A" s)
(< (string-match "ERR-AFTER" s)
(string-match "MEMNOTE-A" s))))))
(flan--render-memory-sites '(("f.flan:3:3" "memory/native" "MEMNOTE-B")))
(with-current-buffer (flan--diagnostics-buffer)
(let ((s (buffer-string)))
(test-flan--check "a second render replaces the section whole"
(and (not (string-match-p "MEMNOTE-A" s))
(string-match-p "MEMNOTE-B" s)))
(test-flan--check "with the errors still above it"
(< (string-match "ERR-AFTER" s)
(string-match "MEMNOTE-B" s)))))
(flan-clear-diagnostics)
(with-current-buffer (flan--diagnostics-buffer)
(test-flan--check "clearing the list takes both sections"
(= (point-min) (point-max))))
;; ── The break loop ────────────────────────────────────────────────────
;;