diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h --- a/clang/include/clang/AST/Decl.h +++ b/clang/include/clang/AST/Decl.h @@ -3822,6 +3822,7 @@ // to save some space. Use the provided accessors to access it. public: friend class DeclContext; + friend class ASTDeclReader; /// Enum that represents the different ways arguments are passed to and /// returned from function calls. This takes into account the target-specific /// and version-specific rules along with the rules determined by the @@ -4071,9 +4072,16 @@ /// nullptr is returned if no named data member exists. const FieldDecl *findFirstNamedDataMember() const; + /// Get precomputed ODRHash or add a new one. + unsigned getODRHash(); + private: /// Deserialize just the fields. void LoadFieldsFromExternalStorage() const; + + /// True if a valid hash is stored in ODRHash. + bool hasODRHash() const { return RecordDeclBits.ODRHash; } + void setODRHash(unsigned Hash) { RecordDeclBits.ODRHash = Hash; } }; class FileScopeAsmDecl : public Decl { diff --git a/clang/include/clang/AST/DeclBase.h b/clang/include/clang/AST/DeclBase.h --- a/clang/include/clang/AST/DeclBase.h +++ b/clang/include/clang/AST/DeclBase.h @@ -1528,10 +1528,14 @@ /// Represents the way this type is passed to a function. uint64_t ArgPassingRestrictions : 2; + + /// True if a valid hash is stored in ODRHash. This should shave off some + /// extra storage and prevent CXXRecordDecl to store unused bits. + uint64_t ODRHash : 28; }; /// Number of non-inherited bits in RecordDeclBitfields. - enum { NumRecordDeclBits = 14 }; + enum { NumRecordDeclBits = 42 }; /// Stores the bits used by OMPDeclareReductionDecl. /// If modified NumOMPDeclareReductionDeclBits and the accessor diff --git a/clang/include/clang/AST/ODRHash.h b/clang/include/clang/AST/ODRHash.h --- a/clang/include/clang/AST/ODRHash.h +++ b/clang/include/clang/AST/ODRHash.h @@ -55,6 +55,10 @@ // more information than the AddDecl class. void AddCXXRecordDecl(const CXXRecordDecl *Record); + // Use this for ODR checking records in C/Objective-C between modules. This + // method compares more information than the AddDecl class. + void AddRecordDecl(const RecordDecl *Record); + // Use this for ODR checking functions between modules. This method compares // more information than the AddDecl class. SkipBody will process the // hash as if the function has no body. diff --git a/clang/include/clang/Serialization/ASTReader.h b/clang/include/clang/Serialization/ASTReader.h --- a/clang/include/clang/Serialization/ASTReader.h +++ b/clang/include/clang/Serialization/ASTReader.h @@ -1131,6 +1131,10 @@ llvm::SmallDenseMap, 2> PendingEnumOdrMergeFailures; + /// C/ObjC definitions in which the structural equivalence check fails + llvm::SmallDenseMap, 2> + PendingRecordOdrMergeFailures; + /// DeclContexts in which we have diagnosed an ODR violation. llvm::SmallPtrSet DiagnosedOdrMergeFailures; diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp --- a/clang/lib/AST/Decl.cpp +++ b/clang/lib/AST/Decl.cpp @@ -4514,6 +4514,7 @@ setHasNonTrivialToPrimitiveCopyCUnion(false); setParamDestroyedInCallee(false); setArgPassingRestrictions(APK_CanPassInRegs); + RecordDeclBits.ODRHash = 0; } RecordDecl *RecordDecl::Create(const ASTContext &C, TagKind TK, DeclContext *DC, @@ -4675,6 +4676,19 @@ return nullptr; } +unsigned RecordDecl::getODRHash() { + if (hasODRHash()) + return RecordDeclBits.ODRHash; + + // Only calculate hash on first call of getODRHash per record. + ODRHash Hash; + Hash.AddRecordDecl(this); + // For RecordDecl the ODRHash is stored in the remaining 28 + // bit of RecordDeclBits, adjust the hash to accomodate. + setODRHash(Hash.CalculateHash() >> 4); + return RecordDeclBits.ODRHash; +} + //===----------------------------------------------------------------------===// // BlockDecl Implementation //===----------------------------------------------------------------------===// diff --git a/clang/lib/AST/ODRHash.cpp b/clang/lib/AST/ODRHash.cpp --- a/clang/lib/AST/ODRHash.cpp +++ b/clang/lib/AST/ODRHash.cpp @@ -469,6 +469,28 @@ ODRDeclVisitor(ID, *this).Visit(D); } +void ODRHash::AddRecordDecl(const RecordDecl *Record) { + AddDecl(Record); + + // Filter out sub-Decls which will not be processed in order to get an + // accurate count of Decl's. + llvm::SmallVector Decls; + for (Decl *SubDecl : Record->decls()) { + if (auto *SubRD = dyn_cast(SubDecl)) { + if (!SubRD->isAnonymousStructOrUnion()) + continue; + ID.AddInteger(SubRD->getODRHash()); + continue; + } + if (isDeclToBeProcessed(SubDecl, Record)) + Decls.push_back(SubDecl); + } + + ID.AddInteger(Decls.size()); + for (auto SubDecl : Decls) + AddSubDecl(SubDecl); +} + void ODRHash::AddCXXRecordDecl(const CXXRecordDecl *Record) { assert(Record && Record->hasDefinition() && "Expected non-null record to be a definition."); diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -9369,7 +9369,8 @@ void ASTReader::diagnoseOdrViolations() { if (PendingOdrMergeFailures.empty() && PendingOdrMergeChecks.empty() && PendingFunctionOdrMergeFailures.empty() && - PendingEnumOdrMergeFailures.empty()) + PendingEnumOdrMergeFailures.empty() && + PendingRecordOdrMergeFailures.empty()) return; // Trigger the import of the full definition of each class that had any @@ -9391,6 +9392,15 @@ } } + // Trigger the import of the full definition of each record in C/ObjC. + auto RecordOdrMergeFailures = std::move(PendingRecordOdrMergeFailures); + PendingRecordOdrMergeFailures.clear(); + for (auto &Merge : RecordOdrMergeFailures) { + Merge.first->decls_begin(); + for (auto &D : Merge.second) + D->decls_begin(); + } + // Trigger the import of functions. auto FunctionOdrMergeFailures = std::move(PendingFunctionOdrMergeFailures); PendingFunctionOdrMergeFailures.clear(); @@ -9498,7 +9508,7 @@ } if (OdrMergeFailures.empty() && FunctionOdrMergeFailures.empty() && - EnumOdrMergeFailures.empty()) + EnumOdrMergeFailures.empty() && RecordOdrMergeFailures.empty()) return; // Ensure we don't accidentally recursively enter deserialization while @@ -9644,9 +9654,6 @@ return true; } - assert(getContext().hasSameType(FirstField->getType(), - SecondField->getType())); - QualType FirstType = FirstField->getType(); QualType SecondType = SecondField->getType(); if (ComputeQualTypeODRHash(FirstType) != @@ -11189,6 +11196,130 @@ } } + auto PopulateRecordHashes = [&ComputeSubDeclODRHash](DeclHashes &Hashes, + RecordDecl *Record, + const DeclContext *DC) { + std::deque> WorkList; + for (auto *D : Record->decls()) + WorkList.push_back(std::make_pair(D, DC)); + + while (!WorkList.empty()) { + auto &P = WorkList.front(); + WorkList.pop_front(); + auto *SubRec = dyn_cast(P.first); + if (!SubRec) { + if (!ODRHash::isDeclToBeProcessed(P.first, P.second)) + continue; + Hashes.emplace_back(P.first, ComputeSubDeclODRHash(P.first)); + continue; + } + if (!SubRec->isAnonymousStructOrUnion()) + continue; + for (auto *SubD : SubRec->decls()) + WorkList.push_front(std::make_pair(SubD, SubRec)); + } + }; + + // Issue any pending ODR-failure (for structural equivalence checks) + // diagnostics for RecordDecl in C/ObjC, note that in C++ this is + // done as paert of CXXRecordDecl ODR checking. + for (auto &Merge : RecordOdrMergeFailures) { + // If we've already pointed out a specific problem with this class, don't + // bother issuing a general "something's different" diagnostic. + if (!DiagnosedOdrMergeFailures.insert(Merge.first).second) + continue; + + bool Diagnosed = false; + RecordDecl *FirstRecord = Merge.first; + + std::string FirstModule = getOwningModuleNameForDiagnostic(FirstRecord); + for (auto *SecondRecord : Merge.second) { + // Multiple different declarations got merged together; tell the user + // where they came from. + if (FirstRecord == SecondRecord) + continue; + + std::string SecondModule = getOwningModuleNameForDiagnostic(SecondRecord); + DeclHashes FirstHashes; + DeclHashes SecondHashes; + + const DeclContext *DC = FirstRecord; + PopulateRecordHashes(FirstHashes, FirstRecord, DC); + PopulateRecordHashes(SecondHashes, SecondRecord, DC); + + auto DR = FindTypeDiffs(FirstHashes, SecondHashes); + ODRMismatchDecl FirstDiffType = DR.FirstDiffType; + ODRMismatchDecl SecondDiffType = DR.SecondDiffType; + Decl *FirstDecl = DR.FirstDecl; + Decl *SecondDecl = DR.SecondDecl; + + // Reaching this point means an unexpected Decl was encountered + // or no difference was detected. This causes a generic error + // message to be emitted. + if (FirstDiffType == Other || SecondDiffType == Other) { + DiagnoseODRUnexpected(DR, FirstRecord, FirstModule, SecondRecord, + SecondModule); + Diagnosed = true; + break; + } + + if (FirstDiffType != SecondDiffType) { + DiagnoseODRMismatch(DR, FirstRecord, FirstModule, SecondRecord, + SecondModule); + Diagnosed = true; + break; + } + + assert(FirstDiffType == SecondDiffType); + switch (FirstDiffType) { + case EndOfClass: + case Other: + // C++ only, invalid in this context. + case PublicSpecifer: + case PrivateSpecifer: + case ProtectedSpecifer: + case StaticAssert: + case CXXMethod: + case TypeAlias: + case Friend: + case FunctionTemplate: + llvm_unreachable("Invalid diff type"); + + case Field: { + Diagnosed = ODRDiagField(FirstRecord, FirstModule, SecondModule, + cast(FirstDecl), + cast(SecondDecl)); + break; + } + case TypeDef: { + Diagnosed = ODRDiagTypeDefOrAlias( + FirstRecord, FirstModule, SecondModule, + cast(FirstDecl), cast(SecondDecl), + false /* IsTypeAlias */); + break; + } + case Var: { + Diagnosed = + ODRDiagVar(FirstRecord, FirstModule, SecondModule, + cast(FirstDecl), cast(SecondDecl)); + break; + } + } + + if (Diagnosed) + continue; + + Diag(FirstDecl->getLocation(), + diag::err_module_odr_violation_mismatch_decl_unknown) + << FirstRecord << FirstModule.empty() << FirstModule << FirstDiffType + << FirstDecl->getSourceRange(); + Diag(SecondDecl->getLocation(), + diag::note_module_odr_violation_mismatch_decl_unknown) + << SecondModule << FirstDiffType << SecondDecl->getSourceRange(); + Diagnosed = true; + } + } + // Issue ODR failures diagnostics for functions. for (auto &Merge : FunctionOdrMergeFailures) { enum ODRFunctionDifference { diff --git a/clang/lib/Serialization/ASTReaderDecl.cpp b/clang/lib/Serialization/ASTReaderDecl.cpp --- a/clang/lib/Serialization/ASTReaderDecl.cpp +++ b/clang/lib/Serialization/ASTReaderDecl.cpp @@ -734,8 +734,11 @@ llvm_unreachable("unexpected tag info kind"); } - if (!isa(TD)) - mergeRedeclarable(TD, Redecl); + if (isa(TD)) + return Redecl; + + // Handle merging in C and Objective-C + mergeRedeclarable(TD, Redecl); return Redecl; } @@ -805,6 +808,28 @@ RD->setHasNonTrivialToPrimitiveCopyCUnion(Record.readInt()); RD->setParamDestroyedInCallee(Record.readInt()); RD->setArgPassingRestrictions((RecordDecl::ArgPassingKind)Record.readInt()); + if (!isa(RD)) { + RD->setODRHash(Record.readInt()); + + // C++ applies ODR checking in VisitCXXRecordDecl instead. Note that + // structural equivalence is the usual way to check for ODR-like semantics + // in ObjC/C, but using ODRHash is prefered if possible because of better + // performance. + RecordDecl *Canon = static_cast(RD->getCanonicalDecl()); + if (RD == Canon || Canon->getODRHash() == RD->getODRHash()) + return Redecl; + // No point in checking equivalence between types that don't match in + // presence of definition. Note that we might still wanna check when + // both types don't have a definition (e.g. when adding checks for + // mismtaches in their __attribute__ lists) + if (RD->isThisDeclarationADefinition() != + Canon->isThisDeclarationADefinition()) + return Redecl; + Reader.PendingRecordOdrMergeFailures[Canon].push_back(RD); + // Track that we merged the definitions. + Reader.MergedDeclContexts.insert(std::make_pair(RD, Canon)); + } + return Redecl; } @@ -2645,7 +2670,8 @@ if (!ND) return false; // TODO: implement merge for other necessary decls. - if (isa(ND)) + if (isa(ND) || isa(ND) || + isa(ND)) return true; return false; } @@ -3310,6 +3336,10 @@ return ED->getASTContext().getLangOpts().CPlusPlus? ED->getDefinition() : nullptr; + if (auto *RD = dyn_cast(DC)) + if (!RD->getASTContext().getLangOpts().CPlusPlus) + return RD->getCanonicalDecl()->getDefinition(); + // We can see the TU here only if we have no Sema object. In that case, // there's no TU scope to look in, so using the DC alone is sufficient. if (auto *TU = dyn_cast(DC)) diff --git a/clang/lib/Serialization/ASTWriterDecl.cpp b/clang/lib/Serialization/ASTWriterDecl.cpp --- a/clang/lib/Serialization/ASTWriterDecl.cpp +++ b/clang/lib/Serialization/ASTWriterDecl.cpp @@ -487,6 +487,10 @@ Record.push_back(D->hasNonTrivialToPrimitiveCopyCUnion()); Record.push_back(D->isParamDestroyedInCallee()); Record.push_back(D->getArgPassingRestrictions()); + // Only compute this for C/Objective-C, in C++ this is computed as part + // of CXXRecordDecl. + if (!isa(D)) + Record.push_back(D->getODRHash()); if (D->getDeclContext() == D->getLexicalDeclContext() && !D->hasAttrs() && @@ -2083,6 +2087,8 @@ Abv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Fixed, 1)); // getArgPassingRestrictions Abv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Fixed, 2)); + // ODRHash + Abv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Fixed, 28)); // DC Abv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::VBR, 6)); // LexicalOffset diff --git a/clang/test/Modules/odr_hash-record.c b/clang/test/Modules/odr_hash-record.c new file mode 100644 --- /dev/null +++ b/clang/test/Modules/odr_hash-record.c @@ -0,0 +1,391 @@ +// Clear and create directories +// RUN: rm -rf %t +// RUN: mkdir %t +// RUN: mkdir %t/cache +// RUN: mkdir %t/Inputs + +// Build first header file +// RUN: echo "#define FIRST" >> %t/Inputs/first.h +// RUN: cat %s >> %t/Inputs/first.h + +// Build second header file +// RUN: echo "#define SECOND" >> %t/Inputs/second.h +// RUN: cat %s >> %t/Inputs/second.h + +// Test that each header can compile +// RUN: %clang_cc1 -fsyntax-only -x c %t/Inputs/first.h +// RUN: %clang_cc1 -fsyntax-only -x c %t/Inputs/second.h + +// Build module map file +// RUN: echo "module FirstModule {" >> %t/Inputs/module.map +// RUN: echo " header \"first.h\"" >> %t/Inputs/module.map +// RUN: echo "}" >> %t/Inputs/module.map +// RUN: echo "module SecondModule {" >> %t/Inputs/module.map +// RUN: echo " header \"second.h\"" >> %t/Inputs/module.map +// RUN: echo "}" >> %t/Inputs/module.map + +// Run test +// RUN: %clang_cc1 -triple x86_64-linux-gnu -x c \ +// RUN: -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \ +// RUN: -I%t/Inputs -verify %s + +#if !defined(FIRST) && !defined(SECOND) +#include "first.h" +#include "second.h" +#endif + +#if defined(FIRST) +struct S1 {}; +struct S1 s1a; +#elif defined(SECOND) +struct S1 {}; +#else +struct S1 s1; +#endif + +#if defined(FIRST) +struct S2 { + int x; + int y; +}; +#elif defined(SECOND) +struct S2 { + int y; + int x; +}; +#else +struct S2 s2; +// expected-error@first.h:* {{'S2' has different definitions in different modules; first difference is definition in module 'FirstModule' found field 'x'}} +// expected-note@second.h:* {{but in 'SecondModule' found field 'y'}} +#endif + +#if defined(FIRST) +struct S3 { + double x; +}; +#elif defined(SECOND) +struct S3 { + int x; +}; +#else +struct S3 s3; +// expected-error@second.h:* {{'S3::x' from module 'SecondModule' is not present in definition of 'struct S3' in module 'FirstModule'}} +// expected-note@first.h:* {{declaration of 'x' does not match}} +#endif + +#if defined(FIRST) +typedef int A; +struct S4 { + A x; +}; + +struct S5 { + A x; +}; +#elif defined(SECOND) +typedef int B; +struct S4 { + B x; +}; + +struct S5 { + int x; +}; +#else +struct S4 s4; +// expected-error@first.h:* {{'S4' has different definitions in different modules; first difference is definition in module 'FirstModule' found field 'x' with type 'A' (aka 'int')}} +// expected-note@second.h:* {{but in 'SecondModule' found field 'x' with type 'B' (aka 'int')}} + +struct S5 s5; +// expected-error@first.h:* {{'S5' has different definitions in different modules; first difference is definition in module 'FirstModule' found field 'x' with type 'A' (aka 'int')}} +// expected-note@second.h:* {{but in 'SecondModule' found field 'x' with type 'int'}} +#endif + +#if defined(FIRST) +struct S6 { + unsigned x; +}; +#elif defined(SECOND) +struct S6 { + unsigned x : 1; +}; +#else +struct S6 s6; +// expected-error@first.h:* {{'S6' has different definitions in different modules; first difference is definition in module 'FirstModule' found non-bitfield 'x'}} +// expected-note@second.h:* {{but in 'SecondModule' found bitfield 'x'}} +#endif + +#if defined(FIRST) +struct S7 { + unsigned x : 2; +}; +#elif defined(SECOND) +struct S7 { + unsigned x : 1; +}; +#else +struct S7 s7; +// expected-error@first.h:* {{'S7' has different definitions in different modules; first difference is definition in module 'FirstModule' found bitfield 'x' with one width expression}} +// expected-note@second.h:* {{but in 'SecondModule' found bitfield 'x' with different width expression}} +#endif + +#if defined(FIRST) +struct S8 { + unsigned x : 2; +}; +#elif defined(SECOND) +struct S8 { + unsigned x : 1 + 1; +}; +#else +struct S8 s8; +// expected-error@first.h:* {{'S8' has different definitions in different modules; first difference is definition in module 'FirstModule' found bitfield 'x' with one width expression}} +// expected-note@second.h:* {{but in 'SecondModule' found bitfield 'x' with different width expression}} +#endif + +#if defined(FIRST) +struct S12 { + unsigned x[5]; +}; +#elif defined(SECOND) +struct S12 { + unsigned x[7]; +}; +#else +struct S12 s12; +// expected-error@second.h:* {{'S12::x' from module 'SecondModule' is not present in definition of 'struct S12' in module 'FirstModule'}} +// expected-note@first.h:* {{declaration of 'x' does not match}} +#endif + +#if defined(FIRST) +struct S13 { + unsigned x[7]; +}; +#elif defined(SECOND) +struct S13 { + double x[7]; +}; +#else +struct S13 s13; +// expected-error@second.h:* {{'S13::x' from module 'SecondModule' is not present in definition of 'struct S13' in module 'FirstModule'}} +// expected-note@first.h:* {{declaration of 'x' does not match}} +#endif + +#if defined(FIRST) +struct B1 {}; +struct SS1 { + struct B1 x; +}; +#elif defined(SECOND) +struct A1 {}; +struct SS1 { + struct A1 x; +}; +#else +struct SS1 ss1; +// expected-error@second.h:* {{'SS1::x' from module 'SecondModule' is not present in definition of 'struct SS1' in module 'FirstModule'}} +// expected-note@first.h:* {{declaration of 'x' does not match}} +#endif + +#if defined(FIRST) +enum E1 { x42 }; +struct SE1 { + enum E1 x; +}; +#elif defined(SECOND) +enum E2 { x42 }; +struct SE1 { + enum E2 x; +}; +#else +struct SE1 se1; +// expected-error@second.h:* {{'SE1::x' from module 'SecondModule' is not present in definition of 'struct SE1' in module 'FirstModule'}} +// expected-note@first.h:* {{declaration of 'x' does not match}} +#endif + +// struct with forward declaration +#if defined(FIRST) +struct P {}; +struct S { + struct P *ptr; +}; +#elif defined(SECOND) +struct S { + struct P *ptr; +}; +#else +struct S s; +#endif + +// struct with forward declaration and no definition +#if defined(FIRST) +struct PA; +struct SA { + struct PA *ptr; +}; +#elif defined(SECOND) +struct SA { + struct PA *ptr; +}; +#else +struct SA sa; +#endif + +// struct with multiple typedefs +#if defined(FIRST) +typedef int BB1; +typedef BB1 AA1; +struct TS1 { + AA1 x; +}; +#elif defined(SECOND) +typedef int AA1; +struct TS1 { + AA1 x; +}; +#else +struct TS1 ts1; +#endif + +#if defined(FIRST) +struct T2 { + int x; +}; +typedef struct T2 B2; +typedef struct B2 A2; +struct TS2 { + struct T2 x; +}; +#elif defined(SECOND) +struct T2 { + int x; +}; +typedef struct T2 A2; +struct TS2 { + struct T2 x; +}; +#else +struct TS2 ts2; +#endif + +#if defined(FIRST) +struct T3; +struct TS3 { + struct T3 *t; +}; +#elif defined(SECOND) +typedef struct T3 { +} T3; +struct TS3 { + struct T3 *t; +}; +#else +struct TS3 ts3; +#endif + +#if defined(FIRST) +struct AU { + union { + int a; + }; +}; +#elif defined(SECOND) +struct AU { + union { + char a; + }; +}; +#else +struct AU au; +// expected-error@first.h:* {{'AU' has different definitions in different modules; first difference is definition in module 'FirstModule' found field 'a' with type 'int'}} +// expected-note@second.h:* {{but in 'SecondModule' found field 'a' with type 'char'}} +#endif + +#if defined(FIRST) +struct AUS { + union { + int a; + }; + struct { + char b; + }; +}; +#elif defined(SECOND) +struct AUS { + union { + int a; + }; + struct { + int b; + }; +}; +#else +struct AUS aus; +// expected-error@first.h:* {{'AUS' has different definitions in different modules; first difference is definition in module 'FirstModule' found field 'b' with type 'char'}} +// expected-note@second.h:* {{but in 'SecondModule' found field 'b' with type 'int'}} +#endif + +#if defined(FIRST) +struct AUS1 { + union { + int x; + union { + int y; + struct { + int z; + }; + }; + }; +}; +#elif defined(SECOND) +struct AUS1 { + union { + int x; + union { + int y; + struct { + char z; + }; + }; + }; +}; +#else +struct AUS1 aus1; +// expected-error@first.h:* {{'AUS1' has different definitions in different modules; first difference is definition in module 'FirstModule' found field 'z' with type 'int'}} +// expected-note@second.h:* {{but in 'SecondModule' found field 'z' with type 'char'}} +#endif + +#if defined(FIRST) +union U { + int a; + char b; +}; +#elif defined(SECOND) +union U { + int a; + float b; +}; +#else +union U u; +// expected-error@second.h:* {{'U::b' from module 'SecondModule' is not present in definition of 'union U' in module 'FirstModule'}} +// expected-note@first.h:* {{declaration of 'b' does not match}} +#endif + +#if defined(FIRST) +struct TSS1 { + int tss1; +}; +#elif defined(SECOND) +typedef struct TSS1 TSS1; +#else +struct TSS1 *tss1; +#endif + +// Keep macros contained to one file. +#ifdef FIRST +#undef FIRST +#endif + +#ifdef SECOND +#undef SECOND +#endif