diff --git a/emacs/flan-dev.el b/emacs/flan-dev.el index da3335c..dd99912 100644 --- a/emacs/flan-dev.el +++ b/emacs/flan-dev.el @@ -15,13 +15,22 @@ ;; The protocol is one s-expression per message, length framed. That is why ;; there is no parser here: `prin1' writes a request and `read' reads a reply. ;; -;; 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: @@ -1405,10 +1414,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 "")) ""))))))) @@ -1556,29 +1575,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 "")) - (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 "")) + (when arg (list :pause t)))) + "expression"))))) ;;;###autoload (defun flan-eval-region (start end) diff --git a/emacs/test-flan-dev.el b/emacs/test-flan-dev.el index b59b658..112fecf 100644 --- a/emacs/test-flan-dev.el +++ b/emacs/test-flan-dev.el @@ -338,6 +338,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 "\\_" 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 "\\_" 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.