Subject: bug#72695: CC Mode 5.35.2 (C++//l); c++-mode misindents 'if constexpr' inside lambdas

From: Arsen Arsenović <[email-masked]>
Date: Sun, 01 Sep 2024 03:03:30 +0200

Hi Alan,

Alan Mackenzie <[email-masked]> writes:

> It's going well, thanks!  At the current rate of progress, I'll have my
> mail setup set up by this time next week, and that is the point at
> which, at least for me, my new machine becomes my current machine.

Great to hear!

>> > But mainly I'm struggling with the unfathomable complexity of C++ lambda
>> > functions, as documented on
>> > https://en.cppreference.com/w/cpp/language/lambda.
>
>> It should be quite simple - just a few optional productions preceded by
>> an introducer production (see https://eel.is/c++draft/expr.prim.lambda
>> for a formal grammar - this is the text from the standard draft).
>
>> > In my not so humble opinion, the C++ standards people have completely
>> > lost the plot.
>
>> Should help be necessary, I do work on G++ so I'm quite familiar with
>> the language.  Feel free to ping me.
>
> Thanks!  It's just that the explanatory text on en.cppreference.com
> about just lambda functions takes 20 scrolls of ~45 lines per scroll,
> coming out at many hundreds of lines, just for lambda functions.  I feel
> that the entire language part of Emacs Lisp could fit into that quite
> comfortably, at the same density.

Heh, given there's only a dozen or so syntax elements in Elisp, I agree
;-).  C++ exposes lots of flexibility, so the syntax grew to match.

Lambda expressions declare both a data type and a function, and their
syntax exposes the syntax elements of both, hence the proverbial
complexity being the sum of the two.

>> > The number of special cases in just the syntax, apparently just to
>> > save a few characters from a program's text, has got beyond the point
>> > where anybody can grasp it.  Nobody in this group of people appears to
>> > be able to say "STOP!", though somebody sorely needs to.  I dread to
>> > think what will be coming out in C++26.  :-(
>
>> It really shouldn't be that bad - I'll help demystify if need be.
>
> Thanks!
>
>> > Anyhow, I've spent all afternoon on the problem so far, and I think I can
>> > see my way to a solution.  It will still take a few more days yet, I'm
>> > afraid.
>
>> Thank you very much!  My apologies for the trouble.
>
> No apologies called for.  You raised a bug report, for which thanks!
>
> Anyhow, I've got a first version, incompletely tested, of a new handling
> for a lambda expression.  The patch is not nearly as big as it looks,
> two thirds of it is just re-indenting a large function I put an extra
> `if' form aroun.

I've tried it on a few examples I came up with, it seems to work well in
'real-life' code.

I've also constructed the following "stress test" which includes (almost
- see below) all the productions that can exist in a lambda function, in
the same context as the original bad example:

  void
  def ()
  {
    auto foo = [&,
  	      a,
  	      b = 123,
  	      c,
  	      ...d,
  	      ...e = 123
  	      ]
      <typename T,
       typename S>
      //alignas (128)
      [[gnu::noreturn]]
      requires some_concept<T>
      (int a,
       int b,
       int c)
      consteval
      constexpr
      mutable
      static
      //noexcept(true)
      [[gnu::packed]]
      //alignas (128)
      //->
      //int
      //requires (std::same_as <decltype (a), int>)
        {
      if constexpr (true)
        ;
    }
  }

c-backward-over-lambda-expression does not work for this example.

It seems that it gets confused by the trailing requires clauses, the
alignas, the noexcept and the trailing return type (all commented -
uncomment any or all to reproduce).  However, this does not appear to
negatively impact the syntax recognition instead, for the most part (I
removed an element from the stress test above in order to demonstrate
that the issue with indentation appears to be separate).

In the following example:

  void
  def ()
  {
    auto foo = [&,
  	      a,
  	      b = 123,
  	      c,
  	      ...d,
  	      ...e = 123
  	      ]
      <typename T,
       typename S>
      alignas (128)
      [[gnu::noreturn]]
      //requires some_concept<T>
      (int a,
       int b,
       int c)
      consteval
      constexpr
      mutable
      static
      noexcept(true)
      [[gnu::packed]]
      alignas (128)
      ->
      int
      requires (std::same_as <decltype (a), int>)
    {
      if constexpr (true)
        ;
    }
  }

... uncommenting the requires line confuses the indentation engine.

Hopefully these examples help.

Please also note that in a few places (such as alignas, the noexcept
specifier, RHS of assignments in the capture list) near-arbitrary
expressions can appear, and that requires clause have special boolean
expression syntax (see
https://eel.is/c++draft/temp.pre#nt:requires-clause).

Both of these pieces of syntax are reused in other contexts (other
declarations), so if there's a bug in them in the lambda case, it likely
carries over to the general declaration case.

Thank you again for maintaining CC-mode!  It is immeasurably useful.
And apologies for the trouble :-)

Have a lovely day.
-- 
Arsen Arsenović