diff mbox series

c++: Avoid "infinite parsing" because of cp_parser_decltype [PR114858]

Message ID 01020191ffe7d291-0ceb3852-2428-49cc-97c5-7c6e8072ec43-000000@eu-west-1.amazonses.com
State New
Headers show
Series c++: Avoid "infinite parsing" because of cp_parser_decltype [PR114858] | expand

Commit Message

Simon Martin Sept. 17, 2024, 12:14 p.m. UTC
The invalid test case in this PR highlights a bad interaction between
the tentative_firewall and error recovery in cp_parser_decltype: the
firewall makes cp_parser_skip_to_closing_parenthesis a no-op, and the
parser does not make any progress, running "forever".

This patch calls cp_parser_commit_to_tentative_parse before initiating
error recovery.

Successfully tested on x86_64-pc-linux-gnu.

	PR c++/114858

gcc/cp/ChangeLog:

	* parser.cc (cp_parser_decltype): Commit tentative parse before
	initiating error recovery.

gcc/testsuite/ChangeLog:

	* g++.dg/cpp0x/decltype10.C: Adjust test expectation.
	* g++.dg/cpp2a/pr114858.C: New test.
---
 gcc/cp/parser.cc                        |  3 +++
 gcc/testsuite/g++.dg/cpp0x/decltype10.C |  2 ++
 gcc/testsuite/g++.dg/cpp2a/pr114858.C   | 25 +++++++++++++++++++++++++
 3 files changed, 30 insertions(+)
 create mode 100644 gcc/testsuite/g++.dg/cpp2a/pr114858.C

Comments

Simon Martin Sept. 30, 2024, 9:24 a.m. UTC | #1
Friendly ping. Thanks!

On 17 Sep 2024, at 14:14, Simon Martin wrote:

