Skip to content

[RFC][C++20][Modules] Fix crash when function and lambda inside loaded from different modules #104512

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions clang/include/clang/Serialization/ASTReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,15 @@ class ASTReader
/// once recursing loading has been completed.
llvm::SmallVector<NamedDecl *, 16> PendingOdrMergeChecks;

/// Lambdas that need to be loaded right after the function they belong to.
/// It is required to have canonical declaration for lambda class from the
/// same module as enclosing function. This is required to correctly resolve
/// captured variables in the lambda. Without this, due to lazy
/// deserialization canonical declarations for the function and lambdas can
/// be from different modules and DeclRefExprs may refer to the AST nodes
/// that don't exist in the function.
SmallVector<GlobalDeclID, 4> PendingLambdas;

using DataPointers =
std::pair<CXXRecordDecl *, struct CXXRecordDecl::DefinitionData *>;
using ObjCInterfaceDataPointers =
Expand Down
8 changes: 7 additions & 1 deletion clang/lib/Serialization/ASTReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9782,7 +9782,8 @@ void ASTReader::finishPendingActions() {
!PendingDeducedVarTypes.empty() || !PendingIncompleteDeclChains.empty() ||
!PendingDeclChains.empty() || !PendingMacroIDs.empty() ||
!PendingDeclContextInfos.empty() || !PendingUpdateRecords.empty() ||
!PendingObjCExtensionIvarRedeclarations.empty()) {
!PendingObjCExtensionIvarRedeclarations.empty() ||
!PendingLambdas.empty()) {
// If any identifiers with corresponding top-level declarations have
// been loaded, load those declarations now.
using TopLevelDeclsMap =
Expand Down Expand Up @@ -9927,6 +9928,11 @@ void ASTReader::finishPendingActions() {
}
PendingObjCExtensionIvarRedeclarations.pop_back();
}

// Load any pendiong lambdas.
for (auto ID : PendingLambdas)
GetDecl(ID);
PendingLambdas.clear();
}

// At this point, all update records for loaded decls are in place, so any
Expand Down
10 changes: 10 additions & 0 deletions clang/lib/Serialization/ASTReaderDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,16 @@ void ASTDeclReader::VisitFunctionDecl(FunctionDecl *FD) {
for (unsigned I = 0; I != NumParams; ++I)
Params.push_back(readDeclAs<ParmVarDecl>());
FD->setParams(Reader.getContext(), Params);

// For the first decl add all lambdas inside for loading them later,
// otherwise skip them.
unsigned NumLambdas = Record.readInt();
if (FD->isFirstDecl()) {
for (unsigned I = 0; I != NumLambdas; ++I)
Reader.PendingLambdas.push_back(Record.readDeclID());
} else {
Record.skipInts(NumLambdas);
Copy link
Member

Choose a reason for hiding this comment

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

Here is not paired with writing process. It will only write a zero if it is not the first.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ChuanqiXu9 could you please elaborate why you think it may not be paired? Code in ASTDeclWriter looks like this:

  if (D->isCanonicalDecl()) {
    llvm::SmallVector<const Decl *, 2> Lambdas = collectLambdas(D);
    Record.push_back(Lambdas.size());
    for (const auto *L : Lambdas)
      Record.AddDeclRef(L);
  } else {
    Record.push_back(0);
  }

So in the first case it writes size + following decls, in the second case it writes 0. I looked the implementation of skipInts for skipInts(0) it does nothing.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, it was an oversight. I took too quickly from the above stack trace.

}
}

void ASTDeclReader::VisitObjCMethodDecl(ObjCMethodDecl *MD) {
Expand Down
42 changes: 42 additions & 0 deletions clang/lib/Serialization/ASTWriterDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "clang/AST/Expr.h"
#include "clang/AST/OpenMPClause.h"
#include "clang/AST/PrettyDeclStackTrace.h"
#include "clang/AST/StmtVisitor.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Serialization/ASTReader.h"
#include "clang/Serialization/ASTRecordWriter.h"
Expand Down Expand Up @@ -625,6 +626,33 @@ void ASTDeclWriter::VisitDeclaratorDecl(DeclaratorDecl *D) {
: QualType());
}

static llvm::SmallVector<const Decl *, 2> collectLambdas(FunctionDecl *D) {
struct LambdaCollector : public ConstStmtVisitor<LambdaCollector> {
llvm::SmallVectorImpl<const Decl *> &Lambdas;

LambdaCollector(llvm::SmallVectorImpl<const Decl *> &Lambdas)
: Lambdas(Lambdas) {}

void VisitLambdaExpr(const LambdaExpr *E) {
VisitStmt(E);
Lambdas.push_back(E->getLambdaClass());
}

void VisitStmt(const Stmt *S) {
if (!S)
return;
for (const Stmt *Child : S->children())
if (Child)
Visit(Child);
}
};

llvm::SmallVector<const Decl *, 2> Lambdas;
if (D->hasBody())
LambdaCollector(Lambdas).VisitStmt(D->getBody());
return Lambdas;
}

