diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index e46bd06..fd8cf75 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -105,14 +105,36 @@ anything. The next time that function is called, the new one runs. It works on the *buffer text*, not the saved file, so you do not have to save first. +If the form point is in is not a declaration — a bare `(+ 1 1)` at column 1, +say — it is evaluated as an expression instead, and its value comes back the +way `C-x C-e`'s does. `C-M-x` is the same command on the binding SLIME and +CIDER use, and does the same thing: whatever is under it, the obvious thing. +Point *inside* a `defn` body still means the `defn` — the form chosen is the +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 printed in the echo area. Not a copy of the program, not a simulation — -the actual process, with its actual state. +its value shown beside it, as `=> 2` after the form you evaluated. 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. + +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. + +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 +which names changed instead of printing a value. + ### `C-u C-c C-c` — stop there The same key with a prefix argument **marks a form as a breakpoint**. `C-u C-c @@ -137,7 +159,10 @@ hit it as many times as you like; an ordinary `C-c C-c` over the same form (or `C-u C-x C-e` does the same for the expression before point: it stops *at* the expression instead of printing its value. That one does not stick, because there -is no definition for it to stick to. +is no definition for it to stick to. `C-u C-c C-c` on a top-level form that is +an expression rather than a declaration means exactly this — there is no inside +for a position to point at, so `C-u` and `C-u C-u` say the one thing there is to +say, and it does not stick either. ### `C-c C-m` — what a macro call expands to @@ -747,12 +772,12 @@ Use `C-c C-g` if you need frames. | Key | Does | |---|---| -| `C-c C-c` | the top-level form at point, recompiled and installed | +| `C-c C-c` | the top-level form at point: a declaration installed, anything else evaluated | | `C-u C-c C-c` | ...and stop at the form point is inside (`C-u C-u`: on entry) | | `C-M-x` | the same as `C-c C-c`, on the binding SLIME and CIDER use | | `C-c C-k` | the whole buffer, as one module | -| `C-x C-e` | the expression before point, evaluated in the running program | -| `C-u C-x C-e` | ...and stop at it instead of printing its value | +| `C-x C-e` | the form before point, evaluated — or installed, if it is a declaration | +| `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 | @@ -785,7 +810,8 @@ Commands with no key: `M-x flan` (start a program), `M-x flan-quit` |---|---|---| | `flan-command` | `"flan"` | the compiler binary | | `flan-socket-name` | `".flan-dev.sock"` | what `C-c C-z` searches for | -| `flan-echo-result` | `t` | print `C-x C-e`'s value in the echo area | +| `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-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 | diff --git a/emacs/flan.el b/emacs/flan.el index 52482d4..3628b48 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -88,6 +88,19 @@ Turning this off makes a successful evaluation indistinguishable from one 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. + +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." + :type 'boolean) + (defcustom flan-names-shown 4 "How many installed names to name before falling back to counting them." :type 'integer) @@ -1303,6 +1316,12 @@ Returns non-nil when it put an overlay somewhere." (when buf (with-current-buffer buf (flan-clear-errors buf) + ;; A refusal is not a value, and the two must never be drawn over + ;; one form at once. Ordinarily the command that ran this + ;; evaluation already cleared the last one through the hook; this + ;; is the case where it did not, because the value being replaced + ;; was drawn by *this* command in another buffer. + (flan-clear-result buf) (let* ((beg (flan--position (nth 1 parts) (nth 2 parts))) (end (save-excursion (goto-char beg) (line-end-position))) (ov (make-overlay beg end buf t nil))) @@ -1324,6 +1343,82 @@ Returns non-nil when it put an overlay somewhere." (when (eq buf (current-buffer)) (goto-char beg)) t))))))) +;;; Inline results + +;; The value of an expression, drawn after the form it came from, the way eros +;; draws one in Emacs Lisp and CIDER in Clojure. The echo area is the older +;; half of this and is still there; what it cannot do is answer "which form +;; was that?", which is the whole question when two expressions on adjacent +;; lines both return 2. +;; +;; The lifetime argument is the error overlay's, above, and it is the same +;; argument rather than a similar one: this is feedback about the evaluation +;; that just ran and not an annotation on the source, so the next command in +;; the buffer takes it away, and it is a `pre-command-hook' for the reason +;; given there — `post-command-hook' fires at the end of the command that drew +;; it, before redisplay has ever shown it. That is also the convention people +;; arrive with from eros. +;; +;; 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. + +(defface flan-result-face '((t :inherit shadow)) + "Face for an expression's value, shown beside the form it came from." + :group 'flan) + +(defun flan--result-overlays (&optional buffer) + "The Flan result overlays in BUFFER, or in the current buffer." + (with-current-buffer (or buffer (current-buffer)) + (seq-filter (lambda (o) (overlay-get o 'flan-result)) + (overlays-in (point-min) (point-max))))) + +(defun flan-clear-result (&optional buffer) + "Remove inline result overlays from BUFFER, or from the current buffer." + (interactive) + (with-current-buffer (or buffer (current-buffer)) + (remove-overlays (point-min) (point-max) 'flan-result t) + (remove-hook 'pre-command-hook #'flan--clear-result-on-command t))) + +(defun flan--clear-result-on-command () + "Take this buffer's result overlays down, as a `pre-command-hook'." + (flan-clear-result)) + +(defun flan--show-result (value at) + "Draw VALUE after position AT. Returns non-nil when it drew one. + +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." + (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))) + (overlay-put ov 'flan-result t) + (overlay-put ov 'after-string + (propertize (concat " => " value) + 'face 'flan-result-face + ;; Without this, point at end of line lands on + ;; the value rather than on the buffer's own + ;; last column. `flan-watch' found this first. + 'cursor t)) + (overlay-put ov 'evaporate nil) + ;; Under an error overlay, which is about one command and should win + ;; while it is up, and over a pause mark, which is an annotation on the + ;; program rather than on this evaluation. + (overlay-put ov 'priority 90) + (add-hook 'pre-command-hook #'flan--clear-result-on-command nil t) + t))) + ;;; Pause marks ;; The other overlay in this file, and deliberately not the same thing. An @@ -1705,8 +1800,12 @@ of the tenth name tells you neither how many there were nor which." (t (format "%d names (%s, …)" (length names) (string-join (seq-take names flan-names-shown) ", "))))) -(defun flan--report (reply what) - "Report REPLY, describing WHAT was sent." +(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." (if (equal (plist-get reply :status) "ok") (let ((fns (plist-get reply :fns)) (names (plist-get reply :names)) @@ -1721,41 +1820,52 @@ of the tenth name tells you neither how many there were nor which." ;; ...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))) - (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 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 "")) + "")))))))) ;; 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))) + ;; 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 + ;; beside a form that was just refused is the lie this prevents. + (ignore-errors (flan-clear-result)) (ignore-errors (flan--show-error loc (or msg "rejected"))) (user-error "flan: %s%s" (or msg "rejected") (if loc (format " (%s)" loc) ""))))) @@ -1776,7 +1886,10 @@ mark is drawn over here. Nothing is inserted in the buffer; see docs/DISCUSS.md (list :op "eval" :code code :file (or buffer-file-name "")) (when pause (list :pause (flan--wire-position (car pause)))))))) - (flan--report reply what) + ;; END as the place a value could go. Every caller of this sends a + ;; declaration and declarations have no value, so this is the path that + ;; stays open rather than one anybody takes today. + (flan--report reply what end) ;; `flan--report' signals on a rejection, so reaching here means it ;; landed. Flashing the text that was sent answers "which form did that ;; take?" — the question the echo area cannot, because point may be nowhere @@ -1864,21 +1977,202 @@ daemon reads that as stopping on entry instead." (scan-error nil))) b)))) +;; Which of the two evaluators a key runs is decided by the form it would +;; send, not by where the cursor is sitting. Point inside a `defn' body, on +;; `(+ ticks 1)', must still evaluate that expression under `C-x C-e' — the +;; enclosing `defn' is not what was asked for and reinstalling it would be a +;; different command. `C-c C-c' picks a different form, and asks the same +;; 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. +;; +;; `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. +(defconst flan--declaration-heads + '("defmacro" "defn" "defvar" "defconst" "defstruct" "defunion" "defenum" + "defalias" "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. +(defconst flan--defun-heads (cons "package" flan--declaration-heads) + "Heads `C-c C-c' recompiles and installs rather than evaluating.") + +(defun flan--declaration-head-at (pos &optional heads) + "HEAD when the form starting at POS is one of HEADS, at top level, else nil. +HEADS defaults to `flan--declaration-heads'. + +Two questions, and both have to answer yes. The depth at POS says whether +the form is top-level — `syntax-ppss' at the open delimiter reports the depth +*before* it, so a form nobody has nested reads as 0 — and that is what keeps +an inner expression inside a `defn' body on the expression path even when the +enclosing form is a declaration. The head then says whether the thing is a +declaration at all, which is what leaves a bare `(+ 1 1)' at column 1 alone. + +Requiring both rather than the head alone is the more conservative of the two +readings, and the one the user asked for: a `defn' written inside a `let' is +not a definition of anything, and installing it as one would quietly accept +code the compiler is right to refuse. Sent as an expression it gets the +parser's own message, which says exactly that. + +The remaining condition is that the form is not inside a string or a comment, +where a `defn' is prose and not a definition. The depth says nothing about +that: a form at column 1 inside a comment is at depth 0 like any other. + +One known limit, left as one: `syntax-ppss' reports depth from the accessible +portion of the buffer, so in a narrowed buffer a form that is nested in the +file reads as top-level here. Widening to ask would be a different decision +about what `C-x C-e' means in a narrowed buffer, and it is not this one's to +make." + (save-excursion + ;; Every `syntax-ppss' call before the `looking-at' below: it moves point + ;; and clobbers the match data, and the head is read back out of that + ;; match. + (let ((state (syntax-ppss pos))) + (goto-char pos) + (and (zerop (car state)) + (not (nth 3 state)) ; inside a string + (not (nth 4 state)) ; inside a comment + (looking-at "([ \t\n]*\\(\\(?:\\sw\\|\\s_\\)+\\)") + (member (match-string-no-properties 1) + (or heads flan--declaration-heads)) + (match-string-no-properties 1))))) + +(defun flan--declaration-before-point () + "The top-level declaration `C-x C-e' would send, as (HEAD START END), or nil. + +`flan--declaration-head-at' asks the two questions; this one finds the form to +ask them about, and has a third of its own before either is worth asking: +there must *be* a form before point. `backward-sexp' does not signal when +there is nothing behind it — it goes to the beginning of the buffer and stays +there, which at point-min is no movement at all — and the form it then looks +at is the one *after* point, whose head is very likely a declaration and whose +text is empty. `C-x C-e' at the top of a file used to install that: a `defn' +by name, with no body, out of a region zero characters wide. So START must +have moved." + (save-excursion + (let ((end (point))) + (condition-case nil + (progn + (backward-sexp) + (let ((start (point))) + (and (> end start) + (let ((head (flan--declaration-head-at start))) + (and head (list head start end)))))) + (scan-error nil))))) + +(defun flan--eval-expression (start end arg) + "Evaluate START..END in the running program as an expression, and report it. + +One function and not one per key, because \"the two keys do the same thing on +an expression\" is the whole of what routing buys: two call sites assembling +their own `eval-expr' request is exactly how they drift apart again. + +ARG is a flag rather than a position. The expression sent *is* the target, so +the daemon wraps it in a `(pause)' before checking it and the thunk breaks +where it stands; there is no inside for a position to point at, which is also +why one `C-u' and two mean the same thing here. + +`flan--text-at' rather than `flan--text': an expression is not a top-level +form and does not start at column 1, so a refusal the daemon reports against +it carries a column measured from the start of the snippet. Padding both ways +is what puts the error overlay on the character it is about — and the overlay +this draws on success would otherwise be competing with one drawn at line 1." + (flan--report + (flan--request + (append + (list :op "eval-expr" :code (flan--text-at start end) + :file (or buffer-file-name "")) + (when arg (list :pause t)))) + "expression" + end)) + ;;;###autoload (defun flan-eval-defun (&optional arg) - "Recompile the top-level form at point and install it in the running program. + "Evaluate the top-level form at point in the running program. -With a prefix ARG, also mark a form inside it so the program stops there when -it next runs — `C-u' the form point is inside, `C-u C-u' the top-level form -itself, which means stopping on entry. The buffer is not edited: the daemon -is told where the form is and splices the call in after parsing, so every -location in the file stays where it was. The mark sticks until the same form -is evaluated without a prefix." +A declaration — a `defn', a `defvar', anything in `flan--defun-heads' — is +recompiled and installed, which is what this key has always done. Anything +else is an expression, and is evaluated and its value shown, because a bare +`(+ 1 1)' written at the top of a file is a form like any other and refusing +it was an editor artifact rather than a property of the language. Every other +Lisp's `C-M-x' does the natural thing with whatever is under it, and so does +this one. + +`C-c C-c' and `C-M-x' are one command and both route. Splitting them would +mean two docstrings and a rule to remember, and nothing is bought by it: +somebody who writes an expression and presses `C-c C-c' has exactly the +complaint that made this change, and `C-c C-c' with point inside a `defn' is +untouched either way — the form picked is still the enclosing declaration, and +`C-c C-k' is still the key that means \"declarations, all of them\". + +With a prefix ARG on the declaration path, also mark a form inside it so the +program stops there when it next runs — `C-u' the form point is inside, `C-u +C-u' the top-level form itself, which means stopping on entry. The buffer is +not edited: the daemon is told where the form is and splices the call in after +parsing, so every location in the file stays where it was. The mark sticks +until the same form is evaluated without a prefix. + +On the expression path a prefix is a flag and cannot be anything else, exactly +as it is for `C-u C-x C-e': the expression sent is the target, and the mark +does not stick because a thunk is built and thrown away, leaving no +declaration for it to live in." (interactive "P") (let* ((b (flan--defun-bounds)) - (pause (flan--pause-bounds b arg))) - (flan--eval (flan--text (car b) (cdr b)) "form" (car b) (cdr b) - pause))) + ;; The head is asked of the form's *start* rather than of point, so + ;; the question is about the form `C-c C-c' already picked and the + ;; selection rule is untouched. `flan--defun-bounds' takes the form + ;; point is inside; `flan--declaration-before-point' takes the one + ;; behind point, and swapping this to that would silently turn + ;; `C-c C-c' from the middle of a `defn' body into `C-x C-e'. + (head (and (< (car b) (cdr b)) + (flan--declaration-head-at (car b) flan--defun-heads)))) + (cond + (head (flan--eval (flan--text (car b) (cdr b)) "form" (car b) (cdr b) + (flan--pause-bounds b arg))) + ;; An empty buffer, or point past the last form in one with nothing at + ;; all behind it: `beginning-of-defun' and `end-of-defun' both stay put + ;; and the bounds come back zero characters wide. Sending that asks the + ;; daemon to evaluate the empty string, which it answers — an empty + ;; program is a valid one — and the echo area then reports a success for + ;; an evaluation nobody made. The same refusal `C-x C-e' gives. + ((= (car b) (cdr b)) + (user-error "flan: no top-level form at point to evaluate")) + (t + ;; `end-of-defun' does not stop at the closing paren: `lisp.el' skips + ;; the blanks after it and then steps over the newline, so END is the + ;; 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. + (let ((end (save-excursion (goto-char (cdr b)) + (skip-chars-backward " \t\n") + (point)))) + (flan--eval-expression (car b) end arg) + ;; Flashed for the reason the declaration path flashes: point may be + ;; nowhere near the form `beginning-of-defun' actually found, and the + ;; value alone does not say which one that was. Only on success — + ;; `flan--report' signals on a rejection. + (pulse-momentary-highlight-region (car b) end)))))) ;;;###autoload (defun flan-eval-buffer () @@ -1895,81 +2189,6 @@ arrive in the same load or the first refers to storage that does not exist." ;; buffer is not feedback, it is a flicker. (flan-clear-pause)) -;; Which of the two evaluators `C-x C-e' runs is decided by the form it would -;; send, not by where the cursor is sitting. Point inside a `defn' body, on -;; `(+ ticks 1)', must still evaluate that expression — the enclosing `defn' is -;; not what was asked for and reinstalling it would be a different command. -;; -;; 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. -;; -;; `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 being fixed; a package -;; declaration is a statement about the file being read and not a change to -;; make to a running program. -(defconst flan--declaration-heads - '("defmacro" "defn" "defvar" "defconst" "defstruct" "defunion" "defenum" - "defalias" "import" "declare" "declare-c") - "Heads whose form is a declaration, and never an expression.") - -(defun flan--declaration-before-point () - "The top-level declaration `C-x C-e' would send, as (HEAD START END), or nil. - -Two questions, and both have to answer yes. The depth at START says whether -the form is top-level — `syntax-ppss' at the open delimiter reports the depth -*before* it, so a form nobody has nested reads as 0 — and that is what keeps -an inner expression inside a `defn' body on the expression path even when the -enclosing form is a declaration. The head then says whether the thing is a -declaration at all, which is what leaves a bare `(+ 1 1)' at column 1 alone. - -Requiring both rather than the head alone is the more conservative of the two -readings, and the one the user asked for: a `defn' written inside a `let' is -not a definition of anything, and installing it as one would quietly accept -code the compiler is right to refuse. Sent as an expression it gets the -parser's own message, which says exactly that. - -Three things have to be true before either question is worth asking, and the -first of them is that there *is* a form before point. `backward-sexp' does -not signal when there is nothing behind it — it goes to the beginning of the -buffer and stays there, which at point-min is no movement at all — and the -form it then looks at is the one *after* point, whose head is very likely a -declaration and whose text is empty. `C-x C-e' at the top of a file used to -install that: a `defn' by name, with no body, out of a region zero characters -wide. So START must have moved. The other two are that the form is not -inside a string or a comment, where a `defn' is prose and not a definition. - -One known limit, left as one: `syntax-ppss' reports depth from the accessible -portion of the buffer, so in a narrowed buffer a form that is nested in the -file reads as top-level here. Widening to ask would be a different decision -about what `C-x C-e' means in a narrowed buffer, and it is not this one's to -make." - (save-excursion - (let ((end (point))) - (condition-case nil - (progn - (backward-sexp) - (let* ((start (point)) - ;; Every `syntax-ppss' call before the `looking-at' below: - ;; it moves point and clobbers the match data, and the head - ;; is read back out of that match. - (state (syntax-ppss start))) - (and (> end start) - (zerop (car state)) - (not (nth 3 state)) ; inside a string - (not (nth 4 state)) ; inside a comment - (looking-at "([ \t\n]*\\(\\(?:\\sw\\|\\s_\\)+\\)") - (member (match-string-no-properties 1) - flan--declaration-heads) - (list (match-string-no-properties 1) start end)))) - (scan-error nil))))) - ;;;###autoload (defun flan-eval-last-sexp (&optional arg) "Evaluate the form before point in the running program and report it. @@ -1984,9 +2203,10 @@ expression\" and the only way to evaluate it was `C-c C-c'. That was an editor artifact and not a property of the language. `flan--declaration-before-point' decides which, off the form that would be -sent rather than off where point is; `C-c C-c' is unchanged and stays the -explicit \"reload the definition I am standing in\" command, which is still -the one to use from inside a body. +sent rather than off where point is. `C-c C-c' routes the same way now, but +off a different form — the one point is *inside* — so it stays the explicit +\"reload the definition I am standing in\" command and is still the one to use +from inside a body. With a prefix ARG, stop there instead. For an expression that is a flag and not a position — the expression sent *is* the target, and it is wrapped in a @@ -2008,10 +2228,9 @@ does, until the same form is evaluated again without a prefix." ;; daemon installed no bodies and declared no names. (flan--eval (flan--text start end) head start end (and arg (cons start end)))) - (let* ((start (save-excursion - (condition-case nil (backward-sexp) (scan-error nil)) - (point))) - (code (buffer-substring-no-properties start (point)))) + (let ((start (save-excursion + (condition-case nil (backward-sexp) (scan-error nil)) + (point)))) ;; Nothing behind point is nothing to send. `backward-sexp' does not ;; signal at the beginning of a buffer, it simply stays there, so the ;; region measured out is empty and the daemon is asked to evaluate @@ -2022,12 +2241,7 @@ does, until the same form is evaluated again without a prefix." ;; file and the form meant was the one *after* it. (when (= start (point)) (user-error "flan: no form before point to evaluate")) - (flan--report - (flan--request - (append - (list :op "eval-expr" :code code :file (or buffer-file-name "")) - (when arg (list :pause t)))) - "expression"))))) + (flan--eval-expression start (point) arg))))) ;;;###autoload (defun flan-eval-region (start end) diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 9b30061..99d8c18 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -30,6 +30,17 @@ is written instead — the real `message' call the real command makes." (advice-remove 'message probe)) said)) +(defun test-flan--result () + "The text of the inline result overlay in this buffer, or nil. +An expression's value now goes beside the form rather than into the echo +area, so this is where the assertions that used to read `test-flan--said' +read instead. Overlays exist under --batch — the error-overlay checks below +already rely on it — so nothing here is a stand-in for the real thing." + (let ((ovs (flan--result-overlays))) + (and (= 1 (length ovs)) + (substring-no-properties + (or (overlay-get (car ovs) 'after-string) ""))))) + (defun test-flan--check (name ok) (if ok (message " ok %s" name) (setq test-flan--failures (1+ test-flan--failures)) @@ -484,11 +495,192 @@ is written instead — the real `message' call the real command makes." (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. + ;; 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. (let ((said (test-flan--said (flan-eval-last-sexp)))) - (test-flan--check "which C-x C-e evaluates and prints" - (and said (string-match-p "5" said) - (string-match-p "=>" said)))) + (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" + (let ((ovs (flan--result-overlays))) + (and (= 1 (length ovs)) + (= (overlay-start (car ovs)) (point-max))))) + ;; It lasts exactly as long as the evaluation it is about. The hook is + ;; buffer-local and installed only while there is something to remove, + ;; the way the error overlays' is. + (test-flan--check "and a command in the buffer takes it away" + (progn + (test-flan--check + "with a hook installed while it is up" + (memq #'flan--clear-result-on-command + (buffer-local-value 'pre-command-hook + (current-buffer)))) + (flan--clear-result-on-command) + (and (null (flan--result-overlays)) + (not (memq #'flan--clear-result-on-command + (buffer-local-value + 'pre-command-hook + (current-buffer)))))))) + (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. + (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" + (and said (string-match-p "15" said) + (string-match-p "=>" said) + (null (flan--result-overlays))))) + (delete-region beg (point-max))) + + ;; ── C-c C-c on what is not a declaration ───────────────────────────── + ;; + ;; The complaint this routing came from: `(+ 1 1)' written at the top level + ;; of a buffer, `C-M-x' pressed, and the parser's "is a top-level + ;; declaration, not an expression" coming back — because the key only ever + ;; had the one path. `C-M-x' and `C-c C-c' are one command and both route. + (goto-char (point-max)) + (let ((beg (point))) + ;; The trailing newline is the point, not tidiness: `end-of-defun' steps + ;; over it, so the bounds it hands back end at the start of the next line + ;; and a value drawn at that position appears in column 0 below the form. + ;; Without the newline here the bug is invisible. + (insert "\n(+ 1 1)\n") + ;; Point *inside* the form, which is where `C-c C-c' is pressed from and + ;; the difference between this and `C-x C-e': the form is the one point is + ;; in, not the one behind it. + (search-backward "1 1") + (test-flan--check "C-M-x on a bare top-level expression evaluates it" + (progn (flan-eval-defun) + (let ((r (test-flan--result))) + (and r (string-match-p "=> 2" r))))) + (test-flan--check "and draws the value after the closing paren" + (let ((ovs (flan--result-overlays))) + (and (= 1 (length ovs)) + (= (char-before (overlay-start (car ovs))) ?\)) + (= (line-number-at-pos (overlay-start (car ovs))) + (line-number-at-pos + (save-excursion + (goto-char (point-min)) + (search-forward "(+ 1 1)"))))))) + ;; What a keystroke would do. There are no commands under --batch, so the + ;; `pre-command-hook' that takes a value down never runs and the next + ;; check below would be reading this one's overlay. + (flan-clear-result) + (goto-char (point-max)) + (delete-region beg (point-max))) + + ;; And the other half, which is the one that must not have moved: point in + ;; the middle of a `defn' body still reinstalls the enclosing declaration. + ;; `flan--defun-bounds' takes the form point is *inside* and + ;; `flan--declaration-before-point' the one *before* point, and routing + ;; C-c C-c off the second would have turned this into C-x C-e. + (goto-char (point-min)) + (search-forward "(+ ticks 41)") + (test-flan--check "the head is asked of the form point is inside" + (equal (flan--declaration-head-at + (car (flan--defun-bounds))) + "defn")) + (let ((said (test-flan--said (flan-eval-defun)))) + (test-flan--check "C-c C-c from inside a defn body still installs it" + (and said (string-match-p "\\_" said) + (string-match-p "installed" said))) + (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. + (with-temp-buffer + (flan-mode) + (insert "(package demo)\n") + (goto-char (point-min)) + (search-forward "demo") + (test-flan--check "C-c C-c treats a package form as a declaration" + (equal (flan--declaration-head-at + (car (flan--defun-bounds)) flan--defun-heads) + "package")) + (goto-char (point-max)) + (test-flan--check "and C-x C-e does not, for the reason it never has" + (null (flan--declaration-before-point)))) + + ;; 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. + ;; + ;; Checked by reading the request rather than by sending it: `:pause t' on a + ;; thunk parks the program in the break loop, and doing that here would hand + ;; every test below this line a stopped daemon. What the daemon then does + ;; with the flag is test_dev.ml's question anyway; this one is only whether + ;; the client sends a flag where it cannot send a position. + (goto-char (point-max)) + (let ((beg (point)) + (sent nil)) + (insert "\n(+ 3 4)") + (search-backward "3 4") + (let ((probe (lambda (req) (setq sent req) '(:status "ok" :value "7")))) + (advice-add 'flan--request :override probe) + (unwind-protect + (progn + (flan-eval-defun '(4)) + (test-flan--check "C-u C-M-x on an expression sends the flag" + (and (equal (plist-get sent :op) "eval-expr") + (eq (plist-get sent :pause) t))) + (flan-eval-defun '(16)) + (test-flan--check "and C-u C-u means the same, having nowhere else to point" + (eq (plist-get sent :pause) t)) + (flan-eval-defun) + (test-flan--check "and no prefix sends no flag at all" + (null (plist-member sent :pause)))) + (advice-remove 'flan--request probe))) + (test-flan--check "none of which leaves a pause mark behind" + (null (flan--pause-overlays))) + (flan-clear-result) + (goto-char (point-max)) + (delete-region beg (point-max))) + + ;; A refusal must never be drawn as a value. This is the collision the two + ;; overlays could have had: one command, one form, and both paths wanting + ;; the end of the same line. + (goto-char (point-max)) + (let ((beg (point))) + (insert "\n(+ 1 nonsense)") + (search-backward "nonsense") + (ignore-errors (flan-eval-defun)) + (test-flan--check "a refused expression gets no value overlay" + (null (flan--result-overlays))) + (test-flan--check "and is marked as an error instead" + (let ((ovs (flan--error-overlays))) + (and (= 1 (length ovs)) + (string-match-p + "unknown name" + (or (overlay-get (car ovs) 'help-echo) ""))))) + ;; The padding argument, which is why the expression path sends + ;; `flan--text-at': the daemon numbers from the start of what it was sent, + ;; so an unpadded snippet puts this overlay on line 1 of the file. The + ;; column is the half `flan--text' alone would not have fixed, so the + ;; check is the token and not the line — a client that padded lines only + ;; would pass "not line 1" and still point at the open paren. + (test-flan--check "and marked at the word it is about, not at line 1" + (let ((ovs (flan--error-overlays))) + (and ovs (save-excursion + (goto-char (overlay-start (car ovs))) + (looking-at-p "nonsense"))))) + (flan-clear-errors) + (goto-char (point-max)) (delete-region beg (point-max))) ;; A declaration can be written where it is prose and not a declaration, and @@ -685,9 +877,11 @@ is written instead — the real `message' call the real command makes." (goto-char (point-max)) (let ((beg (point))) (insert "\n(+ 20 3)") - (let ((said (test-flan--said (flan-eval-last-sexp)))) - (test-flan--check "C-x C-e works while the program is stopped" - (and said (string-match-p "23" said)))) + (flan-eval-last-sexp) + (test-flan--check "C-x C-e works while the program is stopped" + (let ((r (test-flan--result))) + (and r (string-match-p "23" r)))) + (flan-clear-result) (delete-region beg (point-max))) ;; And installing, which the break loop allows on purpose: there is no frame @@ -719,9 +913,11 @@ is written instead — the real `message' call the real command makes." (goto-char (point-max)) (let ((beg (point))) (insert "\n(+ 1 1)") - (let ((said (test-flan--said (flan-eval-last-sexp)))) - (test-flan--check "and everything works again afterwards" - (and said (string-match-p "2" said)))) + (flan-eval-last-sexp) + (test-flan--check "and everything works again afterwards" + (let ((r (test-flan--result))) + (and r (string-match-p "2" r)))) + (flan-clear-result) (delete-region beg (point-max))) ;; ── The documentation buffer ──────────────────────────────────────────