diff --git a/FIX.org b/FIX.org index de939ef..f3a8435 100644 --- a/FIX.org +++ b/FIX.org @@ -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). diff --git a/docs/BUILT.md b/docs/BUILT.md index ddb5897..010816d 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -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 `()` diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 09b6c1d..56ee88d 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -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.** `` and `` walk it while you are on the line you are typing, filtered by whatever you have typed so far — `(sim` then `` 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 | diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 0c2ca04..6d2afa9 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -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 diff --git a/emacs/flan-repl.el b/emacs/flan-repl.el index 2276442..f3b3c6e 100644 --- a/emacs/flan-repl.el +++ b/emacs/flan-repl.el @@ -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 "") #'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 "")) - (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." diff --git a/emacs/flan.el b/emacs/flan.el index 6e16b11..e306533 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -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)) diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 4aa2bd1..4a4a065 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -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 "\\_" 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 ──────────────────────────────────────────────────── ;;