void ASTDeclWriter::VisitFunctionDecl(FunctionDecl *D) {
static_assert(DeclContext::NumFunctionDeclBits == 44,
"You need to update the serializer after you change the "
Expand Down Expand Up @@ -764,6 +792,19 @@ void ASTDeclWriter::VisitFunctionDecl(FunctionDecl *D) {
Record.push_back(D->param_size());
for (auto *P : D->parameters())
Record.AddDeclRef(P);

// Store references to all lambda decls inside function to load them
// immediately after loading the function to make sure that canonical
// decls for lambdas will be from the same module.
if (D->isCanonicalDecl()) {
llvm::SmallVector<const Decl *, 2> Lambdas = collectLambdas(D);
Record.push_back(Lambdas.size());
for (const auto *L : Lambdas)
Record.AddDeclRef(L);
} else {
Record.push_back(0);
}

Code = serialization::DECL_FUNCTION;
}

Expand Down Expand Up @@ -2239,6 +2280,7 @@ getFunctionDeclAbbrev(serialization::DeclCode Code) {
//
// This is:
// NumParams and Params[] from FunctionDecl, and
// NumLambdas, Lambdas[] from FunctionDecl, and
// NumOverriddenMethods, OverriddenMethods[] from CXXMethodDecl.
//
// Add an AbbrevOp for 'size then elements' and use it here.
Expand Down
76 changes: 76 additions & 0 deletions clang/test/Headers/crash-instantiated-in-scope-cxx-modules.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// RUN: rm -fR %t
// RUN: split-file %s %t
// RUN: cd %t
// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header -Werror=uninitialized folly-conv.h
// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header -Werror=uninitialized thrift_cpp2_base.h
// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header -Werror=uninitialized -fmodule-file=folly-conv.pcm -fmodule-file=thrift_cpp2_base.pcm logger_base.h

//--- Conv.h
#pragma once

template <typename _Tp, typename _Up = _Tp&&>
_Up __declval(int);

template <typename _Tp>
auto declval() noexcept -> decltype(__declval<_Tp>(0));

namespace folly {

template <class Value, class Error>
struct Expected {
template <class Yes>
auto thenOrThrow() -> decltype(declval<Value&>()) {
return 1;
}
};

struct ExpectedHelper {
template <class Error, class T>
static constexpr Expected<T, Error> return_(T) {
return Expected<T, Error>();
}

template <class This, class Fn, class E = int, class T = ExpectedHelper>
static auto then_(This&&, Fn&&)
-> decltype(T::template return_<E>((declval<Fn>()(true), 0))) {
return Expected<int, int>();
}
};

template <class Tgt>
inline Expected<Tgt, const char*> tryTo() {
Tgt result = 0;
// In build with asserts:
// clang/lib/Sema/SemaTemplateInstantiate.cpp: llvm::PointerUnion<Decl *, LocalInstantiationScope::DeclArgumentPack *> *clang::LocalInstantiationScope::findInstantiationOf(const Decl *): Assertion `isa<LabelDecl>(D) && "declaration not instantiated in this scope"' failed.
// In release build compilation error on the line below inside lambda:
// error: variable 'result' is uninitialized when used here [-Werror,-Wuninitialized]
ExpectedHelper::then_(Expected<bool, int>(), [&](bool) { return result; });
return {};
}

} // namespace folly

inline void bar() {
folly::tryTo<int>();
}
// expected-no-diagnostics

//--- folly-conv.h
#pragma once
#include "Conv.h"
// expected-no-diagnostics

//--- thrift_cpp2_base.h
#pragma once
#include "Conv.h"
// expected-no-diagnostics

//--- logger_base.h
#pragma once
import "folly-conv.h";
import "thrift_cpp2_base.h";

inline void foo() {
folly::tryTo<unsigned>();
}
// expected-no-diagnostics
30 changes: 30 additions & 0 deletions clang/test/Headers/crash-instantiated-in-scope-cxx-modules2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// RUN: rm -fR %t
// RUN: split-file %s %t
// RUN: cd %t
// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header header.h
// RUN: %clang_cc1 -std=c++20 -fmodule-file=header.pcm main.cpp

//--- header.h
template <typename T>
void f(T) {}

class A {
virtual ~A();
};

inline A::~A() {
f([](){});
}

struct B {
void g() {
f([](){
[](){};
});
}
};
// expected-no-diagnostics

//--- main.cpp
import "header.h";
// expected-no-diagnostics