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 @@ -26,6 +26,7 @@ #include "clang/Basic/Diagnostic.h" #include "clang/Basic/IdentifierTable.h" #include "clang/Basic/LLVM.h" +#include "clang/Basic/LangOptions.h" #include "clang/Basic/Linkage.h" #include "clang/Basic/OperatorKinds.h" #include "clang/Basic/PartialDiagnostic.h" @@ -3123,6 +3124,16 @@ return hasInClassInitializer() && (BitField ? InitAndBitWidth->Init : Init); } + /// Whether it resembles a flexible array member. + bool isFlexibleArrayMemberLike( + ASTContext &Context, + LangOptions::StrictFlexArraysLevelKind StrictFlexArraysLevel, + bool IgnoreTemplateOrMacroSubstitution) const; + + std::optional isFlexibleArrayMemberLike( + ASTContext &Ctx, QualType Ty, bool isUnion, + LangOptions::StrictFlexArraysLevelKind StrictFlexArraysLevel) const; + /// Get the C++11 default member initializer for this member, or null if one /// has not been set. If a valid declaration has a default member initializer, /// but this returns null, then we have not parsed and attached it yet. diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -4233,3 +4233,17 @@ let Documentation = [Undocumented]; } +def CountedBy : InheritableAttr { + let Spellings = [Clang<"counted_by">]; + let Subjects = SubjectList<[Field]>; + let Args = [IdentifierArgument<"CountedByField">]; + let Documentation = [CountedByDocs]; + let LangOpts = [COnly]; + code AdditionalMembers = [{ + private: + SourceRange countedByFieldLoc; + public: + SourceRange getCountedByFieldLoc(void) const { return countedByFieldLoc; } + void setCountedByFieldLoc(SourceRange Loc) { countedByFieldLoc = Loc; } + }]; +} diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -7189,3 +7189,47 @@ (if any) of these ``cleanup`` callback functions. }]; } + +def CountedByDocs : Documentation { + let Category = DocCatField; + let Content = [{ +Clang supports the ``counted_by`` attribute for flexible array members. The +argument for the ``counted_by`` attribute is the name of a field member in +the same structure holding the count of elements in the flexible array. + +For example: + +.. code-block:: c + + struct bar; + + struct foo { + size_t num_fam_elements; + /* ... */ + struct bar *fam[] __attribute__((counted_by(num_fam_elements))); + }; + + struct foo *foo_alloc(size_t num_elements) { + struct foo *f; + + f = malloc(sizeof(struct foo) + num_elements * sizeof(struct bar *)); + f->num_fam_elements = num_elements; + return f; + } + +This attribute is used with the ``-fsantize=array-bounds`` flag to trap if a +flexible array access is outside of the number of elements. + +.. code-block:: c + + void mux(struct foo *); + + struct bar *baz(void) { + struct foo *f = foo_alloc(128); + + mux(f); /* Fills in fam element. */ + + return f->fam[256]; /* Trap. */ + } + }]; +} diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -3419,6 +3419,9 @@ def err_alignment_dependent_typedef_name : Error< "requested alignment is dependent but declaration is not dependent">; +def warn_counted_by_placeholder : Warning< + "counted_by field %0 not found">; + def warn_alignment_builtin_useless : Warning< "%select{aligning a value|the result of checking whether a value is aligned}0" " to 1 byte is %select{a no-op|always true}0">, InGroup; diff --git a/clang/lib/AST/ASTImporter.cpp b/clang/lib/AST/ASTImporter.cpp --- a/clang/lib/AST/ASTImporter.cpp +++ b/clang/lib/AST/ASTImporter.cpp @@ -8907,6 +8907,12 @@ public: AttrImporter(ASTImporter &I) : Importer(I), NImporter(I) {} + // Useful for accessing the imported attribute. + template + T *getAttrAs() { return cast(ToAttr); } + template + const T *getAttrAs() const { return cast(ToAttr); } + // Create an "importer" for an attribute parameter. // Result of the 'value()' of that object is to be passed to the function // 'importAttr', in the order that is expected by the attribute class. @@ -9113,6 +9119,15 @@ From->args_size()); break; } + case attr::CountedBy: { + AI.cloneAttr(FromAttr); + const auto *ECA = cast(FromAttr); + Expected SR = Import(ECA->getCountedByFieldLoc()).get(); + if (!SR) + return SR.takeError(); + AI.getAttrAs()->setCountedByFieldLoc(SR.get()); + break; + } default: { // The default branch works for attributes that have no arguments to import. 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 @@ -4538,6 +4538,102 @@ return CachedFieldIndex - 1; } +std::optional FieldDecl::isFlexibleArrayMemberLike( + ASTContext &Ctx, QualType Ty, bool isUnion, + LangOptions::StrictFlexArraysLevelKind StrictFlexArraysLevel) const { + // For compatibility with existing code, we treat arrays of length 0 or + // 1 as flexible array members. + if (const auto *CAT = Ctx.getAsConstantArrayType(Ty)) { + llvm::APInt Size = CAT->getSize(); + + using FAMKind = LangOptions::StrictFlexArraysLevelKind; + if (StrictFlexArraysLevel == FAMKind::IncompleteOnly) + return false; + + // GCC extension, only allowed to represent a FAM. + if (Size == 0) + return true; + + if (StrictFlexArraysLevel == FAMKind::ZeroOrIncomplete && Size.uge(1)) + return false; + + if (StrictFlexArraysLevel == FAMKind::OneZeroOrIncomplete && Size.uge(2)) + return false; + + // GCC treats an array memeber of a union as an FAM if the size is one or + // zero. + if (isUnion && (Size.isZero() || Size.isOne())) + return true; + } else if (!Ctx.getAsIncompleteArrayType(Ty)) { + return false; + } + + return {}; +} + +bool FieldDecl::isFlexibleArrayMemberLike( + ASTContext &Ctx, + LangOptions::StrictFlexArraysLevelKind StrictFlexArraysLevel, + bool IgnoreTemplateOrMacroSubstitution) const { + // For compatibility with existing code, we treat arrays of length 0 or + // 1 as flexible array members. + if (const auto *CAT = Ctx.getAsConstantArrayType(getType())) { + llvm::APInt Size = CAT->getSize(); + + using FAMKind = LangOptions::StrictFlexArraysLevelKind; + if (StrictFlexArraysLevel == FAMKind::IncompleteOnly) + return false; + + // GCC extension, only allowed to represent a FAM. + if (Size == 0) + return true; + + if (StrictFlexArraysLevel == FAMKind::ZeroOrIncomplete && Size.uge(1)) + return false; + + if (StrictFlexArraysLevel == FAMKind::OneZeroOrIncomplete && Size.uge(2)) + return false; + + // GCC treats an array memeber of a union as an FAM if the size is one or + // zero. + if (getParent()->isUnion() && (Size.isZero() || Size.isOne())) + return true; + } else if (!Ctx.getAsIncompleteArrayType(getType())) { + return false; + } + + if (const auto *OID = dyn_cast(this)) + return OID->getNextIvar() == nullptr; + + // Don't consider sizes resulting from macro expansions or template argument + // substitution to form C89 tail-padded arrays. + if (IgnoreTemplateOrMacroSubstitution) { + TypeSourceInfo *TInfo = getTypeSourceInfo(); + while (TInfo) { + TypeLoc TL = TInfo->getTypeLoc(); + + // Look through typedefs. + if (TypedefTypeLoc TTL = TL.getAsAdjusted()) { + const TypedefNameDecl *TDL = TTL.getTypedefNameDecl(); + TInfo = TDL->getTypeSourceInfo(); + continue; + } + + if (ConstantArrayTypeLoc CTL = TL.getAs()) { + const Expr *SizeExpr = dyn_cast(CTL.getSizeExpr()); + if (!SizeExpr || SizeExpr->getExprLoc().isMacroID()) + return false; + } + + break; + } + } + + RecordDecl::field_iterator FI( + DeclContext::decl_iterator(const_cast(this))); + return ++FI == getParent()->field_end(); +} + SourceRange FieldDecl::getSourceRange() const { const Expr *FinalExpr = getInClassInitializer(); if (!FinalExpr) diff --git a/clang/lib/AST/Expr.cpp b/clang/lib/AST/Expr.cpp --- a/clang/lib/AST/Expr.cpp +++ b/clang/lib/AST/Expr.cpp @@ -205,84 +205,30 @@ } bool Expr::isFlexibleArrayMemberLike( - ASTContext &Context, + ASTContext &Ctx, LangOptions::StrictFlexArraysLevelKind StrictFlexArraysLevel, bool IgnoreTemplateOrMacroSubstitution) const { - - // For compatibility with existing code, we treat arrays of length 0 or - // 1 as flexible array members. - const auto *CAT = Context.getAsConstantArrayType(getType()); - if (CAT) { - llvm::APInt Size = CAT->getSize(); - - using FAMKind = LangOptions::StrictFlexArraysLevelKind; - - if (StrictFlexArraysLevel == FAMKind::IncompleteOnly) - return false; - - // GCC extension, only allowed to represent a FAM. - if (Size == 0) - return true; - - if (StrictFlexArraysLevel == FAMKind::ZeroOrIncomplete && Size.uge(1)) - return false; - - if (StrictFlexArraysLevel == FAMKind::OneZeroOrIncomplete && Size.uge(2)) - return false; - } else if (!Context.getAsIncompleteArrayType(getType())) - return false; - const Expr *E = IgnoreParens(); - const NamedDecl *ND = nullptr; - if (const auto *DRE = dyn_cast(E)) - ND = DRE->getDecl(); - else if (const auto *ME = dyn_cast(E)) + + if (const auto *ME = dyn_cast(E)) { ND = ME->getMemberDecl(); - else if (const auto *IRE = dyn_cast(E)) - return IRE->getDecl()->getNextIvar() == nullptr; + } else if (const auto *IRE = dyn_cast(E)) { + ND = IRE->getDecl(); + } else if (const auto *DRE = dyn_cast(E)) { + const RecordDecl *RD = DRE->getDecl()->getType()->getAsRecordDecl(); + if (!RD || !RD->hasFlexibleArrayMember()) + return false; - if (!ND) - return false; - - // A flexible array member must be the last member in the class. - // FIXME: If the base type of the member expr is not FD->getParent(), - // this should not be treated as a flexible array member access. - if (const auto *FD = dyn_cast(ND)) { - // GCC treats an array memeber of a union as an FAM if the size is one or - // zero. - if (CAT) { - llvm::APInt Size = CAT->getSize(); - if (FD->getParent()->isUnion() && (Size.isZero() || Size.isOne())) - return true; - } - - // Don't consider sizes resulting from macro expansions or template argument - // substitution to form C89 tail-padded arrays. - if (IgnoreTemplateOrMacroSubstitution) { - TypeSourceInfo *TInfo = FD->getTypeSourceInfo(); - while (TInfo) { - TypeLoc TL = TInfo->getTypeLoc(); - // Look through typedefs. - if (TypedefTypeLoc TTL = TL.getAsAdjusted()) { - const TypedefNameDecl *TDL = TTL.getTypedefNameDecl(); - TInfo = TDL->getTypeSourceInfo(); - continue; - } - if (ConstantArrayTypeLoc CTL = TL.getAs()) { - const Expr *SizeExpr = dyn_cast(CTL.getSizeExpr()); - if (!SizeExpr || SizeExpr->getExprLoc().isMacroID()) - return false; - } - break; - } - } - - RecordDecl::field_iterator FI( - DeclContext::decl_iterator(const_cast(FD))); - return ++FI == FD->getParent()->field_end(); + for (RecordDecl::field_iterator It = RD->field_begin(); + It != RD->field_end(); ++It) + ND = *It; } + if (const auto *FD = dyn_cast(ND)) + return FD->isFlexibleArrayMemberLike(Ctx, StrictFlexArraysLevel, + IgnoreTemplateOrMacroSubstitution); + return false; } diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp --- a/clang/lib/CodeGen/BackendUtil.cpp +++ b/clang/lib/CodeGen/BackendUtil.cpp @@ -981,7 +981,8 @@ // Register callbacks to schedule sanitizer passes at the appropriate part // of the pipeline. - if (LangOpts.Sanitize.has(SanitizerKind::LocalBounds)) + if (LangOpts.Sanitize.has(SanitizerKind::LocalBounds) || + LangOpts.Sanitize.has(SanitizerKind::ArrayBounds)) PB.registerScalarOptimizerLateEPCallback( [](FunctionPassManager &FPM, OptimizationLevel Level) { FPM.addPass(BoundsCheckingPass()); diff --git a/clang/lib/CodeGen/CGBuiltin.cpp b/clang/lib/CodeGen/CGBuiltin.cpp --- a/clang/lib/CodeGen/CGBuiltin.cpp +++ b/clang/lib/CodeGen/CGBuiltin.cpp @@ -852,6 +852,51 @@ } } + if (IsDynamic) { + // Check if the RecordDecl is a FAM with a CountedBy attribute. We can then + // pass that information to the __builtin_dynamic_object_size. + // + // EXPECT_EQ(__builtin_dynamic_object_size(p, 1), + // sizeof(*p) + p->count * sizeof(*p->array)); + // + const LangOptions::StrictFlexArraysLevelKind StrictFlexArraysLevel = + getLangOpts().getStrictFlexArraysLevel(); + const Expr *Base = E->IgnoreParenImpCasts(); + + if (FieldDecl *FD = FindCountedByField(Base, StrictFlexArraysLevel)) { + const auto *ME = dyn_cast(Base); + if (!ME) { + const auto *DRE = dyn_cast(Base); + ValueDecl *VD = nullptr; + + if (auto *RD = DRE->getType()->getPointeeType()->getAsRecordDecl()) + for (FieldDecl *Field : RD->fields()) + VD = Field; + + Expr *ICE = ImplicitCastExpr::Create(getContext(), DRE->getType(), + CK_LValueToRValue, + const_cast(dyn_cast(DRE)), + nullptr, VK_PRValue, + FPOptionsOverride()); + ME = MemberExpr::CreateImplicit(getContext(), ICE, true, VD, + VD->getType(), VK_LValue, + OK_Ordinary); + } + + // At this point, we know that \p ME is a flexible array member. + const auto *ArrayTy = dyn_cast(ME->getType()); + unsigned Size = getContext().getTypeSize(ArrayTy->getElementType()); + + llvm::Value *CountField = EmitAnyExprToTemp( + MemberExpr::CreateImplicit(getContext(), + const_cast(ME->getBase()), + ME->isArrow(), FD, FD->getType(), + VK_LValue, OK_Ordinary)).getScalarVal(); + return Builder.CreateMul( + CountField, llvm::ConstantInt::get(CountField->getType(), Size / 8)); + } + } + // LLVM can't handle Type=3 appropriately, and __builtin_object_size shouldn't // evaluate E for side-effects. In either case, we shouldn't lower to // @llvm.objectsize. diff --git a/clang/lib/CodeGen/CGExpr.cpp b/clang/lib/CodeGen/CGExpr.cpp --- a/clang/lib/CodeGen/CGExpr.cpp +++ b/clang/lib/CodeGen/CGExpr.cpp @@ -30,6 +30,7 @@ #include "clang/Basic/CodeGenOptions.h" #include "clang/Basic/SourceManager.h" #include "llvm/ADT/Hashing.h" +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringExtras.h" #include "llvm/IR/DataLayout.h" #include "llvm/IR/Intrinsics.h" @@ -929,10 +930,21 @@ const ArrayType *AT = IndexedType->castAsArrayTypeUnsafe(); if (const auto *CAT = dyn_cast(AT)) return CGF.Builder.getInt(CAT->getSize()); - else if (const auto *VAT = dyn_cast(AT)) + + if (const auto *VAT = dyn_cast(AT)) return CGF.getVLASize(VAT).NumElts; // Ignore pass_object_size here. It's not applicable on decayed pointers. } + + if (FieldDecl *FD = CGF.FindCountedByField(Base, StrictFlexArraysLevel)) { + const auto *ME = dyn_cast(CE->getSubExpr()); + IndexedType = Base->getType(); + return CGF.EmitAnyExprToTemp( + MemberExpr::CreateImplicit(CGF.getContext(), + const_cast(ME->getBase()), + ME->isArrow(), FD, FD->getType(), + VK_LValue, OK_Ordinary)).getScalarVal(); + } } QualType EltTy{Base->getType()->getPointeeOrArrayElementType(), 0}; @@ -944,13 +956,51 @@ return nullptr; } +FieldDecl * +CodeGenFunction::FindCountedByField(const Expr *Base, + LangOptions::StrictFlexArraysLevelKind + StrictFlexArraysLevel) { + const ValueDecl *VD = nullptr; + + Base = Base->IgnoreParenImpCasts(); + + if (const auto *ME = dyn_cast(Base)) { + VD = dyn_cast(ME->getMemberDecl()); + } else if (const auto *DRE = dyn_cast(Base)) { + // Pointing to the full structure. + VD = dyn_cast(DRE->getDecl()); + + QualType Ty = VD->getType()->getPointeeType(); + if (const auto *RD = Ty->getAsRecordDecl()) + for (const FieldDecl *Field : RD->fields()) + VD = Field; + } else if (const auto *CE = dyn_cast(Base)) { + if (const auto *ME = dyn_cast(CE->getSubExpr())) + VD = dyn_cast(ME->getMemberDecl()); + } + + const auto *FD = dyn_cast_if_present(VD); + if (!FD || !FD->isFlexibleArrayMemberLike(getContext(), + StrictFlexArraysLevel, true)) + return nullptr; + + const auto *CBA = FD->getAttr(); + if (!CBA) + return nullptr; + + StringRef FieldName = CBA->getCountedByField()->getName(); + auto It = llvm::find_if(FD->getParent()->fields(), + [&](const FieldDecl *Field) { + return FieldName == Field->getName(); + }); + return It != FD->getParent()->field_end() ? *It : nullptr; +} + void CodeGenFunction::EmitBoundsCheck(const Expr *E, const Expr *Base, llvm::Value *Index, QualType IndexType, bool Accessed) { assert(SanOpts.has(SanitizerKind::ArrayBounds) && "should not be called unless adding bounds checks"); - SanitizerScope SanScope(this); - const LangOptions::StrictFlexArraysLevelKind StrictFlexArraysLevel = getLangOpts().getStrictFlexArraysLevel(); @@ -960,6 +1010,8 @@ if (!Bound) return; + SanitizerScope SanScope(this); + bool IndexSigned = IndexType->isSignedIntegerOrEnumerationType(); llvm::Value *IndexVal = Builder.CreateIntCast(Index, SizeTy, IndexSigned); llvm::Value *BoundVal = Builder.CreateIntCast(Bound, SizeTy, false); diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h --- a/clang/lib/CodeGen/CodeGenFunction.h +++ b/clang/lib/CodeGen/CodeGenFunction.h @@ -3027,6 +3027,12 @@ void EmitBoundsCheck(const Expr *E, const Expr *Base, llvm::Value *Index, QualType IndexType, bool Accessed); + /// Find the FieldDecl specified in a FAM's "counted_by" attribute. Returns + /// \p nullptr if either the attribute or the field doesn't exist. + FieldDecl *FindCountedByField(const Expr *Base, + LangOptions::StrictFlexArraysLevelKind + StrictFlexArraysLevel); + llvm::Value *EmitScalarPrePostIncDec(const UnaryOperator *E, LValue LV, bool isInc, bool isPre); ComplexPairTy EmitComplexPrePostIncDec(const UnaryOperator *E, LValue LV, diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -17904,6 +17904,37 @@ "Broken injected-class-name"); } +static const FieldDecl *FindFieldWithCountedByAttr(const RecordDecl *RD) { + for (const Decl *D : RD->decls()) { + if (const auto *FD = dyn_cast(D)) + if (FD->hasAttr()) + return FD; + + if (const auto *SubRD = dyn_cast(D)) + if (const FieldDecl *FD = FindFieldWithCountedByAttr(SubRD)) + return FD; + } + + return nullptr; +} + +static const IdentifierInfo * +CheckCountedByAttr(const RecordDecl *RD, const FieldDecl *FD, + SourceRange &Loc) { + const CountedByAttr *ECA = FD->getAttr(); + + auto It = llvm::find_if( + RD->fields(), [&](const FieldDecl *Field){ + return Field->getName() == ECA->getCountedByField()->getName(); + }); + if (It == RD->field_end()) { + Loc = ECA->getCountedByFieldLoc(); + return ECA->getCountedByField(); + } + + return nullptr; +} + void Sema::ActOnTagFinishDefinition(Scope *S, Decl *TagD, SourceRange BraceRange) { AdjustDeclIfTemplate(TagD); @@ -17961,6 +17992,19 @@ [](const FieldDecl *FD) { return FD->isBitField(); })) Diag(BraceRange.getBegin(), diag::warn_pragma_align_not_xl_compatible); } + + // Check the "counted_by" attribute to ensure that the count field exists in + // the struct. + if (const RecordDecl *RD = dyn_cast(Tag)) { + if (const FieldDecl *FD = FindFieldWithCountedByAttr(RD)) { + SourceRange SR; + const IdentifierInfo *Unknown = CheckCountedByAttr(RD, FD, SR); + + if (Unknown) + Diag(SR.getBegin(), diag::warn_counted_by_placeholder) + << Unknown << SR; + } + } } void Sema::ActOnObjCContainerFinishDefinition() { diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -8380,6 +8380,21 @@ D->addAttr(ZeroCallUsedRegsAttr::Create(S.Context, Kind, AL)); } +static void handleCountedByAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + // TODO: Probably needs more processing here. See Sema::AddAlignValueAttr. + if (!AL.isArgIdent(0)) { + S.Diag(AL.getLoc(), diag::err_attribute_argument_type) + << AL << AANT_ArgumentIdentifier; + return; + } + + IdentifierLoc *IL = AL.getArgAsIdent(0); + CountedByAttr *CBA = ::new (S.Context) CountedByAttr(S.Context, AL, + IL->Ident); + CBA->setCountedByFieldLoc(IL->Loc); + D->addAttr(CBA); +} + static void handleFunctionReturnThunksAttr(Sema &S, Decl *D, const ParsedAttr &AL) { StringRef KindStr; @@ -9326,6 +9341,10 @@ handleAvailableOnlyInDefaultEvalMethod(S, D, AL); break; + case ParsedAttr::AT_CountedBy: + handleCountedByAttr(S, D, AL); + break; + // Microsoft attributes: case ParsedAttr::AT_LayoutVersion: handleLayoutVersion(S, D, AL); diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test --- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test +++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test @@ -56,6 +56,7 @@ // CHECK-NEXT: ConsumableAutoCast (SubjectMatchRule_record) // CHECK-NEXT: ConsumableSetOnRead (SubjectMatchRule_record) // CHECK-NEXT: Convergent (SubjectMatchRule_function) +// CHECK-NEXT: CountedBy (SubjectMatchRule_field) // CHECK-NEXT: DLLExport (SubjectMatchRule_function, SubjectMatchRule_variable, SubjectMatchRule_record, SubjectMatchRule_objc_interface) // CHECK-NEXT: DLLImport (SubjectMatchRule_function, SubjectMatchRule_variable, SubjectMatchRule_record, SubjectMatchRule_objc_interface) // CHECK-NEXT: Destructor (SubjectMatchRule_function)