Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Clang] [Sema] Don't crash on unexpanded pack in invalid block literal #110762

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

Sirraide
Copy link
Contributor

@Sirraide Sirraide commented Oct 1, 2024

Consider #109148:

template <typename ...Ts>
void f() {
    [] {
        (^Ts);
    };
}

When we encounter ^Ts, we try to parse a block and subsequently call DiagnoseUnexpandedParameterPack() (in ActOnBlockArguments()), which sees Ts and sets ContainsUnexpandedParameterPack to true in the LambdaScopeInfo of the enclosing lambda. However, the entire block is subsequently discarded entirely because it isn’t even syntactically well-formed. As a result, ContainsUnexpandedParameterPack is true despite the lambda’s body no longer containing any unexpanded packs, which causes an assertion the next time DiagnoseUnexpandedParameterPack() is called.

Side note: lambdas don’t suffer from this problem because they push a new LambdaScopeInfo, so if the lambda is discarded, the ‘enclosing lambda’ (which is just that very same lambda) is discarded along with it. I’m not sure making blocks work the same way would work (i.e. allowing unexpanded parameter packs if there is an enclosing lambda or block, but I’m assuming there is a reason why we don’t support that...), because currently, e.g. this doesn’t work at all:

template<typename... Ts>
void f(Ts...ts) {
    ((^ void (Ts) {} (ts)), ...); // error: block contains unexpanded parameter pack 'Ts'
};

I spent some time thinking of a few ways of fixing this (and the author of the issue for this has also proposed some approaches, including the one I decided on); ultimately, I decided on having ActOnBlockError (which is always called when the block doesn’t make it into the AST) reset the enclosing lambda’s ContainsUnexpandedParameterPack member by storing its previous state in the block’s BlockScopeInfo.

This feels like a bit of a hack, but the alternatives I’ve seen so far or managed to think of myself are either

  1. removing the assertion from every overload of DiagnoseUnexpandedParameterPack();
  2. somehow including the malformed block in the AST (by wrapping it in a RecoveryExpr perhaps?), but I’m not sure how well that would work because imo at least something like (^Ts); is just ‘too invalid’ to have a sensible representation in the AST; or
  3. Making blocks work like lambdas (as mentioned above).

Option 1 is dubious at best imo; I have no idea how 2 would work; and 3 seems like it might end up being a lot of work if there’s a reason why blocks don’t act like lambdas wrt unexpanded packs.

This fixes #109148.

@Sirraide Sirraide added clang:frontend Language frontend issues, e.g. anything involving "Sema" extension:clang labels Oct 1, 2024
@llvmbot llvmbot added the clang Clang issues not falling into any other category label Oct 1, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Oct 1, 2024

@llvm/pr-subscribers-clang

Author: None (Sirraide)

Changes

Consider #109148:

template &lt;typename ...Ts&gt;
void f() {
    [] {
        (^Ts);
    };
}

When we encounter ^Ts, we try to parse a block and subsequently call DiagnoseUnexpandedParameterPack() (in ActOnBlockArguments()), which sees Ts and sets ContainsUnexpandedParameterPack to true in the LambdaScopeInfo of the enclosing lambda. However, the entire block is subsequently discarded entirely because it isn’t even syntactically well-formed. As a result, ContainsUnexpandedParameterPack is true despite the lambda’s body no longer containing any unexpanded packs, which causes an assertion the next time DiagnoseUnexpandedParameterPack() is called.

Side note: lambdas don’t suffer from this problem because they push a new LambdaScopeInfo, so if the lambda is discarded, the ‘enclosing lambda’ (which is just that very same lambda) is discarded along with it. I’m not sure making blocks work the same way would work (i.e. allowing unexpanded parameter packs if there is an enclosing lambda or block, but I’m assuming there is a reason why we don’t support that...), because currently, e.g. this doesn’t work at all:

template&lt;typename... Ts&gt;
void f(Ts...ts) {
    ((^ void (Ts) {} (ts)), ...); // error: block contains unexpanded parameter pack 'Ts'
};

I spent some time thinking of a few ways of fixing this (and the author of the issue for this has also proposed some approaches, including the one I decided on); ultimately, I decided on having ActOnBlockError (which is always called when the block doesn’t make it into the AST) reset the enclosing lambda’s ContainsUnexpandedParameterPack member by storing its previous state in the block’s BlockScopeInfo.

