diff --git a/emacs/flan-dev.el b/emacs/flan-dev.el index ac512e1..689ab66 100644 --- a/emacs/flan-dev.el +++ b/emacs/flan-dev.el @@ -1794,14 +1794,37 @@ 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." +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))) - (and (zerop (car (syntax-ppss start))) + (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-dev--declaration-heads) @@ -1846,9 +1869,20 @@ does, until the same form is evaluated again without a prefix." ;; 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)))) + (let* ((start (save-excursion + (condition-case nil (backward-sexp) (scan-error nil)) + (point))) + (code (buffer-substring-no-properties start (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 + ;; the empty string — which it answers, since an empty program is a + ;; valid one, and the echo area then reports a success for an + ;; evaluation nobody made. Saying so is the honest answer, and it is + ;; also the answer to the likelier reading: point is at the top of the + ;; file and the form meant was the one *after* it. + (when (= start (point)) + (user-error "flan: no form before point to evaluate")) (flan-dev--report (flan-dev--request (append diff --git a/emacs/test-flan-dev.el b/emacs/test-flan-dev.el index b29db9b..feac0de 100644 --- a/emacs/test-flan-dev.el +++ b/emacs/test-flan-dev.el @@ -486,6 +486,41 @@ is written instead — the real `message' call the real command makes." (string-match-p "=>" said)))) (delete-region beg (point-max))) + ;; A declaration can be written where it is prose and not a declaration, and + ;; the depth at its open delimiter says nothing about that: a form at column + ;; 1 inside a comment or a string is at depth 0 like any other, so the head + ;; was the only thing being asked and a sentence would have been compiled. + (goto-char (point-max)) + (let ((beg (point))) + (insert "\n;; (defvar commented i64 1)\n\"(defvar inside i64 1)\"") + ;; Point after the form's own closing paren rather than at the end of the + ;; line: `backward-sexp' walks over a whole comment, so from the end of + ;; one it never reaches the paren inside it and the guard is never asked. + (goto-char beg) + (search-forward "commented i64 1)") + (test-flan--check "a declaration written in a comment is not one" + (null (flan-dev--declaration-before-point))) + (goto-char beg) + (search-forward "inside i64 1)") + (test-flan--check "and neither is one written in a string" + (null (flan-dev--declaration-before-point))) + (delete-region beg (point-max))) + + ;; And the top of the buffer, where there is nothing behind point at all. + ;; `backward-sexp' does not signal there — it stays where it is — so the + ;; form the predicate looked at was the one *after* point and the region it + ;; measured out was empty: C-x C-e at point-min sent the first declaration + ;; in the file by name with no body at all, and the daemon installed it. + ;; Nothing is sent now, from either path, and the refusal says why. + (goto-char (point-min)) + (test-flan--check "the form after point is not the form before it" + (null (flan-dev--declaration-before-point))) + (test-flan--check "and C-x C-e at the top of a buffer refuses rather than sending nothing" + (let ((raised nil)) + (condition-case err (flan-eval-last-sexp) + (user-error (setq raised (error-message-string err)))) + (and raised (string-match-p "no form before point" raised)))) + ;; 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