C-x C-e on a top-level form installs it

# Conflicts:
#	emacs/flan-dev.el
This commit is contained in:
Joseph Ferano 2026-09-17 19:10:11 +07:00
commit dd7a4eaea7
2 changed files with 191 additions and 26 deletions

View File

@ -22,13 +22,22 @@
;; reset between runs — it is what calling (main) at a Common Lisp or Clojure
;; prompt does, and the modeline says `flan:parked' while it waits.
;;
;; C-x C-e evaluates the expression before point *in the running program* and
;; shows its value. That is a different primitive from redefining a name:
;; there is nothing to install a body into, so the expression is wrapped in a
;; thunk the program runs at its next frame boundary. Only scalars, bool and
;; strings render so far — a Flan value carries no header, so a printer has to
;; be derived per type at compile time, and the ones that are not derived yet
;; say so rather than guessing.
;; C-x C-e evaluates the form before point *in the running program*. An
;; expression is a different primitive from redefining a name: there is
;; nothing to install a body into, so it is wrapped in a thunk the program
;; runs at its next frame boundary, and its value is shown. Only scalars,
;; bool and strings render so far — a Flan value carries no header, so a
;; printer has to be derived per type at compile time, and the ones that are
;; not derived yet say so rather than guessing.
;;
;; A *declaration* before point takes the other path and is installed, the way
;; C-c C-c installs one. The key is dispatched on the form it would send, so
;; a `defvar' at the top of a file evaluates as a declaration while an
;; expression inside a `defn' body still evaluates as an expression. The
;; split used to be by keybinding, which meant a top-level `defvar' under
;; C-x C-e came back as "defvar is a top-level declaration, not an
;; expression" — an editor artifact, not a limit of the compiler, which has
;; had both evaluators all along.
;;; Code:
@ -1485,10 +1494,20 @@ of the tenth name tells you neither how many there were nor which."
;; `: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-dev--names-phrase fns what)
(flan-dev--names-phrase (or fns names) what)
(or (plist-get reply :ms) 0)
(let ((vars (seq-difference names fns)))
(let ((vars (and fns (seq-difference names fns))))
(if vars (format " (also %s)"
(flan-dev--names-phrase vars ""))
"")))))))
@ -1636,29 +1655,105 @@ arrive in the same load or the first refers to storage that does not exist."
;; buffer is not feedback, it is a flicker.
(flan-dev-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-dev--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-dev--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."
(save-excursion
(let ((end (point)))
(condition-case nil
(progn
(backward-sexp)
(let ((start (point)))
(and (zerop (car (syntax-ppss start)))
(looking-at "([ \t\n]*\\(\\(?:\\sw\\|\\s_\\)+\\)")
(member (match-string-no-properties 1)
flan-dev--declaration-heads)
(list (match-string-no-properties 1) start end))))
(scan-error nil)))))
;;;###autoload
(defun flan-eval-last-sexp (&optional arg)
"Evaluate the expression before point in the running program and show it.
"Evaluate the form before point in the running program and report it.
With a prefix ARG, stop at it instead: the expression is wrapped in a
An expression is compiled into a thunk the program runs at its next frame
boundary, and its value is shown. A top-level declaration a `defvar', a
`defn', anything in `flan-dev--declaration-heads' is compiled and installed
instead, and the reply names what changed. The compiler has always had both
paths; this key used to reach only the first, so a `defvar' typed at the top
of a file came back as \"defvar is a top-level declaration, not an
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-dev--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.
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
`(pause)' before it is checked, so the thunk breaks where it stands and the
break loop gets the frame. A flag rather than a position, because the
expression sent *is* the target and because this path sends a raw
substring, so buffer line numbers would not survive it anyway.
break loop gets the frame. For a declaration it is a position, because a
declaration has an inside: the mark goes on the form itself, which the daemon
reads as stopping on entry, the same as `C-u C-u C-c C-c'.
It does not stick, and cannot: a thunk is built and thrown away, so there is
no declaration for the mark to live in. Nothing is drawn in the buffer for
the same reason."
An expression's mark does not stick and cannot a thunk is built and thrown
away, so there is no declaration for it to live in while a declaration's
does, until the same form is evaluated again without a prefix."
(interactive "P")
(let ((code (buffer-substring-no-properties
(save-excursion (backward-sexp) (point))
(point))))
(flan-dev--report
(flan-dev--request
(append
(list :op "eval-expr" :code code :file (or buffer-file-name "<buffer>"))
(when arg (list :pause t))))
"expression")))
(let ((decl (flan-dev--declaration-before-point)))
(if decl
(pcase-let ((`(,head ,start ,end) decl))
;; `flan-dev--text', not `flan-dev--text-at': a top-level form starts
;; at column 1, so the line padding is the whole fix and the columns
;; already agree. HEAD is what the echo area falls back to when the
;; daemon installed no bodies and declared no names.
(flan-dev--eval (flan-dev--text start end) head start end
(and arg (cons start end))))
(let ((code (buffer-substring-no-properties
(save-excursion (backward-sexp) (point))
(point))))
(flan-dev--report
(flan-dev--request
(append
(list :op "eval-expr" :code code :file (or buffer-file-name "<buffer>"))
(when arg (list :pause t))))
"expression")))))
;;;###autoload
(defun flan-eval-region (start end)

View File

@ -399,6 +399,76 @@ is written instead — the real `message' call the real command makes."
(string-match-p "HELLO" (buffer-string)))))
(test-flan--check "the program's output reaches its buffer" seen))
;; ── Which evaluator C-x C-e reaches ──────────────────────────────────
;;
;; The dispatch is off the form that would be *sent*, so these four are
;; checked with point where the key would be pressed rather than by handing
;; the predicate a string: a form's depth is a fact about the buffer, and an
;; off-by-one at the open delimiter would make every form read as nested,
;; leave the feature switched permanently off, and still pass a test that
;; only looked at the head. No daemon round trip here — nothing is sent —
;; which is why all four are affordable.
(goto-char (point-min))
(search-forward "(defvar ticks i64)")
(test-flan--check "a top-level defvar takes the declaration path"
(equal (car (flan-dev--declaration-before-point)) "defvar"))
(goto-char (point-min))
(search-forward " ticks)")
(test-flan--check "and so does a top-level defn"
(equal (car (flan-dev--declaration-before-point)) "defn"))
;; The case the whole "form, not cursor" rule exists for: this line is
;; inside `step', so the enclosing declaration is a `defn' — and evaluating
;; here must still mean this expression, not a reinstall of the function.
(goto-char (point-min))
(search-forward "(+ ticks 41)")
(test-flan--check "an expression inside a defn body does not"
(null (flan-dev--declaration-before-point)))
(goto-char (point-max))
(let ((beg (point)))
(insert "\n(+ 2 3)")
(test-flan--check "nor does a bare expression at top level"
(null (flan-dev--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.
(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))))
(delete-region beg (point-max)))
;; The bug this key had: a `defvar' typed at the top of a file could only be
;; evaluated with C-c C-c, because C-x C-e sent it to the expression
;; evaluator and the parser refused it as a declaration. One round trip, on
;; the form the report has to be able to name.
(goto-char (point-max))
(let ((beg (point)))
(insert "\n(defvar spark i64 9)")
(let ((said (test-flan--said (flan-eval-last-sexp))))
(test-flan--check "C-x C-e on a top-level defvar installs it"
(and said (string-match-p "\\_<spark\\_>" said)
(string-match-p "installed" said)))
;; The two paths share a key now, so the echo area is the only thing
;; left that says which of them ran. An installed declaration reports
;; what changed; an expression reports `=>' and a value.
(test-flan--check "and says so rather than printing a value"
(and said (not (string-match-p "=>" said)))))
(delete-region beg (point-max)))
;; And the literal complaint, on the `defvar' actually written in the file
;; rather than on one typed in for the occasion. What is asserted is only
;; what the client decides: the var is named and no value is printed.
;; Whether the daemon answers "installed" or "nothing to install" is its
;; call and depends on what this session has been through by now, and a test
;; that pinned one of them here would be testing the order of the checks
;; above it. A defvar declares no functions either way, which is what makes
;; the name — and not the kind of form — the subject of the sentence.
(goto-char (point-min))
(search-forward "(defvar ticks i64)")
(let ((said (test-flan--said (flan-eval-last-sexp))))
(test-flan--check "C-x C-e on the file's own defvar reports the var"
(and said (string-match-p "\\_<ticks\\_>" said)
(not (string-match-p "=>" said)))))
;; The REPL buffer: typed input goes through the same eval-expr request, and
;; the value lands at the prompt while the program's own output goes to
;; *flan-output*. Conflating those two is the bug worth testing for.