This feels like a bit of a hack, but the alternatives I’ve seen so far or managed to think of myself are either

  1. removing the assertion from every overload of DiagnoseUnexpandedParameterPack();
  2. somehow including the malformed block in the AST (by wrapping it in a RecoveryExpr perhaps?), but I’m not sure how well that would work because imo at least something like (^Ts); is just ‘too invalid’ to have a sensible representation in the AST; or
  3. Making blocks work like lambdas (as mentioned above).

Option 1 is dubious at best imo; I have no idea how 2 would work; and 3 seems like it might end up being a lot of work if there’s a reason why blocks don’t act like lambdas wrt unexpanded packs.

This fixes #109148.


Full diff: https://github.com/llvm/llvm-project/pull/110762.diff

5 Files Affected:

  • (modified) clang/docs/ReleaseNotes.rst (+2)
  • (modified) clang/include/clang/Sema/ScopeInfo.h (+10)
  • (modified) clang/lib/Sema/Sema.cpp (+6-2)
  • (modified) clang/lib/Sema/SemaExpr.cpp (+12)
  • (added) clang/test/SemaCXX/block-unexpanded-pack.cpp (+48)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 34d2b584274a5f..ed4a7affcf6919 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -457,6 +457,8 @@ Bug Fixes to C++ Support
   containing outer unexpanded parameters were not correctly expanded. (#GH101754)
 - Fixed a bug in constraint expression comparison where the ``sizeof...`` expression was not handled properly
   in certain friend declarations. (#GH93099)
+- Clang no longer crashes when a lambda contains an invalid block declaration that contains an unexpanded
+  parameter pack. (#GH109148)
 
 Bug Fixes to AST Handling
 ^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/clang/include/clang/Sema/ScopeInfo.h b/clang/include/clang/Sema/ScopeInfo.h
index 700e361ef83f13..3bad1e3bad4312 100644
--- a/clang/include/clang/Sema/ScopeInfo.h
+++ b/clang/include/clang/Sema/ScopeInfo.h
@@ -793,6 +793,16 @@ class BlockScopeInfo final : public CapturingScopeInfo {
   /// Its return type may be BuiltinType::Dependent.
   QualType FunctionType;
 
+  /// We sometimes diagnose unexpanded parameter packs in block literals,
+  /// but an error while the block is parsed can cause it to be discarded,
+  /// in which case we need to reset the enclosing lambda's
+  /// ContainsUnexpandedParameterPack flag.
+  ///
+  /// Note: This issue does not exist with lambdas because they push a new
+  /// LambdaScopeInfo, so if the expression is discarded, the 'enclosing
+  /// lambda' is discarded along with it.
+  bool EnclosingLambdaContainsUnexpandedParameterPack = false;
+
   BlockScopeInfo(DiagnosticsEngine &Diag, Scope *BlockScope, BlockDecl *Block)
       : CapturingScopeInfo(Diag, ImpCap_Block), TheDecl(Block),
         TheScope(BlockScope) {
diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp
index 4be7dfbc293927..1e72910da27d5e 100644
--- a/clang/lib/Sema/Sema.cpp
+++ b/clang/lib/Sema/Sema.cpp
@@ -2187,9 +2187,13 @@ void Sema::PushFunctionScope() {
 }
 
 void Sema::PushBlockScope(Scope *BlockScope, BlockDecl *Block) {
-  FunctionScopes.push_back(new BlockScopeInfo(getDiagnostics(),
-                                              BlockScope, Block));
+  auto *BSI = new BlockScopeInfo(getDiagnostics(), BlockScope, Block);
+  FunctionScopes.push_back(BSI);
   CapturingFunctionScopes++;
+
+  LambdaScopeInfo *Enclosing = getEnclosingLambda();
+  BSI->EnclosingLambdaContainsUnexpandedParameterPack =
+      Enclosing && Enclosing->ContainsUnexpandedParameterPack;
 }
 
 LambdaScopeInfo *Sema::PushLambdaScope() {
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index 2db9d1fc69ed1e..d1c91296a9d468 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -16187,6 +16187,16 @@ void Sema::ActOnBlockArguments(SourceLocation CaretLoc, Declarator &ParamInfo,
 }
 
 void Sema::ActOnBlockError(SourceLocation CaretLoc, Scope *CurScope) {
+  // If the enclosing lambda did not contain any unexpanded parameter
+  // packs before this block, then reset the unexpanded parameter pack
+  // flag, otherwise, we might end up crashing trying to find a pack
+  // that we are about to discard along with the rest of the block.
+  if (!getCurBlock()->EnclosingLambdaContainsUnexpandedParameterPack) {
+    LambdaScopeInfo *L = getEnclosingLambda();
+    if (L)
+      L->ContainsUnexpandedParameterPack = false;
+  }
+
   // Leave the expression-evaluation context.
   DiscardCleanupsInEvaluationContext();
   PopExpressionEvaluationContext();
@@ -16379,6 +16389,8 @@ ExprResult Sema::ActOnBlockStmtExpr(SourceLocation CaretLoc,
   if (getCurFunction())
     getCurFunction()->addBlock(BD);
 
+  // This can happen if the block's return type is deduced, but
+  // the return expression is invalid.
   if (BD->isInvalidDecl())
     return CreateRecoveryExpr(Result->getBeginLoc(), Result->getEndLoc(),
                               {Result}, Result->getType());
diff --git a/clang/test/SemaCXX/block-unexpanded-pack.cpp b/clang/test/SemaCXX/block-unexpanded-pack.cpp
new file mode 100644
index 00000000000000..c1435b8e314a81
--- /dev/null
+++ b/clang/test/SemaCXX/block-unexpanded-pack.cpp
@@ -0,0 +1,48 @@
+// RUN: %clang_cc1 -fblocks -triple x86_64-apple-darwin -fsyntax-only -verify %s
+// RUN: %clang_cc1 -fblocks -triple x86_64-apple-darwin -fsyntax-only -verify %s -frecovery-ast -frecovery-ast-type
+
+// This checks that when a block is discarded, the enclosing lambda’s
+// unexpanded parameter pack flag is reset to what it was before the
+// block is parsed so we don't crash when trying to diagnose unexpanded
+// parameter packs in the lambda.
+
+template <typename ...Ts>
+void gh109148() {
+  (^Ts); // expected-error {{expected expression}} expected-error {{unexpanded parameter pack 'Ts'}}
+
+  [] {
+    (^Ts); // expected-error {{expected expression}}
+    ^Ts;   // expected-error {{expected expression}}
+    ^(Ts); // expected-error {{expected expression}}
+    ^ Ts); // expected-error {{expected expression}}
+  };
+
+  ([] {
+    (^Ts); // expected-error {{expected expression}}
+    ^Ts;   // expected-error {{expected expression}}
+    ^(Ts); // expected-error {{expected expression}}
+    ^ Ts); // expected-error {{expected expression}}
+  }, ...); // expected-error {{pack expansion does not contain any unexpanded parameter packs}}
+
+  [] { // expected-error {{unexpanded parameter pack 'Ts'}}
+    ^ (Ts) {};
+  };
+
+  [] { // expected-error {{unexpanded parameter pack 'Ts'}}
+    (void) ^ { Ts x; };
+  };
+
+  [] { // expected-error {{unexpanded parameter pack 'Ts'}}
+    Ts s;
+    (^Ts); // expected-error {{expected expression}}
+  };
+
+  ([] {
+    Ts s;
+    (^Ts); // expected-error {{expected expression}}
+  }, ...)
+
+  [] { // expected-error {{unexpanded parameter pack 'Ts'}}
+    ^ { Ts s; return not_defined; }; // expected-error {{use of undeclared identifier 'not_defined'}}
+  };
+}

@rjmccall
Copy link
Contributor

rjmccall commented Oct 2, 2024

I don't think there's a deep reason blocks and lambdas don't use quite the same scope mechanics in the compiler, so if you wanted to pursue that, it seems reasonable. But this approach also seems viable. I agree that removing the assertion would be the wrong thing to do, and I also don't see a reliable way to include the pack in the AST.

@hokein
Copy link
Collaborator

hokein commented Oct 2, 2024

The current solution seems reasonable to me.

somehow including the malformed block in the AST (by wrapping it in a RecoveryExpr perhaps?)

RecoveryExpr is designed to preserve Expr nodes, so it can't be used to retain arbitrary AST nodes. In the case of (^Ts), as far as I understand, Ts is parsed as a block-id, making RecoveryExpr inapplicable.

@Sirraide
Copy link
Contributor Author

Sirraide commented Oct 2, 2024

I don't think there's a deep reason blocks and lambdas don't use quite the same scope mechanics in the compiler, so if you wanted to pursue that, it seems reasonable. But this approach also seems viable.

Yeah, there is a comment about parameter packs in block arguments being a problem somewhere, but that’s from 2012, so that might no longer apply. I might look into this at some point if I have the time, but not for this pr (there are still other issues need to deal with first...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category extension:clang
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[clang] Ill-formed block expressions within a lambda can yield assert-failures
4 participants