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.
This commit is contained in:
Joseph Ferano 2026-09-19 03:49:27 +07:00
parent d757fd23f5
commit 9417fdec42
3 changed files with 557 additions and 148 deletions

View File

@ -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 |

View File

@ -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 "<buffer>"))
(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 "<buffer>"))
(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 "<buffer>"))
(when arg (list :pause t))))
"expression")))))
(flan--eval-expression start (point) arg)))))
;;;###autoload
(defun flan-eval-region (start end)

View File

@ -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 "\\_<step\\_>" 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 ──────────────────────────────────────────