> The invalid test case in this PR highlights a bad interaction between
> the tentative_firewall and error recovery in cp_parser_decltype: the
> firewall makes cp_parser_skip_to_closing_parenthesis a no-op, and the
> parser does not make any progress, running "forever".
>
> This patch calls cp_parser_commit_to_tentative_parse before initiating
> error recovery.
>
> Successfully tested on x86_64-pc-linux-gnu.
>
> 	PR c++/114858
>
> gcc/cp/ChangeLog:
>
> 	* parser.cc (cp_parser_decltype): Commit tentative parse before
> 	initiating error recovery.
>
> gcc/testsuite/ChangeLog:
>
> 	* g++.dg/cpp0x/decltype10.C: Adjust test expectation.
> 	* g++.dg/cpp2a/pr114858.C: New test.
> ---
>  gcc/cp/parser.cc                        |  3 +++
>  gcc/testsuite/g++.dg/cpp0x/decltype10.C |  2 ++
>  gcc/testsuite/g++.dg/cpp2a/pr114858.C   | 25 
> +++++++++++++++++++++++++
>  3 files changed, 30 insertions(+)
>  create mode 100644 gcc/testsuite/g++.dg/cpp2a/pr114858.C
>
> diff --git a/gcc/cp/parser.cc b/gcc/cp/parser.cc
> index 4dd9474cf60..3a7c5ffe4c8 100644
> --- a/gcc/cp/parser.cc
> +++ b/gcc/cp/parser.cc
> @@ -17508,6 +17508,9 @@ cp_parser_decltype (cp_parser *parser)
>    /* Parse to the closing `)'.  */
>    if (expr == error_mark_node || !parens.require_close (parser))
>      {
> +      /* Commit to the tentative_firewall so we actually skip to the 
> closing
> +	 parenthesis.  */
> +      cp_parser_commit_to_tentative_parse (parser);
>        cp_parser_skip_to_closing_parenthesis (parser, true, false,
>  					     /*consume_paren=*/true);
>        expr = error_mark_node;
> diff --git a/gcc/testsuite/g++.dg/cpp0x/decltype10.C 
> b/gcc/testsuite/g++.dg/cpp0x/decltype10.C
> index fe7247269f5..bd606e325d4 100644
> --- a/gcc/testsuite/g++.dg/cpp0x/decltype10.C
> +++ b/gcc/testsuite/g++.dg/cpp0x/decltype10.C
> @@ -7,3 +7,5 @@ template<int> struct A
>  };
>
>  template<int N> int A<N>::i(decltype (A::i;	// { dg-error "expected" 
> }
> +
> +// { dg-excess-errors "" }
> diff --git a/gcc/testsuite/g++.dg/cpp2a/pr114858.C 
> b/gcc/testsuite/g++.dg/cpp2a/pr114858.C
> new file mode 100644
> index 00000000000..6ffde4c3a2c
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/cpp2a/pr114858.C
> @@ -0,0 +1,25 @@
> +// PR c++/114858
> +// { dg-do compile { target c++20 } }
> +// { dg-timeout 2 }
> +
> +template <class F> void g(F);
> +template <class... A>
> +auto h(A &&... a) -> decltype(
> +  decltype(g< // { dg-error "expected primary-expression" }
> +  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
> +  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
> +  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
> +  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
> +  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
> +  decltype(g<decltype(g<
> +  decltype()>)(a)...)
> +{
> +  h([] {});
> +}
> +
> +int main() {
> +  h();
> +  return 0;
> +}
> +
> +// { dg-excess-errors "" }
> -- 
> 2.44.0
Jason Merrill Sept. 30, 2024, 6:56 p.m. UTC | #2
On 9/17/24 8:14 AM, Simon Martin wrote:
> The invalid test case in this PR highlights a bad interaction between
> the tentative_firewall and error recovery in cp_parser_decltype: the
> firewall makes cp_parser_skip_to_closing_parenthesis a no-op, and the
> parser does not make any progress, running "forever".
> 
> This patch calls cp_parser_commit_to_tentative_parse before initiating
> error recovery.
> 
> Successfully tested on x86_64-pc-linux-gnu.
> 
> 	PR c++/114858
> 
> gcc/cp/ChangeLog:
> 
> 	* parser.cc (cp_parser_decltype): Commit tentative parse before
> 	initiating error recovery.
> 
> gcc/testsuite/ChangeLog:
> 
> 	* g++.dg/cpp0x/decltype10.C: Adjust test expectation.
> 	* g++.dg/cpp2a/pr114858.C: New test.
> ---
>   gcc/cp/parser.cc                        |  3 +++
>   gcc/testsuite/g++.dg/cpp0x/decltype10.C |  2 ++
>   gcc/testsuite/g++.dg/cpp2a/pr114858.C   | 25 +++++++++++++++++++++++++
>   3 files changed, 30 insertions(+)
>   create mode 100644 gcc/testsuite/g++.dg/cpp2a/pr114858.C
> 
> diff --git a/gcc/cp/parser.cc b/gcc/cp/parser.cc
> index 4dd9474cf60..3a7c5ffe4c8 100644
> --- a/gcc/cp/parser.cc
> +++ b/gcc/cp/parser.cc
> @@ -17508,6 +17508,9 @@ cp_parser_decltype (cp_parser *parser)
>     /* Parse to the closing `)'.  */
>     if (expr == error_mark_node || !parens.require_close (parser))
>       {
> +      /* Commit to the tentative_firewall so we actually skip to the closing
> +	 parenthesis.  */
> +      cp_parser_commit_to_tentative_parse (parser);

I don't think this is right.

Earlier in cp_parser_decltype I see

>   /* If in_declarator_p, a reparse as an expression might succeed (60361).                                                          
>      Otherwise, commit now for better diagnostics.  */
>   if (cp_parser_uncommitted_to_tentative_parse_p (parser)
>       && !parser->in_declarator_p)
>     cp_parser_commit_to_topmost_tentative_parse (parser);

Here we're in a declarator, so we didn't commit at that point.  And we 
still don't want to commit if parsing fails; as the comment says, when 
reparsing as an expression-statement it might work.  Though there seems 
not to be a testcase for that...

In trying to come up with a testcase, I wrote this one that already 
fails because the error doesn't happen until after the decltype, so we 
memorize the wrong result:

struct Helper { Helper(int, ...); };
template <class T> struct C;
template<> struct C<char> {};
char A = 1;
Helper testFail(int(A), C<decltype(A)>{}); // { dg-bogus "C<int>" }

So in the long term we need to overhaul this code to handle reparsing 
even without a syntax error.  But it's not a high priority.

Getting back to your patch, I think the problem is in 
cp_parser_simple_type_specifier:

>     case RID_DECLTYPE:
>       /* Since DR 743, decltype can either be a simple-type-specifier by                                                            
>          itself or begin a nested-name-specifier.  Parsing it will replace                                                          
>          it with a CPP_DECLTYPE, so just rewind and let the CPP_DECLTYPE                                                            
>          handling below decide what to do.  */
>       cp_parser_decltype (parser);
>       cp_lexer_set_token_position (parser->lexer, token);
>       break;

This assumes that cp_parser_decltype will always succeed, which is 
wrong.  We need to check whether the token actually became CPP_DECLTYPE 
and parser_error if not.

Jason
Simon Martin Oct. 7, 2024, 7:57 p.m. UTC | #3
Hi Jason,

On 30 Sep 2024, at 20:56, Jason Merrill wrote:

> On 9/17/24 8:14 AM, Simon Martin wrote:
>> The invalid test case in this PR highlights a bad interaction between

>> the tentative_firewall and error recovery in cp_parser_decltype: the
>> firewall makes cp_parser_skip_to_closing_parenthesis a no-op, and the

>> parser does not make any progress, running "forever".
>>
>> This patch calls cp_parser_commit_to_tentative_parse before 
>> initiating
>> error recovery.
>>
>> Successfully tested on x86_64-pc-linux-gnu.
>>
>> 	PR c++/114858
>>
>> gcc/cp/ChangeLog:
>>
>> 	* parser.cc (cp_parser_decltype): Commit tentative parse before
>> 	initiating error recovery.
>>
>> gcc/testsuite/ChangeLog:
>>
>> 	* g++.dg/cpp0x/decltype10.C: Adjust test expectation.
>> 	* g++.dg/cpp2a/pr114858.C: New test.
>> ---
>>   gcc/cp/parser.cc                        |  3 +++
>>   gcc/testsuite/g++.dg/cpp0x/decltype10.C |  2 ++
>>   gcc/testsuite/g++.dg/cpp2a/pr114858.C   | 25 
>> +++++++++++++++++++++++++
>>   3 files changed, 30 insertions(+)
>>   create mode 100644 gcc/testsuite/g++.dg/cpp2a/pr114858.C
>>
>> diff --git a/gcc/cp/parser.cc b/gcc/cp/parser.cc
>> index 4dd9474cf60..3a7c5ffe4c8 100644
>> --- a/gcc/cp/parser.cc
>> +++ b/gcc/cp/parser.cc
>> @@ -17508,6 +17508,9 @@ cp_parser_decltype (cp_parser *parser)
>>     /* Parse to the closing `)'.  */
>>     if (expr == error_mark_node || !parens.require_close (parser))
>>       {
>> +      /* Commit to the tentative_firewall so we actually skip to the 
>> closing
>> +	 parenthesis.  */
>> +      cp_parser_commit_to_tentative_parse (parser);
>
> I don't think this is right.
>
> Earlier in cp_parser_decltype I see
>
>>   /* If in_declarator_p, a reparse as an expression might succeed 
>> (60361).                                                              
>>  Otherwise, commit now for better diagnostics.  */
>>   if (cp_parser_uncommitted_to_tentative_parse_p (parser)
>>       && !parser->in_declarator_p)
>>     cp_parser_commit_to_topmost_tentative_parse (parser);
>
> Here we're in a declarator, so we didn't commit at that point.  And we 
> still don't want to commit if parsing fails; as the comment says, when 
> reparsing as an expression-statement it might work.  Though there 
> seems not to be a testcase for that...
Right, understood. I’ll see if I can come up with something in a 
follow-up patch.
>
> In trying to come up with a testcase, I wrote this one that already 

> fails because the error doesn't happen until after the decltype, so we 
> memorize the wrong result:
>
> struct Helper { Helper(int, ...); };
> template <class T> struct C;
> template<> struct C<char> {};
> char A = 1;
> Helper testFail(int(A), C<decltype(A)>{}); // { dg-bogus "C<int>" }
>
> So in the long term we need to overhaul this code to handle reparsing 
> even without a syntax error.  But it's not a high priority.
Nice; I’ll open a PR for that, and try to take a stab at it.
>
> Getting back to your patch, I think the problem is in 
> cp_parser_simple_type_specifier:
>
>>     case RID_DECLTYPE:
>>       /* Since DR 743, decltype can either be a simple-type-specifier 
>> by                                                                    
>>  itself or begin a nested-name-specifier.  Parsing it will replace    
>>                                                                it 
>> with a CPP_DECLTYPE, so just rewind and let the CPP_DECLTYPE          
>>                                                            handling 
>> below decide what to do.  */
>>       cp_parser_decltype (parser);
>>       cp_lexer_set_token_position (parser->lexer, token);
>>       break;
>
> This assumes that cp_parser_decltype will always succeed, which is 
> wrong.  We need to check whether the token actually became 
> CPP_DECLTYPE and parser_error if not.
We should definitely to this. Note however that it’s not sufficient to 
fix the test case: compilation time still explodes because for 
constantly re-parse “brain dead” template ids (unterminated 
parameter list). The attached patch fixes both issues, and has been 
successfully tested on x86_64-pc-linux-gnu. Ok for trunk?

Thanks, Simon
From 61fadd961442b919281c865d0db90fb4ed6265aa Mon Sep 17 00:00:00 2001
From: Simon Martin <simon@nasilyan.com>
Date: Mon, 16 Sep 2024 10:14:46 +0200
Subject: [PATCH] c++: Avoid "infinite parsing" because of cp_parser_decltype [PR114858]

The invalid test case in this PR highlights two deficiencies:
 1. cp_parser_simple_type_specifier assumes that parsing a decltype
always succeeds
 2. cp_parser_template_id does not turn the sequence of tokens into a
CPP_TEMPLATE_ID even in case we're sure that no reparse will ever
succeed (e.g. the template parameter list is not terminated).

So for each "decltype level" in the test case, we try to parse the
decltype expression as an id-expression, then a postfix-expression, then
an expression, failing every time, and the compilation time explodes.

This patches addresses issue #1 by checking whether the decltype token
was turned into a CPP_DECLTYPE, and #2 by still producing a
CPP_TEMPLATE_ID if the template parameter list is not terminated, to
avoid reparsing ad nauseam.

Successfully tested on x86_64-pc-linux-gnu.

	PR c++/114858

gcc/cp/ChangeLog:

	* parser.cc (cp_parser_template_id): Produce a CPP_TEMPLATE_ID
	if the parameter list is not terminated.
	(cp_parser_simple_type_specifier): cp_parser_decltype not
	producing a CPP_DECLTYPE is an error.

gcc/testsuite/ChangeLog:

	* g++.dg/cpp0x/pr33839.C: Adjust test expectation.
	* g++.dg/cpp2a/pr114858.C: New test.

---
 gcc/cp/parser.cc                      | 26 +++++++++++++++++++++-----
 gcc/testsuite/g++.dg/cpp0x/pr33839.C  |  2 +-
 gcc/testsuite/g++.dg/cpp2a/pr114858.C | 26 ++++++++++++++++++++++++++
 3 files changed, 48 insertions(+), 6 deletions(-)
 create mode 100644 gcc/testsuite/g++.dg/cpp2a/pr114858.C

diff --git a/gcc/cp/parser.cc b/gcc/cp/parser.cc
index 9d31a975dcf..251b1cc5c47 100644
--- a/gcc/cp/parser.cc
+++ b/gcc/cp/parser.cc
@@ -19133,6 +19133,7 @@ cp_parser_template_id (cp_parser *parser,
   cp_token_position start_of_id = 0;
   cp_token *next_token = NULL, *next_token_2 = NULL;
   bool is_identifier;
+  bool unrecoverable_error_p = false;
 
   /* If the next token corresponds to a template-id, there is no need
      to reparse it.  */
@@ -19271,6 +19272,14 @@ cp_parser_template_id (cp_parser *parser,
 	  pop_deferring_access_checks ();
 	  return error_mark_node;
 	}
+      else if (cp_parser_error_occurred (parser))
+	{
+	  cp_token *prev = cp_lexer_safe_previous_token (parser->lexer);
+	  if (!(prev && prev->type == CPP_GREATER))
+	    /* If the parameter list is not closed, no reparse will ever
+	       succeed.  */
+	    unrecoverable_error_p = true;
+	}
     }
 
   /* Set the location to be of the form:
@@ -19341,11 +19350,12 @@ cp_parser_template_id (cp_parser *parser,
      error messages about problems during instantiation of the
      template.  */
   if (start_of_id
-      /* Don't do this if we had a parse error in a declarator; re-parsing
+      /* Don't do this if we had a parse error in a declarator (unless it's
+	 unrecoverable, e.g. the parameter list is unterminated); re-parsing
 	 might succeed if a name changes meaning (60361).  */
-      && !(cp_parser_error_occurred (parser)
-	   && cp_parser_parsing_tentatively (parser)
-	   && parser->in_declarator_p))
+      && (!(cp_parser_error_occurred (parser)
+	    && parser->in_declarator_p
+	    && !unrecoverable_error_p)))
     {
       /* Reset the contents of the START_OF_ID token.  */
       token->type = CPP_TEMPLATE_ID;
@@ -20636,7 +20646,13 @@ cp_parser_simple_type_specifier (cp_parser* parser,
 	 it with a CPP_DECLTYPE, so just rewind and let the CPP_DECLTYPE
 	 handling below decide what to do.  */
       cp_parser_decltype (parser);
-      cp_lexer_set_token_position (parser->lexer, token);
+      if (token->type == CPP_DECLTYPE)
+	cp_lexer_set_token_position (parser->lexer, token);
+      else
+	{
+	  cp_parser_error(parser, "invalid %<decltype%> type");
+	  type = NULL_TREE;
+	}
       break;
 
     case RID_TYPEOF:
diff --git a/gcc/testsuite/g++.dg/cpp0x/pr33839.C b/gcc/testsuite/g++.dg/cpp0x/pr33839.C
index 50bcfe81cf5..3c3487cf206 100644
--- a/gcc/testsuite/g++.dg/cpp0x/pr33839.C
+++ b/gcc/testsuite/g++.dg/cpp0x/pr33839.C
@@ -3,6 +3,6 @@ template<int> struct A;
 
 void foo()
 {
-  __decltype A<0>; // { dg-error "invalid declarator|expected" }
+  __decltype A<0>; // { dg-error "does not declare anything|expected" }
   __decltype (A<0>); // { dg-error "must be an expression" }
 }
diff --git a/gcc/testsuite/g++.dg/cpp2a/pr114858.C b/gcc/testsuite/g++.dg/cpp2a/pr114858.C
new file mode 100644
index 00000000000..3a44c47ca09
--- /dev/null
+++ b/gcc/testsuite/g++.dg/cpp2a/pr114858.C
@@ -0,0 +1,26 @@
+// PR c++/114858
+// { dg-do compile { target c++20 } }
+// { dg-additional-options "-fno-diagnostics-show-caret" }
+// { dg-timeout 2 }
+
+template <class F> void g(F);
+template <class... A>
+auto h(A &&... a) -> decltype(
+  decltype(g< // { dg-error "expected primary-expression" }
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<
+  decltype()>)(a)...)
+{
+  h([] {});
+}
+
+int main() { 
+  h(); 
+  return 0; 
+}
+
+// { dg-excess-errors "" }
diff mbox series

Patch

diff --git a/gcc/cp/parser.cc b/gcc/cp/parser.cc
index 4dd9474cf60..3a7c5ffe4c8 100644
--- a/gcc/cp/parser.cc
+++ b/gcc/cp/parser.cc
@@ -17508,6 +17508,9 @@  cp_parser_decltype (cp_parser *parser)
   /* Parse to the closing `)'.  */
   if (expr == error_mark_node || !parens.require_close (parser))
     {
+      /* Commit to the tentative_firewall so we actually skip to the closing
+	 parenthesis.  */
+      cp_parser_commit_to_tentative_parse (parser);
       cp_parser_skip_to_closing_parenthesis (parser, true, false,
 					     /*consume_paren=*/true);
       expr = error_mark_node;
diff --git a/gcc/testsuite/g++.dg/cpp0x/decltype10.C b/gcc/testsuite/g++.dg/cpp0x/decltype10.C
index fe7247269f5..bd606e325d4 100644
--- a/gcc/testsuite/g++.dg/cpp0x/decltype10.C
+++ b/gcc/testsuite/g++.dg/cpp0x/decltype10.C
@@ -7,3 +7,5 @@  template<int> struct A
 };
 
 template<int N> int A<N>::i(decltype (A::i;	// { dg-error "expected" }
+
+// { dg-excess-errors "" }
diff --git a/gcc/testsuite/g++.dg/cpp2a/pr114858.C b/gcc/testsuite/g++.dg/cpp2a/pr114858.C
new file mode 100644
index 00000000000..6ffde4c3a2c
--- /dev/null
+++ b/gcc/testsuite/g++.dg/cpp2a/pr114858.C
@@ -0,0 +1,25 @@ 
+// PR c++/114858
+// { dg-do compile { target c++20 } }
+// { dg-timeout 2 }
+
+template <class F> void g(F);
+template <class... A>
+auto h(A &&... a) -> decltype(
+  decltype(g< // { dg-error "expected primary-expression" }
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<decltype(g<
+  decltype(g<decltype(g<
+  decltype()>)(a)...)
+{
+  h([] {});
+}
+
+int main() { 
+  h(); 
+  return 0; 
+}
+
+// { dg-excess-errors "" }