Skip to content

Commit

Permalink
Fix ProofGeneral#781 PG does not position to error.
Browse files Browse the repository at this point in the history
Tests is also added to check the exact position of the error
highlighting.

Description of the fix.

We cannot use the " ^^^ " thing to compute the error location because
commands are not stripped of newlines anymore (ProofGeneral#774).

Solution: use the character position information given in the error
message with the following subtleties:

- position are given by coq **bytes** positions.
- position given by coq is the position from the previous "." fed to
  coq. I.e. we must be careful to strip any blank after the final "." of
  a command.
- Coq < 8.11 has a small glitch on positions, the test is compatible
  with both behaviour.
- coq <= 8.20 has a bug on position for curly braces.

In the test we make the disappearing timeout of overlay bigger
so that tests don't fail on slow VMs (maybe useless).

NOTE: coq error locations are quite wrong when we insert
spaces (instead of a newline) after a command. PG was wrongly
inserting a space after Set Silent./ Unset Silent, which triggered
this problem. This is fixed by this PR. But other silent commends
should triped of trailing blanks. I leave it to another PR.
  • Loading branch information
Matafou committed Sep 5, 2024
1 parent eca47ea commit 16b70c9
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 57 deletions.
86 changes: 77 additions & 9 deletions ci/coq-tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,83 @@ For example, COMMENT could be (*test-definition*)"
(coq-test-full-path "test_stepwise.v")
(lambda ()
(coq-test-goto-before " (*test-lemma2*)")
(let ((proof-point (save-excursion (coq-test-goto-after "(*error*)"))))
(proof-goto-point)
(proof-shell-wait)
(coq-should-buffer-string "Error: Unable to unify \"false\" with \"true\".")
(should (equal (proof-queue-or-locked-end) proof-point))))))

