diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 1644d47..09b6c1d 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -170,21 +170,31 @@ one point is in, and only then is it asked whether it is a definition. ### `C-x C-e` — evaluate an expression The expression before point is compiled, run **inside the running program**, and -its value shown beside it, as `=> 2` after the form you evaluated. Not a copy of +its value shown as `=> 2` at the end of the line the form is on. Not a copy of the program, not a simulation — the actual process, with its actual state. So in a game you can type `(len enemies)` and get the real number. -The value goes away on your next keystroke, like every other bit of feedback -about a command that just ran. Set `flan-inline-result` to `nil` and it goes to -the echo area instead; that is also where it goes when there is nowhere in a -buffer to draw it, as at the REPL. +**Where it appears.** At the end of the *line*, which is where the watch buffer's +ghost text goes too — the two are the same thing seen twice and they are drawn +the same way. It used to sit at the end of the form instead, so that the two +features could be told apart by position; what that cost is a value wedged into +the middle of a line whenever the form was not the last thing on it, reflowing +everything after it while you read. -The two are different questions and the settings are not two spellings of one -thing. `flan-echo-result` is about the sentence an *install* reports — which -names landed and what it cost — and an install has no value to draw. Where they -would overlap, on an expression, the inline value wins and the echo is not also -written: the same number twice is how a reader learns to skip both. +**And in the echo area, always.** The overlay goes away on your next keystroke, +like every other bit of feedback about a command that just ran — so the same +value is written to the echo area as well, where it is still there afterwards. +The echo used to be suppressed whenever the overlay drew, on the argument that +the same number twice teaches a reader to skip both; between saying it twice and +losing it before it was read, saying it twice is the smaller cost. + +The two settings are still different questions, and neither now suppresses the +other. `flan-inline-result` decides the overlay: set it to `nil` and the echo +area is the only place a value appears, which is also what happens when there is +nowhere in a buffer to draw one, as at the REPL. `flan-echo-result` decides the +echo area, and it also covers the sentence an *install* reports — which names +landed and what it cost — since an install has no value to draw. The form before point can also be a declaration — a `defvar` typed at the top of a file — in which case `C-x C-e` installs it rather than refusing it, and says @@ -366,8 +376,16 @@ everything it had is still there. ## Looking at values -**`C-c C-i`** inspects a value. Give it an expression; you get its fields, one -per line. +**`C-c C-i`** inspects a value: the expression point is on, with no prompt. You +get its fields, one per line. It takes the innermost thing point is on rather +than only the one behind point, so the middle of a name works as well as the end +of one — point anywhere in `enemies` takes `enemies`, and on the open paren of +`(len enemies)` takes the whole call. + +**`C-u C-c C-i`** opens the minibuffer instead, pre-filled with whatever was at +point — for inspecting something that is not written in the buffer, or is +written somewhere else. That is also what you get when there is nothing at point +to take. | Key | Does | |---|---| @@ -847,6 +865,23 @@ if one is written, and the body alike. `{.x 1.0}`. The colon is still a constant too; it means an enum member, `:green`, and a key in a map. +**The keywords are the parser's.** Both lists — the forms that introduce a +top-level name, and the forms with meaning to the checker — are read off +`Parse.decl` and `Parse.form` in `lib/parse.ml` and kept whole, rather than +topped up whenever somebody notices a gap. So `defmacro`, `defclass`, +`defgeneric`, `defmulti`, `defmethod` and `declare-c` colour as definitions, and +`when`, `cond`, `and`, `or`, `break`, `continue`, `recur`, `fn`, `quote`, +`array`, `signal`, `error` and the four condition forms — `handler-bind`, +`handler-case`, `restart-case`, `invoke-restart` — colour as keywords. A macro +*you* define is still drawn as an ordinary call: macro-ness is erased by the +time the daemon can be asked about a name, so there is nothing to ask. + +**`#_` greys out the form after it**, the way it reads: the discarded form is +given the comment syntax class, so it is drawn as a comment and skipped by +everything else that skips comments. Chaining works as the reader's does — +`#_#_ a b` discards both, with nothing counting — and an unfinished form is +left alone, so `#_(` does not grey the rest of the file while you are typing it. + Nothing here needs a running program. Indentation and colouring are the major mode's, so they work in a file you have only opened. @@ -955,6 +990,23 @@ other three are unaffected. next time that buffer's evaluation is accepted — so it disappears when you fix 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 +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. + +**A build that failed is in `*flan-dev*`**, 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 +socket at all, and this is the only account of why. + **"No .flan-dev.sock found above this buffer"** — nothing is running, or you are outside the project. Start one with `M-x flan`. @@ -987,7 +1039,8 @@ Use `C-c C-g` if you need frames. | `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 | -| `C-c C-i` | inspect a value | +| `C-c C-i` | inspect the expression at point | +| `C-u C-c C-i` | ...or one you type in the minibuffer | | `C-c C-m` | what the macro call at point expands to | | `C-u C-c C-m` | ...all the way, rather than one step | | `C-c C-a` | disassemble; `C-u` first for LLVM IR | @@ -1001,6 +1054,8 @@ Use `C-c C-g` if you need frames. Commands with no key: `M-x flan` (start a program), `M-x flan-quit` (stop it), `M-x flan-watch` (the watch buffer), `M-x flan-watch-stop`, `M-x flan-watch-ghost-mode` (the same values inline), +`M-x flan-show-diagnostics` / `M-x flan-clear-diagnostics` (every compiler +message this session has been handed, and emptying that), `M-x flan-inspect-address` (what is at an address), `M-x flan-macroexpand-all` (the `C-u` half of `C-c C-m`, by name), `M-x flan-allocations` / `M-x flan-leaks` (where the memory went, and what is still held), and @@ -1017,11 +1072,12 @@ in the buffer). | `flan-daemon-args` | `nil` | extra arguments for `flan dev` — `("--llvm")`, `("--debug")` | | `flan-socket-name` | `".flan-dev.sock"` | what `C-c C-z` searches for | | `flan-echo-result` | `t` | report an accepted evaluation in the echo area | -| `flan-inline-result` | `t` | show an expression's value beside the form, not 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-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-inspect.el b/emacs/flan-inspect.el index 2587989..2e76e0a 100644 --- a/emacs/flan-inspect.el +++ b/emacs/flan-inspect.el @@ -677,19 +677,56 @@ and whether that may be followed is the answer")) (display-buffer buf) buf)) +(defun flan-inspect--expression-at-point () + "The text of the expression point is on, or nil. + +The innermost thing point is on, and not merely the one behind it: point in +the middle of a name is where a key pressed without looking lands, and +`backward-sexp\=' alone answers with the half of the name already typed past — +which reads as the command being broken rather than as point being one +character early. So the end of the thing under point is found first, and the +form is taken back from there; on an opening delimiter the form that opens +there is taken instead, which is the rule `flan-macroexpand\=' follows for the +same reason. + +Innermost, so point on `enemies\=' inside `(len enemies)\=' takes `enemies\=' and +not the call — which is the right answer for a command that shows you one +value, and the enclosing call is a paren away in either direction. + +Nil when there is nothing there to take, which is a buffer position with no +form at it at all." + (ignore-errors + (save-excursion + (skip-chars-forward " \t") + (if (looking-at-p "[([{]") + (buffer-substring-no-properties + (point) (progn (forward-sexp) (point))) + ;; Inside a form: out to its end, so that the text taken is the whole + ;; of it. Nothing to step over when point is already after one — at + ;; whitespace, at a closing delimiter, or at the end of the buffer. + (unless (or (eobp) + (memq (char-syntax (char-after)) '(?\s ?> ?\)))) + (forward-sexp)) + (buffer-substring-no-properties + (save-excursion (backward-sexp) (point)) (point)))))) + ;;;###autoload (defun flan-inspect (expr) "Inspect the value of EXPR in the running program. -Interactively, the expression before point, or one you type. + +Interactively, the expression at point — no prompt, because the expression is +already written in the buffer and retyping it is the whole cost of the +command. With a prefix argument, or with nothing at point to take, the +minibuffer opens instead, pre-filled with whatever was there. This is the expression root: EXPR is evaluated where the evaluator stands, so it works on a running program but cannot say which frame it means. `i\=' in the break buffer uses `flan-inspect-slot\=' for a local, for exactly that reason." (interactive - (list (read-string "Inspect: " - (ignore-errors - (buffer-substring-no-properties - (save-excursion (backward-sexp) (point)) (point)))))) + (let ((at (flan-inspect--expression-at-point))) + (list (if (and at (not current-prefix-arg) (not (string-blank-p at))) + at + (read-string "Inspect: " at))))) ;; A new root, and therefore an empty stack. That is the whole of why `l\=' ;; cannot walk out of one root into another: there is never an entry from a ;; different root left underneath it. diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 00905c7..0c2ca04 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -111,17 +111,37 @@ :group 'languages :prefix "flan-") +;; Both lists are read off `Parse.decl' and `Parse.form' (lib/parse.ml), which +;; are where the two questions are actually decided: a head `decl' has an arm +;; for introduces a name, and a head `form' has an arm for is never a call. +;; Kept whole rather than topped up one name at a time — the list that is +;; patched only when somebody notices a gap is the list that is always a +;; release behind the parser. (defconst flan--definers - '("defn" "defvar" "defconst" "defstruct" "defdata" "defunion" "defenum" - "defalias" - "declare" "import" "package") + '("defn" "defmacro" "defvar" "defconst" "defstruct" "defdata" "defunion" + "defenum" "defalias" + ;; The object and dispatch heads (lib/parse.ml:1277-1334). + "defclass" "defgeneric" "defmulti" "defmethod" + "declare" "declare-c" "import" "package") "Forms that introduce a top-level name.") (defconst flan--special - '("let" "if" "do" "while" "until" "dotimes" "loop" "match" "set" "return" - "defer" "some" "none" "try" "zeroed" "uninit" "slice" "at" "len" "addr" + '("quote" "let" "if" "when" "cond" "and" "or" "do" "while" "until" "dotimes" + "loop" "recur" "break" "continue" "match" "set" "return" "fn" "array" + "defer" "some" "none" "try" "signal" "error" + "handler-bind" "handler-case" "restart-case" "invoke-restart" + "zeroed" "uninit" "slice" "at" "len" "addr" "bytes" "cast" "true" "false" "nil" "print" "println") - "Forms with meaning to the checker.") + "Forms with meaning to the checker. + +The heads `Parse.form' dispatches on, plus the literals and the handful of +builtins that are never anything else. Two groups of real heads are +deliberately left out: `quasiquote', `unquote' and `unquote-splicing', which +nobody writes as words — the reader makes them out of \\=`, ~ and ~@, and the +sigils are not symbols for a keyword rule to reach — and `find-restart', +`compute-restarts', `errdefer' and `await', which the parser recognises only +in order to refuse them. Drawing those four as keywords would advertise four +forms that cannot be used.") (defvar flan-font-lock-keywords `((,(concat "(" (regexp-opt flan--definers t) "\\_>" @@ -194,6 +214,95 @@ line is off screen." table) "Syntax table for `flan-mode'.") +;;; The discard reader macro + +;; `#_' reads the form after it and throws it away (lib/reader.ml:16-24, +;; 202-262), which is how a form is commented out without counting its closing +;; parens. The table above knows nothing about it — it comes from +;; `lisp-mode-syntax-table', and Common Lisp has no such construct — so a +;; discarded form used to render as ordinary code, which is the one thing it +;; must not look like. +;; +;; The fix is `clojure-mode's, which has the same reader macro and the same +;; problem: a `syntax-propertize-function' that gives the span the comment +;; syntax class. Font lock then draws it as a comment with no rule of its own, +;; and every other thing that reads syntax classes — `forward-comment', +;; `comment-only-p', a package that skips comments — agrees for free. +;; +;; Generic comment fences (the `!' class) rather than a line comment, because a +;; discarded form is a region and may be one line or twenty. The `#' is the +;; opening fence and the last character of the form is the closing one, so +;; everything in between is inside a comment and is no longer read as +;; structure. That is the reader's own answer, and it leaves the *enclosing* +;; list's parens alone: only the discarded form's own last delimiter changes +;; class, so `(a #_(b c) d)' still balances. + +(defun flan--discard-end (pos) + "Where the discard whose form starts at POS ends, or nil. + +The reader's rule rather than a count: a discard reads *a form*, and reading +one skips leading discards of its own, so `#_#_ a b' throws away both a and b +with nothing ever counting to two (lib/reader.ml:202-262). Written as the +loop that recursion amounts to — every `#_' seen owes one more form — since +the walk over the buffer is the same either way. + +Nil when a form is unfinished, which is every buffer halfway through being +typed: nothing is propertized then, rather than `#_(' greying out the rest of +the file while the parenthesis is still open." + (let ((owed 1) + ;; The scan must not consult the properties it is being run to + ;; compute: with `parse-sexp-lookup-properties' on, `forward-sexp' + ;; would ask for the syntax of positions this pass has not reached. + ;; The table alone is enough — the nesting is counted here, and + ;; strings and escapes are the table's own business. + (parse-sexp-lookup-properties nil)) + (ignore-errors + (save-excursion + (goto-char pos) + (while (> owed 0) + (forward-comment (buffer-size)) + (if (looking-at-p "#_") + (progn (forward-char 2) (setq owed (1+ owed))) + (forward-sexp) + (setq owed (1- owed)))) + ;; `forward-sexp' at the end of the buffer stays put rather than + ;; signalling, so a trailing `#_' with nothing after it would otherwise + ;; come back as a span covering only itself — a two-character comment + ;; drawn over the reader's one real discard error. It is unfinished + ;; like any other, and answers nil like any other. + (and (> (point) pos) (point)))))) + +(defun flan--syntax-propertize (start end) + "Mark every `#_' discard between START and END as a comment. +For `syntax-propertize-function'." + (goto-char start) + (while (search-forward "#_" end t) + ;; Both ends of the match are read off point before anything else runs, and + ;; the `syntax-ppss' below is wrapped: it moves point and clobbers the + ;; match data, so asking it first and reading the match afterwards is how + ;; this would come to scan from the `#' instead of from the form — and how + ;; a `#_' it declined would be found again, forever, at the same place. + (let* ((beg (- (point) 2)) + (from (point)) + ;; `#_' written inside a string, or inside a `;' comment, is text + ;; and not a reader macro. One inside a span an earlier discard + ;; already covers is not reached at all: that discard's own scan + ;; consumed it, and point jumps past the whole span below. + (quoted (save-excursion (nth 8 (syntax-ppss beg)))) + (fin (and (not quoted) (flan--discard-end from)))) + (when fin + (put-text-property beg (1+ beg) + 'syntax-table (string-to-syntax "!")) + (put-text-property (1- fin) fin + 'syntax-table (string-to-syntax "!")) + ;; So that a change *inside* a discarded form re-propertizes from the + ;; `#_' rather than from the line it was typed on; the other half is + ;; `syntax-propertize-multiline' in the mode body. + (put-text-property beg fin 'syntax-multiline t)) + ;; Past the span when there was one, and past the `#_' when there was + ;; not. Either way forward, which is the whole of why this terminates. + (goto-char (or fin from))))) + (defvar flan-mode-map (let ((map (make-sparse-keymap))) ;; Autoloaded from flan.el, so the client loads on first use. @@ -270,6 +379,12 @@ line is off screen." (setq-local lisp-indent-function #'flan-indent-function) (setq-local outline-regexp ";;;;+[ \t]*") (setq-local imenu-generic-expression flan-imenu-generic-expression) + ;; `#_' — see the section above the syntax table. + (setq-local syntax-propertize-function #'flan--syntax-propertize) + ;; A discarded form spans as many lines as it likes, so the region handed to + ;; the propertizer has to be widened back to the `#_' that owns it. + (add-hook 'syntax-propertize-extend-region-functions + #'syntax-propertize-multiline nil t) ;; Buffer-locally, because this answers for Flan and nothing else. (add-hook 'which-func-functions #'flan-current-defun-name nil t)) diff --git a/emacs/flan.el b/emacs/flan.el index 31bcdf4..6e16b11 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -67,6 +67,11 @@ (require 'cl-lib) (require 'xref) (require 'eldoc) +;; For `compilation-minor-mode', which is what makes a `file:line:col:' line in +;; the daemon's buffer and in the diagnostics log somewhere `next-error' can +;; go. Flan's messages already have that shape, so nothing here teaches +;; `compile' a new one. +(require 'compile) ;; The macroexpansion buffer is a `flan-mode' buffer — what is in it is Flan ;; source and reading it is the point — so the mode has to exist by the time ;; this file defines one derived from it. Not a cycle: flan-mode.el autoloads @@ -89,16 +94,22 @@ that quietly did nothing, which is why it is on." :type 'boolean) (defcustom flan-inline-result t - "Whether an expression's value is shown beside the form it came from. + "Whether an expression's value is shown at the end of the line it came from. The two knobs are not two ways of saying one thing. This one is about a *value*: nothing else an evaluation produces has a place in the buffer to sit beside, so an install still reports through `flan-echo-result' whatever this -is set to. Where they do overlap — an expression whose value could go either -place — the overlay wins and the echo is not also written, because saying the -same number twice is how a reader learns to stop reading both. The echo is -what happens when no overlay could be drawn: the REPL, a value that came back -with no region behind it, or this turned off." +is set to. + +Where they overlap — an expression, whose value can go both places — both are +written. The overlay used to suppress the echo, on the argument that saying +one number twice teaches a reader to skip both; in practice the echo area is +the half that is still there after the next keystroke takes the overlay down, +and a value that is gone before it was read is worse than a value said twice. +So the two are now independent: this one decides the overlay, and +`flan-echo-result' decides the echo, and turning this off leaves the echo as +the only place a value appears — which is also what happens where no overlay +could be drawn at all, as at the REPL." :type 'boolean) (defcustom flan-names-shown 4 @@ -118,6 +129,18 @@ build that is merely slow can be watched, which is what the reply timeout below points at." :type 'string) +(defcustom flan-diagnostics-buffer "*flan-diagnostics*" + "Buffer every compiler diagnostic the editor is handed is appended to. + +The overlay a refusal draws is about the command that just ran and is gone at +the next keystroke, which is right for feedback and wrong for a message you +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." + :type 'string) + (defcustom flan-reply-timeout 30 "Seconds to wait for one reply from the daemon before giving up. @@ -755,7 +778,25 @@ It builds the program first, which for a cold project is most of this." (let ((inhibit-read-only t)) (erase-buffer) (insert (mapconcat #'identity args " ") "\n\n")) - (setq default-directory (file-name-directory (expand-file-name file)))) + ;; The buffer's own, not just the `let' above, and now load-bearing + ;; twice: a relative path in a diagnostic is relative to where the daemon + ;; ran, and that is what the jump below resolves it against. + (setq default-directory (file-name-directory (expand-file-name file))) + ;; This is where a `main' that does not compile says so, and Flan writes + ;; a diagnostic as `file:line:col: message' — the GCC shape + ;; `compilation-error-regexp-alist' was built around, and which its + ;; `gnu' entry matches with nothing added here. (The two lines a + ;; diagnostic carries under that one, the source line and its carets, + ;; match nothing and are left as text, which is what they are.) So the + ;; buffer only has to be told it is compilation output, and `M-g M-n', + ;; `next-error' and RET on the line all reach the code. + ;; + ;; The minor mode and not `compilation-mode': the buffer is also the + ;; daemon's own log and the program's stderr, arriving for as long as the + ;; program runs, and `compilation-mode' would claim it as the output of + ;; one finished command — killing the process on a `recompile', among + ;; other things it has no business doing to a live session. + (compilation-minor-mode 1)) (make-process :name "flan-daemon" :buffer buf :command args @@ -1326,6 +1367,70 @@ else, and an overlay that survived a fix would be pointing at code that is no longer wrong." (flan-clear-errors)) +;;; The diagnostics log + +;; The overlay above is the whole of what a refusal used to leave behind, and +;; it is deliberately short-lived: it is feedback about one command and the +;; next keystroke takes it away. That is right for reading and wrong for +;; keeping. A message worth quoting somewhere else — into a bug report, into a +;; chat window, into a file — had to be fished back out of *Messages*, where it +;; sits among everything else Emacs has said and with no mark saying where one +;; message ends and the next begins. +;; +;; So both: the overlay, unchanged, and a buffer that accumulates. One entry +;; per message, the time it arrived on its own line above it, nothing ever +;; removed but by hand. The message line itself is left in exactly the shape +;; 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. + +(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) + (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." + (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)) + (unless (bolp) (insert "\n")) + (insert (format-time-string "── %H:%M:%S ──────────────────────\n")) + (insert (if (stringp loc) (format "%s: %s\n" loc msg) + (format "%s\n" msg)) + "\n")) + ;; `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))))))) + +;;;###autoload +(defun flan-show-diagnostics () + "Show every compiler message this session has been handed." + (interactive) + (display-buffer (flan--diagnostics-buffer))) + +;;;###autoload +(defun flan-clear-diagnostics () + "Empty the diagnostics log. +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)))) + (defun flan--show-error (loc msg) "Mark MSG at LOC, if LOC names a file some buffer is visiting. Returns non-nil when it put an overlay somewhere." @@ -1381,12 +1486,21 @@ Returns non-nil when it put an overlay somewhere." ;; The shape is `flan-watch--ghost-text's, deliberately: " => " and a shadow ;; face. A watched value at a spy site and an expression's value at the form ;; you evaluated are the same thing seen two ways, and two spellings of it -;; would read as two features. What keeps them from fighting is that they sit -;; at different places — the watch's at end of line, this at end of form — and -;; where a one-line top-level form makes those the same position, two -;; after-strings at one position stack in priority order rather than one -;; hiding the other. They are also on different properties, so neither -;; clearing pass reaches into the other's overlays. +;; would read as two features. The position is the watch's too — the end of +;; the *line* the form ends on, not the end of the form. It used to be the end +;; of the form, on the argument that a value at a different place is how you +;; tell the two features apart; what that cost is a value wedged into the +;; middle of a line, reflowing the code around it, every time the form +;; evaluated was not the last thing on its line. Telling them apart is not a +;; job worth that, and it was never really the position doing it — the watch +;; buffer's values are standing annotations on a running program and this one +;; is gone at the next keystroke, which is a difference you can see. +;; +;; So they do now land at one position where a watched call and an evaluated +;; form share a line. Two after-strings at one position stack in priority +;; order rather than one hiding the other, which is what that case did before +;; on a one-line top-level form. They are also on different properties, so +;; neither clearing pass reaches into the other's overlays. (defface flan-result-face '((t :inherit shadow)) "Face for an expression's value, shown beside the form it came from." @@ -1410,18 +1524,24 @@ Returns non-nil when it put an overlay somewhere." (flan-clear-result)) (defun flan--show-result (value at) - "Draw VALUE after position AT. Returns non-nil when it drew one. + "Draw VALUE at the end of the line AT is on. Returns non-nil when it drew. + +AT is a position in the form that was evaluated and the value goes at the end +of its line, for the reason the section above gives. Empty overlays rather than a region: the value is not a property of any text, so nothing should be highlighted and nothing should move when the buffer is -edited under it. Returning whether it drew is what lets the caller fall back -to the echo area instead of losing the value entirely." +edited under it. Whether it drew is no longer what decides the echo area — +both are written now — and is still answered, because the REPL and a value +with no buffer behind it are cases where nothing was drawn and the caller may +want to know." (when (and flan-inline-result at (buffer-live-p (current-buffer))) ;; The old one first, so a second evaluation in one command — which is ;; what `flan-eval-buffer' and a macro of these amount to — leaves one ;; value and not a column of them. (flan-clear-result) - (let ((ov (make-overlay at at nil t nil))) + (let* ((eol (save-excursion (goto-char at) (line-end-position))) + (ov (make-overlay eol eol nil t nil))) (overlay-put ov 'flan-result t) (overlay-put ov 'after-string (propertize (concat " => " value) @@ -1821,10 +1941,11 @@ of the tenth name tells you neither how many there were nor which." (defun flan--report (reply what &optional at) "Report REPLY, describing WHAT was sent. -AT, when given, is where in the current buffer an expression's value may be -drawn — the end of the form that was sent. A reply with no value in it never -reaches an overlay whatever AT says, because an install is a sentence and not -a value and has nothing to sit beside." +AT, when given, is the end of the form that was sent: the position that says +which *line* an expression's value belongs to, since that is where +`flan--show-result' draws it. A reply with no value in it never reaches an +overlay whatever AT says, because an install is a sentence and not a value and +has nothing to sit beside." (if (equal (plist-get reply :status) "ok") (let ((fns (plist-get reply :fns)) (names (plist-get reply :names)) @@ -1839,47 +1960,58 @@ a value and has nothing to sit beside." ;; ...and a name that was just installed should complete, and have a ;; signature, from this moment rather than from the next connect. (when (or fns names) (ignore-errors (flan-refresh-defs))) - ;; The overlay first, because whether it drew is what decides the echo - ;; area's business: a value shown beside the form does not want saying - ;; again one line below it. Independent of `flan-echo-result' on - ;; purpose — turning the echo off is a statement about the echo, and - ;; it should not silently take the inline value with it. - (let ((shown (and value (ignore-errors (flan--show-result value at))))) - (when (and flan-echo-result (not shown)) - (cond - ;; An expression's value, rendered inside the running program — - ;; nothing was marshalled back, because nothing could be. - (value (message "=> %s" value)) - ;; The daemon accepted it and had nothing to send. Say so rather - ;; than claiming an install that did not happen. - (note (message "flan: %s — %s" - (flan--names-phrase names what) note)) - (t - ;; `:fns' are the bodies that were installed and `:names' is - ;; everything the evaluation declared; a buffer of five functions - ;; and two vars should not report as "five". - ;; - ;; A declaration with no body at all — a `defvar', a `defstruct' — - ;; has none of the first, and leading with WHAT then read as "form - ;; installed in 4 ms (also ticks)": the one name that actually - ;; changed, parenthesised as an afterthought, beside a label that - ;; says nothing. Where there are no functions the names *are* what - ;; changed, so they are what the sentence is about, and the aside - ;; is then empty by construction rather than a repeat of it. Now - ;; that `C-x C-e' reaches this path too, that sentence is also how - ;; you tell an installed declaration from an expression's `=>'. - (message "flan: %s installed in %.0f ms%s" - (flan--names-phrase (or fns names) what) - (or (plist-get reply :ms) 0) - (let ((vars (and fns (seq-difference names fns)))) - (if vars (format " (also %s)" - (flan--names-phrase vars "")) - "")))))))) + ;; Both, where both are possible. The overlay is the one that answers + ;; "which form was that?" and the echo area is the one that is still + ;; readable after the next keystroke has taken the overlay down — + ;; between a value said twice and a value gone before it was read, the + ;; second is the worse failure, and it is the one that kept happening. + ;; The two settings are independent in both directions: turning the + ;; echo off is a statement about the echo and does not take the inline + ;; value with it, and turning the inline value off leaves the echo. + ;; + ;; The overlay first, still — not because the echo waits on the answer + ;; any more, but because the order is what survives: a `message' + ;; written first is wiped by whatever the drawing does to the buffer. + (when value (ignore-errors (flan--show-result value at))) + (when flan-echo-result + (cond + ;; An expression's value, rendered inside the running program — + ;; nothing was marshalled back, because nothing could be. + (value (message "=> %s" value)) + ;; The daemon accepted it and had nothing to send. Say so rather + ;; than claiming an install that did not happen. + (note (message "flan: %s — %s" + (flan--names-phrase names what) note)) + (t + ;; `:fns' are the bodies that were installed and `:names' is + ;; everything the evaluation declared; a buffer of five functions + ;; and two vars should not report as "five". + ;; + ;; A declaration with no body at all — a `defvar', a `defstruct' — + ;; has none of the first, and leading with WHAT then read as "form + ;; installed in 4 ms (also ticks)": the one name that actually + ;; changed, parenthesised as an afterthought, beside a label that + ;; says nothing. Where there are no functions the names *are* what + ;; changed, so they are what the sentence is about, and the aside + ;; is then empty by construction rather than a repeat of it. Now + ;; that `C-x C-e' reaches this path too, that sentence is also how + ;; you tell an installed declaration from an expression's `=>'. + (message "flan: %s installed in %.0f ms%s" + (flan--names-phrase (or fns names) what) + (or (plist-get reply :ms) 0) + (let ((vars (and fns (seq-difference names fns)))) + (if vars (format " (also %s)" + (flan--names-phrase vars "")) + ""))))))) ;; The daemon reports where, so mark it there. This must not itself ;; signal: the error the caller is owed is the daemon's, and losing it to a ;; bad location would report the wrong thing entirely. (let ((loc (plist-get reply :loc)) (msg (plist-get reply :message))) + ;; Logged before it is drawn, and whether or not it can be drawn: the + ;; 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"))) ;; 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 @@ -2004,34 +2136,44 @@ daemon reads that as stopping on entry instead." ;; question of it: the top-level form point is *in*, which for a bare ;; `(+ 1 1)' written at column 1 is that expression and nothing else. ;; -;; Hardcoded, which is the thing to be careful about: the authority is the -;; declaration arm of `Parse.expr' in lib/parse.ml (the "is a top-level -;; declaration, not an expression" refusal, around line 458), plus the -;; `declare'/`declare-c' arm a few forms below it. This list is that set and -;; has to be changed with it. `defunion' is being renamed `defdata', with -;; `defunion' becoming a C-style untagged union; both spellings are top-level -;; declarations either way, so this list wants `defdata' adding when that -;; lands rather than a swap. +;; Hardcoded, which is the thing to be careful about: the authority is +;; `Parse.decl' in lib/parse.ml — every head it has an arm for is a +;; declaration and belongs here — and this list is that set minus `package', +;; for the reason below. Re-derived whole when it is touched, rather than +;; topped up a name at a time: patching in the one name somebody noticed is +;; exactly how it came to be missing `defdata' and all four of the object and +;; dispatch heads for weeks after they landed. (`Parse.decl's top-level +;; `(do ...)' arm is not a head — it splices the declarations inside it, and +;; the forms this list is matched against are what comes out of that.) ;; -;; `package' is deliberately not here even though flan-mode.el's -;; `flan--definers' has it. This is the set of heads that *fail* when sent to -;; the expression evaluator, which is the bug `C-x C-e' was fixed for, and a -;; `package' form does not fail — `Parse.expr' has no arm refusing it. It is -;; still a declaration, so `C-c C-c' has to keep installing it; see -;; `flan--defun-heads' below, which is this list plus that one head. +;; It used to be derived from somewhere else: the declaration arm of +;; `Parse.expr', the "is a top-level declaration, not an expression" refusal, +;; on the reading that this is the set of heads that *fail* when sent to the +;; expression evaluator. That reading does not survive `declare' and +;; `declare-c', which have been in this list from the start and which +;; `Parse.expr' has no arm for at all: sent as expressions they parse as calls +;; of an unknown name, and the message that comes back is worse than the one +;; the declaration path gives. Which is an argument for installing them, not +;; for leaving them out. So the rule is the plain one — is it a declaration — +;; and `package' is a single named exception rather than the edge of a subtler +;; rule that was never quite true. (defconst flan--declaration-heads - '("defmacro" "defn" "defvar" "defconst" "defstruct" "defunion" "defenum" - "defalias" "import" "declare" "declare-c") + '("defmacro" "defn" "defvar" "defconst" + "defstruct" "defdata" "defunion" "defenum" "defalias" + ;; The object and dispatch heads (lib/parse.ml:1277-1334), which are + ;; declarations in exactly the way `defn' is: each introduces a top-level + ;; name and none of them is an expression. + "defclass" "defgeneric" "defmulti" "defmethod" + "import" "declare" "declare-c") "Heads whose form is a declaration, and never an expression.") -;; The two keys ask *nearly* the same question, and `package' is where the two -;; readings come apart. `C-x C-e' asks "would this fail if I sent it to the -;; expression evaluator?", and a `package' form would not: `Parse.expr' has no -;; arm refusing it, so it falls through to a call of an unknown name and the -;; message would be a worse one than the declaration path gives. `C-c C-c' -;; asks the plain question — "is this a declaration?" — and `package' is one: -;; `Parse.decl' has an arm for it, `C-c C-c' has always installed it, and -;; routing it to the expression evaluator would take that away. +;; `package' is the one declaration `C-x C-e' does not claim, and it is kept +;; out to keep a behaviour rather than to follow a rule: the two keys have +;; always differed here, `C-c C-c' installing a `package' form and `C-x C-e' +;; leaving it to the expression evaluator. Nothing wants changing — a +;; `package' line is written once at the top of a file and is not a thing +;; anybody re-evaluates by hand — so the difference stays, named here where it +;; can be found rather than inferred from a list that is otherwise complete. (defconst flan--defun-heads (cons "package" flan--declaration-heads) "Heads `C-c C-c' recompiles and installs rather than evaluating.") @@ -2181,8 +2323,9 @@ declaration for it to live in." ;; start of the *next* line in every file that ends a line after a form ;; — which is every file. The declaration path never noticed, because ;; a newline more or less in the text sent changes nothing. This one - ;; draws a value at END, and at the untrimmed one it lands in column 0 - ;; of the line below, over the gap before the next form. + ;; draws a value on END's line, and the untrimmed END is on the line + ;; below the form — so the value would sit at the end of the blank line + ;; after it, or at the end of whatever form comes next. (let ((end (save-excursion (goto-char (cdr b)) (skip-chars-backward " \t\n") (point)))) @@ -2458,8 +2601,10 @@ sends padded text." (msg (plist-get r :message))) ;; The two refusals that reach here are a macro that never settles and ;; a ring, and both name a macro at a location — so mark it, exactly as - ;; a refused evaluation is marked. This must not itself signal: the - ;; error the caller is owed is the daemon's. + ;; a refused evaluation is marked, and log it for the same reason. + ;; This must not itself signal: the error the caller is owed is the + ;; daemon's. + (ignore-errors (flan--record-diagnostic loc (or msg "refused"))) (ignore-errors (flan--show-error loc (or msg "refused"))) (user-error "flan: %s%s" (or msg "refused") (if loc (format " (%s)" loc) "")))) diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 0b958c3..4aa2bd1 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -496,20 +496,23 @@ already rely on it — so nothing here is a stand-in for the real thing." (test-flan--check "nor does a bare expression at top level" (null (flan--declaration-before-point))) ;; ...and it still evaluates as one, which is the half of this key that - ;; was already working and must not have moved. The value is now drawn - ;; beside the form and no longer echoed, so both halves are asserted: an - ;; overlay that says it, and an echo area that does not say it twice. + ;; was already working and must not have moved. The value goes to both + ;; places, so both halves are asserted: an overlay that says it, and an + ;; echo area that says it too. The echo used to be suppressed whenever the + ;; overlay drew; what that cost is a value gone at the next keystroke with + ;; nothing left anywhere, which is worse than a number said twice. (let ((said (test-flan--said (flan-eval-last-sexp)))) (test-flan--check "which C-x C-e evaluates and shows beside the form" (let ((r (test-flan--result))) (and r (string-match-p "5" r) (string-match-p "=>" r)))) - (test-flan--check "and does not also say it in the echo area" - (not (and said (string-match-p "=>" said)))) - ;; The overlay is anchored to the form that produced it, not to point - ;; and not to the start of the line: two expressions one under the - ;; other both returning 2 is the case the echo area cannot answer. - (test-flan--check "the value is drawn at the end of the form" + (test-flan--check "and says it in the echo area as well" + (and said (string-match-p "=> 5" said))) + ;; The overlay is anchored to the line the form ends on — `flan-watch's + ;; position, and now this one's too, so that a value never lands in the + ;; middle of a line and reflows the code after it. Here the form is the + ;; last thing in the buffer, so that line's end is `point-max'. + (test-flan--check "the value is drawn at the end of the form's line" (let ((ovs (flan--result-overlays))) (and (= 1 (length ovs)) (= (overlay-start (car ovs)) (point-max))))) @@ -531,14 +534,35 @@ already rely on it — so nothing here is a stand-in for the real thing." (current-buffer)))))))) (delete-region beg (point-max))) + ;; The position that changed, and the only arrangement that can show it: with + ;; something written after the form on its line, the end of the form and the + ;; end of the line are different places. The value belongs at the second, + ;; where `flan-watch' has always put one — a value wedged into the middle of + ;; a line reflows every character after it. + (goto-char (point-max)) + (let ((beg (point))) + (insert "\n(+ 2 3) ; and a comment after it") + (search-backward ") ;") + (forward-char 1) + (test-flan--said (flan-eval-last-sexp)) + (test-flan--check "the value goes to the end of the line, not of the form" + (let ((ovs (flan--result-overlays))) + (and (= 1 (length ovs)) + (= (overlay-start (car ovs)) (point-max))))) + ;; What a keystroke would do; there are no commands under --batch. + (flan-clear-result) + (goto-char (point-max)) + (delete-region beg (point-max))) + ;; Switched off, the echo area is what is left: the two settings are not two - ;; spellings of one thing, and a value has to come out somewhere. + ;; spellings of one thing, and turning the overlay off must not take the + ;; value with it. (goto-char (point-max)) (let ((beg (point))) (insert "\n(+ 7 8)") (let* ((flan-inline-result nil) (said (test-flan--said (flan-eval-last-sexp)))) - (test-flan--check "with the overlay off the value goes back to the echo area" + (test-flan--check "with the overlay off the value is still in the echo area" (and said (string-match-p "15" said) (string-match-p "=>" said) (null (flan--result-overlays))))) @@ -599,12 +623,13 @@ already rely on it — so nothing here is a stand-in for the real thing." (test-flan--check "and draws no value beside it" (null (flan--result-overlays)))) - ;; `package' is the one head the two keys disagree about, and it is a real - ;; disagreement rather than an oversight: `Parse.expr' has no arm refusing - ;; it, so C-x C-e's "would this fail as an expression" says no, while - ;; `Parse.decl' has one and C-c C-c has always installed it. Routing both - ;; off one list would have quietly taken that away. Nothing is sent here — - ;; the claim is only about which path each key picks. + ;; `package' is the one head the two keys disagree about, and the + ;; disagreement is a behaviour being kept rather than a rule being followed: + ;; `C-c C-c' has always installed a `package' form and `C-x C-e' has always + ;; left it to the expression evaluator, and nothing about a line written once + ;; at the top of a file asks for that to change. Routing both keys off one + ;; list would quietly take it away. Nothing is sent here — the claim is only + ;; about which path each key picks. (with-temp-buffer (flan-mode) (insert "(package demo)\n") @@ -618,6 +643,33 @@ already rely on it — so nothing here is a stand-in for the real thing." (test-flan--check "and C-x C-e does not, for the reason it never has" (null (flan--declaration-before-point)))) + ;; And every other head does agree, which is the half that had rotted: the + ;; list was topped up by name and so lost `defdata' and all four object and + ;; dispatch heads for as long as they had existed. A head missing from it + ;; is not a quiet fallback — the form goes to the expression evaluator and + ;; the daemon refuses it with "is a top-level declaration, not an + ;; expression", which is the complaint this is here to keep from coming + ;; back. Read off `Parse.decl', so the check is the derivation. + (with-temp-buffer + (flan-mode) + (dolist (head '("defmacro" "defn" "defvar" "defconst" "defstruct" + "defdata" "defunion" "defenum" "defalias" + "defclass" "defgeneric" "defmulti" "defmethod" + "import" "declare" "declare-c")) + (erase-buffer) + ;; A body neither key needs to be valid: both questions are asked of the + ;; head alone, and a form that parses is a different test. + (insert "(" head " thing [x])\n") + (goto-char (point-min)) + (search-forward "thing") + (test-flan--check (format "C-c C-c installs a %s form" head) + (equal (flan--declaration-head-at + (car (flan--defun-bounds)) flan--defun-heads) + head)) + (goto-char (point-max)) + (test-flan--check (format "and C-x C-e installs one too, for %s" head) + (equal (car (flan--declaration-before-point)) head)))) + ;; A prefix on the expression path is a flag and not a position — there is ;; no inside for one to point at — so both `C-u' and `C-u C-u' mean the one ;; thing, which is `C-u C-x C-e's behaviour reached through the other key.