Subject: bug#62386: CC Mode 5.35.2 (C++//l); C++ concept indentation

From: Alan Mackenzie <[email-masked]>
Date: Mon, 27 Mar 2023 18:37:17 +0000

Hello, Michael.

On Fri, Mar 24, 2023 at 14:58:28 -0400, Michael Welsh Duggan wrote:
> Alan Mackenzie <[email-masked]> writes:

> > On Wed, Mar 22, 2023 at 11:58:32 -0400, Michael Welsh Duggan wrote:
> >> Package: cc-mode

> >> Given a concept definition like:

> >>     template <typename T>
> >>     concept Foo =
> >>       Bar<T>
> >>       && requires (T t) {
> >>         { t + t } -> std::same_as<T>;
> >>       }
> >>       && something_else;

> >> This is how it actually indents in cc-mode:

> >>     template <typename T>
> >>     concept Foo =
> >>     Bar<T>
> >>       && requires (T t) {
> >>       { t + t } -> std::same_as<T>;
> >>     }
> >>       && something_else;

> >> I would expect "Bar<T>" to be statement-cont, but it is
> >> topmost-intro-cont instead.  The indentation of "{ t + t }" isn't offset
> >> properly with respect to the "requires" clause, and the closing brace is
> >> way out of line.

> [...]

> > I've made an attempt to improve the indentation of code like your test
> > case.  Would you please try it out and let me know how well it solves
> > the problem.  Thanks!

> Better for that test case and a few others.  Here's a few test outliers
> as indented by cc:

OK.  It is apparent that the former "approximate" solution for concepts
and requireses wasn't adequate, and that deeper, more rigorous analysis
was needed.  I've attempted this, and the results are in the patch in
this post.  They mostly indent your test cases to very close to how you'd
like them to be.

There's one test in particular that is slightly different, namely:

[ .... ]

> template <typename T>
> requires 
>   (requires (T t) { ++t; }
>    && Baz<T>)
> int foo();

Here, I haven't managed to align the && under the requires.  Rather it is
aligned underneath the (.  CC Mode currently has no syntactic symbols for
this kind of alignment, but there might be one or two line-up functions
which could do the trick.  (I haven't actually searched for one).

> The latter of these is especially interesting because TAB on the "&&
> Baz<T>)" section causes an "End of buffer" signal.

I never found out why this was happening.  It's not happening any more.

Here's the patch.  Please try it out on your real code, and let me know
how well it solves the bug.  Thanks!

diff -r 300ebf19cd62 cc-align.el
--- a/cc-align.el	Fri Feb 17 08:58:11 2023 +0000
+++ b/cc-align.el	Mon Mar 27 18:21:42 2023 +0000
@@ -87,16 +87,22 @@
 statement-cont.)
 
 Works with: topmost-intro-cont."
