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

Merged
merged 6 commits into from
Oct 11, 2024

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.

This pr moves handling of unexpanded parameter packs into CapturingScopeInfo instead so that the same logic is used for both blocks and lambdas. This fixes this issue since the ContainsUnexpandedParameterPack flag is now part of the block (and before that, its CapturingScopeInfo) and no longer affects the surrounding lambda directly when the block is parsed. Moreover, this change makes blocks actually usable with pack expansion.

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...)

@cor3ntin
Copy link
Contributor

cor3ntin commented Oct 3, 2024

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:

I am not sure i understand that.

Imo the issue is that DiagnoseUnexpandedParameterPacks (where we set LambdaScopeInfo::ContainsUnexpandedParameterPack does not account for blocks - and probably should.

Ideally

  • DiagnoseUnexpandedParameterPacks would handle arbitrary capturing-scope thingies (statement expressions, lambdas, blocks, eventually do expressions, and other terrifying such constructs)
  • Once the innermost such construct is fully constructed, the ContainsUnexpandedParameterPack flag could bubble upward to the enclosing capturing scope

@Sirraide
Copy link
Contributor Author

Sirraide commented Oct 4, 2024

I am not sure i understand that.

Imo the issue is that DiagnoseUnexpandedParameterPacks (where we set LambdaScopeInfo::ContainsUnexpandedParameterPack does not account for blocks - and probably should.

Ideally

  • DiagnoseUnexpandedParameterPacks would handle arbitrary capturing-scope thingies (statement expressions, lambdas, blocks, eventually do expressions, and other terrifying such constructs)
  • Once the innermost such construct is fully constructed, the ContainsUnexpandedParameterPack flag could bubble upward to the enclosing capturing scope

Yeah, that’s also what I figured initially. I just assumed that there was some reason why blocks don’t or can’t work that way (I have never used Objective-C and I candidly don’t know a lot about blocks...), but it seems that there isn’t.

So if you think that this is a better idea, I can instead look into refactoring blocks to work the same way as lambdas wrt DiagnoseUnexpandedParameterPacks() (and I think that mainly just means moving some things from LambdaScopeInfo to CapturingScopeInfo or whatever the base class was called again and making DiagnoseUnexpandedParameterPacks() etc. use that instead).

@Sirraide
Copy link
Contributor Author

I can instead look into refactoring blocks to work the same way as lambdas

Surprisingly, this turned out to be fairly straight-forward to implement.

(I had to fix something in the template instantiator, but I’ve run into the exact same problem before with lambdas, so I knew what to do this time round ;Þ)

Copy link
Contributor

@rjmccall rjmccall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this looks really clean. Thanks for taking this on!

@Sirraide Sirraide merged commit 48bda00 into llvm:main Oct 11, 2024
5 of 7 checks passed
Copy link

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff 2aa1dbf9c92e3b27954ba9166c927616625073e8 daf41fb0502e54e4ab5c63ed2227b7be88297c74 --extensions cpp,h -- clang/test/SemaCXX/block-packs.cpp clang/include/clang/AST/ComputeDependence.h clang/include/clang/AST/Expr.h clang/include/clang/Sema/ScopeInfo.h clang/include/clang/Sema/Sema.h clang/include/clang/Sema/Template.h clang/lib/AST/ComputeDependence.cpp clang/lib/Sema/Sema.cpp clang/lib/Sema/SemaDecl.cpp clang/lib/Sema/SemaExpr.cpp clang/lib/Sema/SemaLambda.cpp clang/lib/Sema/SemaTemplate.cpp clang/lib/Sema/SemaTemplateInstantiate.cpp clang/lib/Sema/SemaTemplateVariadic.cpp
View the diff from clang-format here.
diff --git a/clang/lib/Sema/SemaTemplateVariadic.cpp b/clang/lib/Sema/SemaTemplateVariadic.cpp
index 19bd454766..42a075ae14 100644
--- a/clang/lib/Sema/SemaTemplateVariadic.cpp
+++ b/clang/lib/Sema/SemaTemplateVariadic.cpp
@@ -8,7 +8,6 @@
 //  This file implements semantic analysis for C++0x variadic templates.
 //===----------------------------------------------------------------------===/
 
-#include "clang/Sema/Sema.h"
 #include "TypeLocBuilder.h"
 #include "clang/AST/Expr.h"
 #include "clang/AST/RecursiveASTVisitor.h"
@@ -16,6 +15,7 @@
 #include "clang/Sema/Lookup.h"
 #include "clang/Sema/ParsedTemplate.h"
 #include "clang/Sema/ScopeInfo.h"
+#include "clang/Sema/Sema.h"
 #include "clang/Sema/SemaInternal.h"
 #include "clang/Sema/Template.h"
 #include "llvm/Support/SaveAndRestore.h"

@Sirraide
Copy link
Contributor Author

C/C++ code formatter, clang-format found issues in your code

... I don’t think I changed anything about that include? Or did it just reorder them because I added one? 🤔

DanielCChen pushed a commit to DanielCChen/llvm-project that referenced this pull request Oct 16, 2024
llvm#110762)

Consider llvm#109148:
```c++
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.

This pr moves handling of unexpanded parameter packs into
`CapturingScopeInfo` instead so that the same logic is used for both
blocks and lambdas. This fixes this issue since the
`ContainsUnexpandedParameterPack` flag is now part of the block (and
before that, its `CapturingScopeInfo`) and no longer affects the
surrounding lambda directly when the block is parsed. Moreover, this
change makes blocks actually usable with pack expansion.

This fixes llvm#109148.
bricknerb pushed a commit to bricknerb/llvm-project that referenced this pull request Oct 17, 2024
llvm#110762)

Consider llvm#109148:
```c++
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.

This pr moves handling of unexpanded parameter packs into
`CapturingScopeInfo` instead so that the same logic is used for both
blocks and lambdas. This fixes this issue since the
`ContainsUnexpandedParameterPack` flag is now part of the block (and
before that, its `CapturingScopeInfo`) and no longer affects the
surrounding lambda directly when the block is parsed. Moreover, this
change makes blocks actually usable with pack expansion.

This fixes llvm#109148.
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
6 participants