From 9417fdec426418150069dcd789e9430e3ad48988 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 03:49:27 +0700 Subject: [PATCH 1/2] C-c C-c evaluates what is not a declaration, and shows a value where it was The routing C-x C-e already had, asked of the form C-c C-c already picks: the top-level form point is in, not the one behind it, so reloading the defn you are standing in is untouched. Both keys are one command and both route -- a person who writes (+ 1 1) and presses C-c C-c has the complaint that started this, and splitting them would buy a rule to remember and nothing else. A prefix on the expression path is a flag, the way C-u C-x C-e's already is: there is no inside for a position to point at, so C-u and C-u C-u say the one thing, and it does not stick because the thunk is thrown away. package is where the two keys disagree, which is the one real finding here. Parse.expr has no arm refusing it, so C-x C-e's question answers no; Parse.decl has one, so C-c C-c must keep installing it. flan--defun-heads is that list plus that head. The value now goes beside the form, in flan-watch's ghost shape and with the error overlay's lifetime, because eros and CIDER put it there and the echo area cannot say which of two forms returned 2. flan-inline-result turns it off; where the two would overlap the overlay wins and the echo is not also written. A refusal clears it rather than sitting beside it. The expression path sends flan--text-at now: an expression does not start at column 1, and an unpadded snippet put its refusal on line 1 of the file. --- emacs/MANUAL.md | 40 +++- emacs/flan.el | 465 ++++++++++++++++++++++++++++++++------------- emacs/test-flan.el | 200 ++++++++++++++++++- 3 files changed, 557 insertions(+), 148 deletions(-) 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..493033a 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,191 @@ 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 (flan--eval-expression (car b) (cdr b) 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) (cdr b)))))) ;;;###autoload (defun flan-eval-buffer () @@ -1895,81 +2178,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 +2192,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 +2217,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 +2230,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..624ab3d 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,176 @@ 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))) + (insert "\n(+ 1 1)") + ;; 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))))) + ;; 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. + (test-flan--check "and marked where it is written, not at line 1" + (let ((ovs (flan--error-overlays))) + (and ovs (> (line-number-at-pos + (overlay-start (car ovs))) + 1)))) + (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 +861,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 +897,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 ────────────────────────────────────────── From 8d64eda2d43882e1569a3d7da430e1109a5ac693 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 19 Sep 2026 03:52:41 +0700 Subject: [PATCH 2/2] end-of-defun steps over the newline, and the value was drawn on the next line lisp.el finishes end-of-defun by skipping blanks and stepping over the newline after the closing paren, so the bounds C-c C-c works from end at the start of the next line. The declaration path never cared -- a newline more or less in the text sent changes nothing -- but a value drawn at that position sits in column 0 below the form, in the gap before the next one. The fixture that missed it inserted the expression at point-max with nothing after it, which is the one shape where end-of-defun has no newline to step over. It has one now, and the check is the closing paren rather than a line number. The error-overlay check was the same kind of weak: it asserted the line was not 1, which flan--text alone would have given. It asserts the token now, which is what flan--text-at is for -- and eval-expr's replies do respect the column padding, so the switch stands. --- emacs/flan.el | 15 +++++++++++++-- emacs/test-flan.el | 28 ++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/emacs/flan.el b/emacs/flan.el index 493033a..3628b48 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -2156,12 +2156,23 @@ declaration for it to live in." ;; 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 (flan--eval-expression (car b) (cdr b) arg) + (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) (cdr b)))))) + (pulse-momentary-highlight-region (car b) end)))))) ;;;###autoload (defun flan-eval-buffer () diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 624ab3d..99d8c18 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -551,7 +551,11 @@ already rely on it — so nothing here is a stand-in for the real thing." ;; 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))) - (insert "\n(+ 1 1)") + ;; 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. @@ -560,6 +564,15 @@ already rely on it — so nothing here is a stand-in for the real thing." (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. @@ -657,12 +670,15 @@ already rely on it — so nothing here is a stand-in for the real thing." (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. - (test-flan--check "and marked where it is written, not at line 1" + ;; 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 (> (line-number-at-pos - (overlay-start (car ovs))) - 1)))) + (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)))