-  (save-excursion
-    (beginning-of-line)
-    (unless (re-search-forward c-fun-name-substitute-key
-			       (c-point 'eol) t)
+  (let ((here (point))
+	conpos)
+    (save-excursion
       (beginning-of-line)
       (c-backward-syntactic-ws (c-langelem-pos langelem))
-      (if (and (memq (char-before) '(?} ?,))
-	       (not (and c-overloadable-operators-regexp
-			 (c-after-special-operator-id))))
-	  c-basic-offset))))
+      (cond
+       ((and (memq (char-before) '(?} ?, ?=))
+	     (not (and c-overloadable-operators-regexp
+		       (c-after-special-operator-id))))
+	c-basic-offset)
+       ((and
+	 (goto-char (c-langelem-pos langelem))
+	 (setq conpos (car (c-looking-at-concept)))
+	 (> here (c-point 'eol conpos)))
+	c-basic-offset)
+       (t 0)))))
 
 (defun c-lineup-gnu-DEFUN-intro-cont (langelem)
   "Line up the continuation lines of a DEFUN macro in the Emacs C source.
diff -r 300ebf19cd62 cc-engine.el
--- a/cc-engine.el	Fri Feb 17 08:58:11 2023 +0000
+++ b/cc-engine.el	Mon Mar 27 18:21:42 2023 +0000
@@ -9683,6 +9683,9 @@
   ;;
   ;; Note that this function is incomplete, handling only those cases expected
   ;; to be common in a C++20 requires clause.
+  ;;
+  ;; Note also that (...) is not recognised as a primary expression if the next
+  ;; token is an open brace.
   (let ((here (point))
 	(c-restricted-<>-arglists t)
 	(c-parse-and-markup-<>-arglists nil)
@@ -9696,7 +9699,8 @@
 	  (and (c-go-list-forward (point) limit)
 	       (eq (char-before) ?\))
 	       (progn (c-forward-syntactic-ws limit)
-		      t)))
+		      (not (eq (char-after) ?{))
+		      )))
 	 ((c-forward-over-compound-identifier)
 	  (c-forward-syntactic-ws limit)
 	  (while (cond
@@ -9744,6 +9748,7 @@
 	   (setq final-point (point))
 	   (while
 	       (and (looking-at "\\(?:&&\\|||\\)")
+		    (<= (match-end 0) limit)
 		    (progn (goto-char (match-end 0))
 			   (c-forward-syntactic-ws limit)
 			   (and (< (point) limit)
@@ -13223,6 +13228,65 @@
 	   (t nil)))
       (goto-char here))))
 
+(defun c-looking-at-concept (&optional limit)
+  ;; Are we currently at the start of a concept construct?  I.e. at the
+  ;; "template" keyword followed by the construct?  If so, we return a cons of
+  ;; the position of "concept" and the position of the first constraint
+  ;; expression following the "=" sign, otherwise we return nil.  LIMIT is a
+  ;; forward search limit
+  (save-excursion
+    (let (conpos)
+      (and (looking-at c-pre-concept-<>-key)
+	   (goto-char (match-end 1))
+	   (progn (c-forward-syntactic-ws limit)
+		  (eq (char-after) ?<))
+	   (let ((c-parse-and-markup-<>-arglists t)
+		 c-restricted-<>-arglists)
+	     (c-forward-<>-arglist nil))
+	   (progn (c-forward-syntactic-ws limit)
+		  (looking-at c-equals-nontype-decl-key)) ; "concept"
+	   (setq conpos (match-beginning 0))
+	   (goto-char (match-end 0))
+	   (c-syntactic-re-search-forward
+	    "=" limit t t)
+	   (goto-char (match-end 0))
+	   (progn (c-forward-syntactic-ws limit)
+		  (cons conpos (point)))))))
+
+(defun c-in-or-at-end-of-requires (&optional pos)
+  ;; Is POS (default POINT) in a C++ "requires" clause or at the end of one?
+  ;; If so return a cons (POSITION . END) where POSITION is that of the
+  ;; "requires" keyword and END is t if POS is at the end of the clause,
+  ;; otherwise nil.  "End of the clause" means just after the last non
+  ;; syntactic WS on the line where the clause ends.
+  (save-excursion
+    (if pos (goto-char pos) (setq pos (point)))
+    (let ((limit (max (- (point) 2000) (point-min)))
+	  found-req req-pos found-clause res
+	  )
+      (while
+	  (progn
+	    (while
+		(and
+		 (setq found-req (re-search-backward
+				  c-fun-name-substitute-key limit t)) ; Fast!
+		 (not (setq found-req (not (c-in-literal)))))) ; Slow!
+	    (setq req-pos (point))
+	    (cond
+	     ((not found-req)
+	      nil)
+	     ((progn (setq found-clause (c-forward-c++-requires-clause))
+		     nil))
+	     ((and found-clause (>= (point) pos))
+	      (c-backward-syntactic-ws)
+	      (setq res (cons req-pos (eq (point) pos)))
+	      nil)
+	     (found-clause
+	      (c-go-up-list-backward req-pos limit))
+	     (t (goto-char req-pos)
+		t))))
+      res)))
+
 (defun c-looking-at-inexpr-block (lim containing-sexp &optional check-at-end)
   ;; Return non-nil if we're looking at the beginning of a block
   ;; inside an expression.  The value returned is actually a cons of
@@ -13419,6 +13483,20 @@
 	 (looking-at c-pre-lambda-tokens-re)))
    (not (c-in-literal))))
 
+(defun c-c++-vsemi-p (&optional pos)
+  ;; C++ Only - Is there a "virtual semicolon" at POS or point?
+  ;; (See cc-defs.el for full details of "virtual semicolons".)
+  ;;
+  ;; This is true either when point is at the last non syntactic WS position
+  ;; on the line, and either there is a "macro with semicolon" just before it
+  ;; (see `c-at-macro-vsemi-p') or there is a "requires" clause which ends
+  ;; there.
+  (let (res)
+    (cond
+     ((setq res (c-in-or-at-end-of-requires pos))
+      (and res (cdr res)))
+     ((c-at-macro-vsemi-p)))))
+
 (defun c-at-macro-vsemi-p (&optional pos)
   ;; Is there a "virtual semicolon" at POS or point?
   ;; (See cc-defs.el for full details of "virtual semicolons".)
@@ -14385,6 +14463,32 @@
 				containing-decl-start
 				containing-decl-kwd))
 
+	   ;; CASE 5A.7: "defun" open in a requires expression.
+	   ((save-excursion
+	      (goto-char indent-point)
+	      (c-backward-syntactic-ws lim)
+	      (and (or (not (eq (char-before) ?\)))
+		       (c-go-list-backward nil lim))
+		   (progn (c-backward-syntactic-ws lim)
+			  (zerop (c-backward-token-2 nil nil lim)))
+		   (looking-at c-fun-name-substitute-key)
+		   (setq placeholder (point))))
+	    (goto-char placeholder)
+	    (back-to-indentation)
+	    (c-add-syntax 'defun-open (point)))
+
+	   ;; CASE 5A.6: "defun" open in concept.
+	   ((save-excursion
+	      (goto-char indent-point)
+	      (skip-chars-forward " \t")
+	      (and (eq (char-after) ?{)
+		   (eq (c-beginning-of-statement-1 lim) 'same)
+		   (setq placeholder
+			 (cdr (c-looking-at-concept indent-point)))))
+	    (goto-char placeholder)
+	    (back-to-indentation)
+	    (c-add-syntax 'defun-open (point)))
+
 	   ;; CASE 5A.5: ordinary defun open
 	   (t
 	    (save-excursion
@@ -14660,17 +14764,31 @@
 	   ;; similar).
 	   ((and c-equals-nontype-decl-key
 		 (save-excursion
-		   (prog1
-		       (and (zerop (c-backward-token-2 1 nil lim))
-			    (looking-at c-operator-re)
-			    (equal (match-string 0) "=")
-			    (zerop (c-backward-token-2 1 nil lim))
-			    (looking-at c-symbol-start)
-			    (not (looking-at c-keywords-regexp))
-			    (zerop (c-backward-token-2 1 nil lim))
-			    (looking-at c-equals-nontype-decl-key)
-			    (eq (c-beginning-of-statement-1 lim) 'same))
-		     (setq placeholder (point)))))
+		   (and
+		    (eq (c-beginning-of-statement-1 lim) 'same)
+		    (setq placeholder (point))
+		    (< (point) indent-point)
+		    (re-search-forward c-equals-nontype-decl-key
+				       indent-point t)
+		    (progn (c-forward-syntactic-ws indent-point)
+			   (looking-at c-symbol-start))
+		    (not (looking-at c-keywords-regexp))
+		    (zerop (c-forward-token-2))
+		    (< (point) indent-point)
+		    (looking-at c-operator-re)
+		    (prog1 (equal (match-string 0) "=")
+		      ;; (c-forward-token-2 nil nil indent-point)
+		      (c-forward-over-token-and-ws))
+		    (progn
+		      (while
+			  (and (< (point) indent-point)
+			       (c-forward-primary-expression indent-point)
+			       (<= (point) indent-point)
+			       (looking-at "&&\\|||")
+			       (<= (point) indent-point))
+			(goto-char (match-end 0))
+			(c-forward-syntactic-ws indent-point))
+		      (>= (point) indent-point)))))
 	    (goto-char placeholder)
 	    (c-add-stmt-syntax 'topmost-intro-cont nil nil containing-sexp
 			       paren-state))
@@ -14912,6 +15030,10 @@
 		 (goto-char placeholder)))
 	  (c-add-syntax 'annotation-top-cont (c-point 'boi tmp-pos2)))
 
+	 ;; CASE 5T: We are inside or after a C++ "requires" clause.
+	 ((setq placeholder (car-safe (c-in-or-at-end-of-requires)))
+	  (c-add-syntax 'statement-cont placeholder))
+
 	 ;; CASE 5M: we are at a topmost continuation line
 	 (t
 	  (c-beginning-of-statement-1
@@ -14927,6 +15049,14 @@
 	  (c-add-syntax 'topmost-intro-cont (c-point 'boi)))
 	 ))
 
+       ;; CASE 20: A C++ requires sub-clause.
+       ((setq placeholder (car (c-in-or-at-end-of-requires indent-point)))
+	(c-add-syntax
+	 (if (eq char-after-ip ?{)
+	     'substatement-open
+	   'substatement)
+	 (c-point 'boi placeholder)))
+
        ;; ((Old) CASE 6 has been removed.)
        ;; CASE 6: line is within a C11 _Generic expression.
        ((and c-generic-key
@@ -15310,6 +15440,19 @@
 	      (c-add-syntax 'defun-close (point))
 	    (c-add-syntax 'inline-close (point))))
 
+	 ;; CASE 16G: Do we have the closing brace of a "requires" clause
+	 ;; of a C++20 "concept"?
+	 ((save-excursion
+	    (c-backward-syntactic-ws lim)
+	    (and (or (not (eq (char-before) ?\)))
+		     (c-go-list-backward nil lim))
+		 (progn (c-backward-syntactic-ws lim)
+			(zerop (c-backward-token-2 nil nil lim)))
+		 (looking-at c-fun-name-substitute-key)))
+	  (goto-char containing-sexp)
+	  (back-to-indentation)
+	  (c-add-stmt-syntax 'defun-close nil t lim paren-state))
+
 	 ;; CASE 16F: Can be a defun-close of a function declared
 	 ;; in a statement block, e.g. in Pike or when using gcc
 	 ;; extensions, but watch out for macros followed by
@@ -15460,6 +15603,20 @@
 	  (if (eq char-after-ip ?{)
 	      (c-add-syntax 'block-open)))
 
+	 ;; CASE 17J: first "statement" inside a C++20 requires
+	 ;; "function".
+	 ((save-excursion
+	    (goto-char containing-sexp)
+	    (c-backward-syntactic-ws lim)
+	    (and (or (not (eq (char-before) ?\)))
+		     (c-go-list-backward nil lim))
+		 (progn (c-backward-syntactic-ws lim)
+			(zerop (c-backward-token-2 nil nil lim)))
+		 (looking-at c-fun-name-substitute-key)))
+	  (goto-char containing-sexp)
+	  (back-to-indentation)
+	  (c-add-syntax 'defun-block-intro (point)))
+
 	 ;; CASE 17F: first statement in an inline, or first
 	 ;; statement in a top-level defun. we can tell this is it
 	 ;; if there are no enclosing braces that haven't been
diff -r 300ebf19cd62 cc-langs.el
--- a/cc-langs.el	Fri Feb 17 08:58:11 2023 +0000
+++ b/cc-langs.el	Mon Mar 27 18:21:42 2023 +0000
@@ -571,7 +571,8 @@
 point), and returns nil or t.  This variable contains nil for languages which
 don't have EOL terminated statements. "
   t nil
-  (c c++ objc) 'c-at-macro-vsemi-p
+  (c objc) 'c-at-macro-vsemi-p
+  c++ 'c-c++-vsemi-p
   awk 'c-awk-at-vsemi-p)
 (c-lang-defvar c-at-vsemi-p-fn (c-lang-const c-at-vsemi-p-fn))

> -- 
> Michael Welsh Duggan
> ([email-masked])

-- 
Alan Mackenzie (Nuremberg, Germany).