;; redefining this function locally so that self removing spans
;; remain longer. Cf span.el
(cl-letf (((symbol-function 'span-make-self-removing-span)
(lambda (beg end &rest props)
(let ((ol (span-make beg end)))
(while props
(overlay-put ol (car props) (cadr props))
(setq props (cddr props)))
(add-timeout 10 'delete-overlay ol)
ol))))

(let ((proof-point (save-excursion (coq-test-goto-after "(*error*)") (point)))
(proof-cmd-point (save-excursion
(coq-test-goto-after "(*error*)")
(re-search-forward "reflexivity")
(re-search-backward "reflexivity"))))
(proof-goto-point)
(proof-shell-wait)
(coq-should-buffer-string "Error: Unable to unify \"false\" with \"true\".")
;; checking that there is an overlay with warning face exactly
;; on "reflexivity". WARNING: this overlat lasts only for 2
;; secs, if this test is done in a (very) slow virtual machine
;; this may fail.
(should (equal (point) proof-cmd-point))
(let ((sp (span-at proof-cmd-point 'face)))
(should sp)
(should (equal (span-property sp 'face) 'proof-warning-face))
(should (equal (span-start sp) proof-cmd-point))
;; coq-8.11 used to hace ending ps shifted by one
(should (or (equal (span-end sp) (+ proof-cmd-point (length "reflexivity")))
(equal (span-end sp) (+ 1 proof-cmd-point (length "reflexivity")))))
)
(should (equal (proof-queue-or-locked-end) proof-point)))))))


;; Testing the error location for curly braces specifically. Coq bug
;; #19355 (coq <= 8.20) needs to be worked around.
(ert-deftest 51_coq-test-error-highlight ()
"Test error highlghting for curly brace."
(coq-fixture-on-file
(coq-test-full-path "test_error_loc_1.v")
(lambda ()
(coq-test-goto-before " (*test-lemma*)")
;; redefining this function locally so that self removing spans
;; remain longer. Cf span.el
(cl-letf (((symbol-function 'span-make-self-removing-span)
(lambda (beg end &rest props)
(let ((ol (span-make beg end)))
(while props
(overlay-put ol (car props) (cadr props))
(setq props (cddr props)))
(add-timeout 10 'delete-overlay ol)
ol))))

(let ((proof-point (save-excursion (coq-test-goto-after "(*error*)") (point)))
(proof-cmd-point (save-excursion
(coq-test-goto-after "(*error*)")
(re-search-forward "}")
(re-search-backward "}"))))
(coq-test-goto-before " (*test-lemma*)")
(proof-goto-point)
(proof-shell-wait)
(coq-should-buffer-string "Error: The proof is not focused")
;; checking that there is an overlay with warning face exactly
;; on "reflexivity". WARNING: this overlat lasts only for 2
;; secs, if this test is done in a (very) slow virtual machine
;; this may fail.
(should (equal (point) proof-cmd-point))
(let ((sp (span-at proof-cmd-point 'face)))
(should sp)
(should (equal (span-property sp 'face) 'proof-warning-face))
(should (equal (span-start sp) proof-cmd-point))
;; coq-8.11 used to hace ending ps shifted by one
(should (or (equal (span-end sp) (+ proof-cmd-point (length "}")))
(equal (span-end sp) (+ 1 proof-cmd-point (length "}")))))
)
(should (equal (proof-queue-or-locked-end) proof-point)))))))

;; Disable tests that use test_wholefile.v. The file is outdated, uses
;; deprecated features, is prone to caues Coq errors and therefore
Expand Down Expand Up @@ -376,9 +447,6 @@ For example, COMMENT could be (*test-definition*)"
(backward-char 3)
(should (span-at (point) 'proofusing))))))




(provide 'coq-tests)

;;; coq-tests.el ends here
9 changes: 9 additions & 0 deletions ci/test_error_loc_1.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Lemma false_proof : forall A B : bool, A = B.
Proof.
intros A B.
destruct A.
destruct B.
reflexivity. (*error*)
}
Qed. (*test-lemma*)

100 changes: 52 additions & 48 deletions coq/coq.el
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,6 @@ It is mostly useful in three window mode, see also
(filtered-frame-list (lambda (x) (and (member x frame-resp) (member x frame-gls))))))


(defun coq-remove-trailing-blanks (s)
(let ((pos (string-match "\\s-*\\'" s)))
(substring s 0 pos)))

(defun coq-remove-starting-blanks (s)
(string-match "\\`\\s-*" s)
(substring s (match-end 0) (length s)))
Expand Down Expand Up @@ -454,7 +450,7 @@ This is a subroutine of `proof-shell-filter'."
;; 'proof-eager-annotation-face))
(str (proof-shell-strip-eager-annotations start end))
(strnotrailingspace
(coq-remove-starting-blanks (coq-remove-trailing-blanks str))))
(coq-remove-starting-blanks (proof-strip-whitespace-at-end str))))
(pg-response-display-with-face strnotrailingspace))) ; face


Expand Down Expand Up @@ -1946,8 +1942,8 @@ at `proof-assistant-settings-cmds' evaluation time.")
;; span menu
(setq proof-script-span-context-menu-extensions #'coq-create-span-menu)

(setq proof-shell-start-silent-cmd "Set Silent. "
proof-shell-stop-silent-cmd "Unset Silent. ")
(setq proof-shell-start-silent-cmd "Set Silent."
proof-shell-stop-silent-cmd "Unset Silent.")

;; prooftree config
(setq
Expand Down Expand Up @@ -2327,7 +2323,8 @@ Return command that undos to state."
;; Don't add the prefix if this is a command sent internally
(unless (or (eq action 'proof-done-invisible)
(coq-bullet-p string)) ;; coq does not accept "Time -".
(setq string (concat coq--time-prefix string))))))
(setq string (concat coq--time-prefix string))
(setq string (proof-strip-whitespace-at-end string))))))

(add-hook 'proof-shell-insert-hook #'coq-preprocessing)

Expand Down Expand Up @@ -2957,7 +2954,7 @@ Completion is on a quasi-exhaustive list of Coq tacticals."
"Last error from `coq-get-last-error-location' and `coq-highlight-error'.")

(defvar coq--error-location-regexp
"^Toplevel input[^:]+:\n> \\(.*\\)\n> \\([^^]*\\)\\(\\^+\\)\n"
"^Toplevel input, characters \\(?1:[0-9]+\\)-\\(?2:[0-9]+\\):\\(\n> [^\n]*\\)*\n"
"A regexp to search the header of coq error locations.")

;; I don't use proof-shell-last-output here since it is not always set to the
Expand All @@ -2966,12 +2963,16 @@ Completion is on a quasi-exhaustive list of Coq tacticals."
;; buffer (better to only scroll it?)
(defun coq-get-last-error-location (&optional parseresp clean)
"Return location information on last error sent by coq.
Return a two elements list (POS LEN) if successful, nil otherwise.
POS is the number of characters preceding the underlined expression,
and LEN is its length.
Coq error message must be like this:
POS is the number of **bytes** preceding the error location in
the current command, and LEN is its length (in bytes too).
Coq error message is like this:
\"
Toplevel input, characters 22-26:
> command with an error here ...
> ^^^^
\"
Expand All @@ -2983,45 +2984,33 @@ If PARSERESP is nil, don't really parse response buffer but take the value of
If PARSERESP and CLEAN are non-nil, delete the error location from the response
buffer."
(if (not parseresp) last-coq-error-location
;; proof-shell-handle-error-or-interrupt-hook is called from shell buffer
;; then highlight the corresponding error location
(proof-with-current-buffer-if-exists proof-response-buffer
(goto-char (point-max)) ;\nToplevel input, character[^:]:\n
(when (re-search-backward coq--error-location-regexp nil t)
(let ((text (match-string 1))
(pos (length (match-string 2)))
(len (length (match-string 3))))
(goto-char (point-max))
(if (not (re-search-backward coq--error-location-regexp nil t))
(setq last-coq-error-location (list 0 0))
(let ((pos (string-to-number (match-string 1)))
(end (string-to-number (match-string 2))))
(setq last-coq-error-location (list pos (- end pos)))
;; clean the response buffer from ultra-ugly underlined command line
;; parsed above. Don't kill the first \n
;; Don't kill the first \n
;; TODO: make coq stop displaying it it?
(let ((inhibit-read-only t))
(when clean (delete-region (match-beginning 0) (match-end 0))))
(when proof-shell-unicode ;; TODO: remove this (when...) when coq-8.3 is out.
;; `pos' and `len' are actually specified in bytes, apparently. So
;; let's convert them, assuming the encoding used is utf-8.
;; Presumably in Emacs-23 we could use `string-bytes' for that
;; since the internal encoding happens to use utf-8 as well.
;; Actually in coq-8.3 one utf8 char = one space so we do not need
;; this at all
(let ((bytes text)) ;(encode-coding-string text 'utf-8-unix)
;; Check that pos&len make sense in `bytes', if not give up.
(when (>= (length bytes) (+ pos len))
;; We assume here that `text' is a single line and use \n as
;; a marker so we can find it back after decoding.
(setq bytes (concat (substring bytes 0 pos)
"\n" (substring bytes pos (+ pos len))))
(let ((chars (decode-coding-string bytes 'utf-8-unix)))
(setq pos (string-match "\n" chars))
(setq len (- (length chars) pos 1))))))
(setq last-coq-error-location (list pos len)))))))
(when clean (delete-region (match-beginning 0) (match-end 0)))))
last-coq-error-location))))

(defun point-add-bytes (nbytes)
"Return current point shifted by NBYTES bytes."
(byte-to-position (+ (position-bytes (point)) nbytes)))

(defun coq-highlight-error (&optional parseresp clean)
"Parse the last Coq output looking at an error message.
"Return position and length of error in last message.
Highlight the text pointed by it.
Coq error message must be like this:
\"
> command with an error here ...
Toplevel input, characters 60-66:
> ...
> ^^^^
\"
Expand All @@ -3030,20 +3019,35 @@ If PARSERESP is nil, don't really parse response buffer but take the value of
`last-coq-error-location'.
If PARSERESP and CLEAN are non-nil, delete the error location from the response
buffer."
buffer.
Important: Coq gives char positions in bytes instead of chars.
"
(proof-with-current-buffer-if-exists proof-script-buffer
(let ((mtch (coq-get-last-error-location parseresp clean)))
(when mtch
(let ((pos (car mtch))
(lgth (cadr mtch)))
(goto-char (+ (proof-unprocessed-begin) 1))
(coq-find-real-start)

;; utf8 adaptation is made in coq-get-last-error-location above
(let ((time-offset (if coq-time-commands (length coq--time-prefix) 0)))
(goto-char (+ (point) pos))
(span-make-self-removing-span (point) (+ (point) (- lgth time-offset))
'face 'proof-warning-face)))))))
;; locations are given in bytes. translate into valid positions
;; + deal with coq bug #19355
(let* ((cmdstart (point))
(next-cmd (progn (coq-script-parse-cmdend-forward) (point)))
;; test suspîcious location info from coq (coq#19355). coq <=
;; 8.20 on curly braces gives the global location instead of
;; the location inside the command. fallback: we locate the
;; error at the command start, with given length. we detect
;; the problem by testing if the location of the error is
;; greater than the end of the command itself.
(bug19355 (> pos next-cmd)))
(goto-char cmdstart)
(let* ((pos (if bug19355 0 pos)) ;; 0 means start of command
(beg (goto-char (point-add-bytes pos)))
(end (goto-char (point-add-bytes lgth))))
(span-make-self-removing-span beg end 'face 'proof-warning-face)
;; user usually expect the point to move to the error location
(goto-char beg))))))))

(defun coq-highlight-error-hook ()
(coq-highlight-error t t))
Expand Down

0 comments on commit 16b70c9

Please sign in to comment.