Skip to content

Commit 132f491

Browse files
committed
Check attributes in @abi attr
This commit compares the attributes on the decl inside the `@abi` attribute to those in the decl it’s attached to, diagnosing ABI-incompatible differences. It also rejects many attributes that don’t need to be specified in the `@abi` attribute, such as ObjC-ness, access control, or ABI-neutral traits like `@discardableResult`, so developers know to remove them.
1 parent 32b8a11 commit 132f491

File tree

11 files changed

+1574
-6
lines changed

11 files changed

+1574
-6
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8376,6 +8376,27 @@ ERROR(attr_abi_incompatible_with_silgen_name,none,
83768376
"the same purpose",
83778377
(DescriptiveDeclKind))
83788378

8379+
ERROR(attr_abi_missing_attr,none,
8380+
"missing '%0' %select{attribute|modifier}1 in '@abi'",
8381+
(StringRef, bool))
8382+
ERROR(attr_abi_extra_attr,none,
8383+
"extra %select{|implicit }2'%0' %select{attribute|modifier}1 in '@abi'",
8384+
(StringRef, bool, /*isImplicit=*/bool))
8385+
ERROR(attr_abi_forbidden_attr,none,
8386+
"unused '%0' %select{attribute|modifier}1 in '@abi'",
8387+
(StringRef, bool))
8388+
REMARK(abi_attr_inferred_attribute,none,
8389+
"inferred '%0' in '@abi' to match %select{attribute|modifier}1 on API",
8390+
(StringRef, bool))
8391+
8392+
ERROR(attr_abi_mismatched_attr,none,
8393+
"'%0' %select{attribute|modifier}1 in '@abi' should match '%2'",
8394+
(StringRef, bool, StringRef))
8395+
NOTE(attr_abi_matching_attr_here,none,
8396+
"%select{should match|matches}0 %select{attribute|modifier}1 "
8397+
"%select{|implicitly added }2here",
8398+
(/*matches=*/bool, /*isModifier=*/bool, /*isImplicit=*/bool))
8399+
83798400
ERROR(attr_abi_mismatched_type,none,
83808401
"type %0 in '@abi' should match %1",
83818402
(Type, Type))

include/swift/Basic/LangOptions.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ namespace swift {
269269
/// Emit a remark on early exit in explicit interface build
270270
bool EnableSkipExplicitInterfaceModuleBuildRemarks = false;
271271

272+
/// Emit a remark when \c \@abi infers an attribute or modifier.
273+
bool EnableABIInferenceRemarks = false;
274+
272275
///
273276
/// Support for alternate usage modes
274277
///

include/swift/Option/Options.td

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,10 @@ def remark_module_serialization : Flag<["-"], "Rmodule-serialization">,
468468
Flags<[FrontendOption, DoesNotAffectIncrementalBuild]>,
469469
HelpText<"Emit remarks about module serialization">;
470470

471+
def remark_abi_inference : Flag<["-"], "Rabi-inference">,
472+
Flags<[FrontendOption, DoesNotAffectIncrementalBuild]>,
473+
HelpText<"Emit a remark when an '@abi' attribute adds an attribute or modifier to the ABI declaration based on its presence in the API">;
474+
471475
def emit_tbd : Flag<["-"], "emit-tbd">,
472476
HelpText<"Emit a TBD file">,
473477
Flags<[FrontendOption, NoInteractiveOption, SupplementaryOutput]>;

lib/Frontend/CompilerInvocation.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,8 @@ static bool ParseLangArgs(LangOptions &Opts, ArgList &Args,
14221422

14231423
Opts.EnableSkipExplicitInterfaceModuleBuildRemarks = Args.hasArg(OPT_remark_skip_explicit_interface_build);
14241424

1425+
Opts.EnableABIInferenceRemarks = Args.hasArg(OPT_remark_abi_inference);
1426+
14251427
if (Args.hasArg(OPT_experimental_skip_non_exportable_decls)) {
14261428
// Only allow -experimental-skip-non-exportable-decls if either library
14271429
// evolution is enabled (in which case the module's ABI is independent of

lib/Sema/TypeCheckAttr.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6672,6 +6672,12 @@ static bool typeCheckDerivativeAttr(DerivativeAttr *attr) {
66726672
// Note: Implementation must be idempotent because it may be called multiple
66736673
// times for the same attribute.
66746674
Decl *D = attr->getOriginalDeclaration();
6675+
6676+
// ABI-only decls can't have @derivative; bail out and let ABIDeclChecker
6677+
// diagnose this.
6678+
if (!ABIRoleInfo(D).providesAPI())
6679+
return false;
6680+
66756681
auto &Ctx = D->getASTContext();
66766682
auto &diags = Ctx.Diags;
66776683
// `@derivative` attribute requires experimental differentiable programming

lib/Sema/TypeCheckAttrABI.cpp

Lines changed: 215 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "swift/AST/Effects.h"
2121
#include "swift/AST/DiagnosticsSema.h"
2222
#include "swift/AST/ParameterList.h"
23+
#include "swift/AST/ASTPrinter.h"
2324
#include "swift/AST/Types.h"
2425
#include "swift/Basic/Assertions.h"
2526
#include "swift/Parse/Lexer.h"
@@ -129,6 +130,40 @@ void fixItReplaceKeywords(InFlightDiagnostic &diag,
129130
return fixItReplaceKeywords(diag, charRange, newText);
130131
}
131132

133+
/// Returns a string representation of \p attr suitable to either replace an
134+
/// existing attribute or be inserted as a new attribute, depending on the
135+
/// value of \p toInsert .
136+
StringRef printAttr(DeclAttribute *attr,
137+
Decl *decl,
138+
SmallVectorImpl<char> &scratch,
139+
bool toInsert = false) {
140+
auto &ctx = decl->getASTContext();
141+
auto opts = PrintOptions::printForDiagnostics(AccessLevel::Private,
142+
ctx.TypeCheckerOpts.PrintFullConvention);
143+
opts.PrintLongAttrsOnSeparateLines = false;
144+
145+
llvm::raw_svector_ostream os{scratch};
146+
StreamPrinter printer{os};
147+
attr->print(printer, opts, decl);
148+
149+
auto str = StringRef(scratch.begin(), scratch.size());
150+
if (!toInsert)
151+
str = str.trim(' ');
152+
return str;
153+
}
154+
155+
/// Emit \c diag::attr_abi_matching_attr_here in the best available location.
156+
void noteAttrHere(DeclAttribute *attr, Decl *decl, bool isMatch = false) {
157+
auto &ctx = decl->getASTContext();
158+
SourceLoc loc = attr->getLocation();
159+
if (loc.isValid())
160+
ctx.Diags.diagnose(loc, diag::attr_abi_matching_attr_here,
161+
isMatch, attr->isDeclModifier(), attr->isImplicit());
162+
else
163+
ctx.Diags.diagnose(decl, diag::attr_abi_matching_attr_here,
164+
isMatch, attr->isDeclModifier(), attr->isImplicit());
165+
}
166+
132167
/// Get the best available \c SourceLoc representing the type in \p storage .
133168
SourceLoc getTypeLoc(AbstractStorageDecl *storage, Decl *owner = nullptr) {
134169
auto loc = storage->getTypeSourceRangeForDiagnostics().Start;
@@ -187,7 +222,11 @@ class ABIDeclChecker : public ASTComparisonVisitor<ABIDeclChecker> {
187222
bool didDiagnose = false;
188223

189224
auto noteShouldMatch = [&](bool isModifier) {
190-
ctx.Diags.diagnose(apiTypeLoc, diag::attr_abi_should_match_type_here);
225+
if (isSelfParam)
226+
ctx.Diags.diagnose(apiTypeLoc, diag::attr_abi_matching_attr_here,
227+
/*matches=*/false, isModifier, /*isImplicit=*/false);
228+
else
229+
ctx.Diags.diagnose(apiTypeLoc, diag::attr_abi_should_match_type_here);
191230
};
192231

193232
// These assertions represent values that should have been normalized.
@@ -646,13 +685,187 @@ class ABIDeclChecker : public ASTComparisonVisitor<ABIDeclChecker> {
646685

647686
// MARK: @abi checking - attributes
648687

688+
/// Are these attributes similar enough that they should be checked against
689+
/// one another? At minimum this means they're of the same kind, but for some
690+
/// attrs there are additional criteria.
691+
bool canCompareAttrs(DeclAttribute *api, DeclAttribute *abi,
692+
Decl *apiDecl, Decl *abiDecl) {
693+
if (api->getKind() != abi->getKind())
694+
return false;
695+
696+
auto getAvailableDomain = [](Decl *D, DeclAttribute *A) {
697+
return D->getSemanticAvailableAttr(cast<AvailableAttr>(A))->getDomain();
698+
};
699+
700+
// Extra logic for specific attributes.
701+
switch (api->getKind()) {
702+
case DeclAttrKind::Expose:
703+
return cast<ExposeAttr>(api)->getExposureKind()
704+
== cast<ExposeAttr>(abi)->getExposureKind();
705+
706+
case DeclAttrKind::Extern:
707+
return cast<ExternAttr>(api)->getExternKind()
708+
== cast<ExternAttr>(abi)->getExternKind();
709+
710+
case DeclAttrKind::Available:
711+
return getAvailableDomain(apiDecl, api)
712+
== getAvailableDomain(abiDecl, abi);
713+
return true;
714+
715+
default:
716+
break;
717+
}
718+
719+
return true;
720+
}
721+
722+
/// Check two attribute lists against one another.
723+
///
724+
/// This pairs up attributes which are sufficiently similar (as determined by
725+
/// \c canCompareAttrs() ) and then checks them. Attributes which
726+
/// have no counterpart are checked individually.
649727
bool checkAttrs(DeclAttributes api, DeclAttributes abi,
650728
Decl *apiDecl, Decl *abiDecl) {
651729
bool didDiagnose = false;
652-
// TODO: Actually check attributes
730+
731+
// Collect all ABI attrs.
732+
SmallVector<DeclAttribute*, 32> remainingABIDeclAttrs;
733+
for (auto *abiDeclAttr : abi) {
734+
remainingABIDeclAttrs.push_back(abiDeclAttr);
735+
}
736+
737+
// Visit each API attr, pairing it with an ABI attr if possible.
738+
// Note that this will visit even invalid attributes.
739+
for (auto *apiDeclAttr : api) {
740+
auto abiAttrIter = llvm::find_if(remainingABIDeclAttrs,
741+
[&](DeclAttribute *abiDeclAttr) {
742+
return abiDeclAttr && canCompareAttrs(apiDeclAttr, abiDeclAttr,
743+
apiDecl, abiDecl);
744+
});
745+
DeclAttribute *abiDeclAttr = nullptr;
746+
if (abiAttrIter != remainingABIDeclAttrs.end()) {
747+
// Found a matching ABI attr. Claim and use it.
748+
std::swap(abiDeclAttr, *abiAttrIter);
749+
}
750+
didDiagnose |= checkAttr(apiDeclAttr, abiDeclAttr, apiDecl, abiDecl);
751+
}
752+
753+
// Visit leftover ABI attrs.
754+
for (auto *abiDeclAttr : remainingABIDeclAttrs) {
755+
if (abiDeclAttr)
756+
didDiagnose |= checkAttr(nullptr, abiDeclAttr, apiDecl, abiDecl);
757+
}
653758
return didDiagnose;
654759
}
655760

761+
/// Check a single attribute against its counterpart. If an attribute has no
762+
/// counterpart, the counterpart may be \c nullptr ; either \p abi or \p abi
763+
/// may be \c nullptr , but never both.
764+
bool checkAttr(DeclAttribute *api, DeclAttribute *abi,
765+
Decl *apiDecl, Decl *abiDecl) {
766+
ASSERT(api || abi && "checkAttr() should have at least one attribute");
767+
768+
// If either attribute has already been diagnosed, don't check here.
769+
if ((api && api->isInvalid()) || (abi && abi->isInvalid()))
770+
return true;
771+
772+
auto kind = api ? api->getKind() : abi->getKind();
773+
auto behaviors = DeclAttribute::getBehaviors(kind);
774+
775+
switch (behaviors & DeclAttribute::InABIAttrMask) {
776+
case DeclAttribute::UnreachableInABIAttr:
777+
ASSERT(abiAttr->canAppearOnDecl(apiDecl)
778+
&& "checking @abi on decl that can't have it???");
779+
ASSERT(!abiAttr->canAppearOnDecl(apiDecl)
780+
&& "unreachable-in-@abi attr on reachable decl???");
781+
782+
// If the asserts are disabled, fall through to no checking.
783+
LLVM_FALLTHROUGH;
784+
785+
case DeclAttribute::UnconstrainedInABIAttr:
786+
// No checking required.
787+
return false;
788+
789+
case DeclAttribute::ForbiddenInABIAttr:
790+
// Diagnose if ABI has attribute.
791+
if (abi) {
792+
// A shorthand `@available(foo 1, bar 2, *)` attribute gets parsed into
793+
// several separate `AvailableAttr`s, each with the full range of the
794+
// shorthand attribute. If we've already diagnosed one of them, don't
795+
// diagnose the rest; otherwise, record that we've diagnosed this one.
796+
if (isa<AvailableAttr>(abi) &&
797+
!diagnosedAvailableAttrSourceLocs.insert(abi->getLocation()))
798+
return true;
799+
800+
diagnoseAndRemoveAttr(abi, diag::attr_abi_forbidden_attr,
801+
abi->getAttrName(), abi->isDeclModifier());
802+
return true;
803+
}
804+
805+
return false;
806+
807+
case DeclAttribute::InferredInABIAttr:
808+
if (!abi && api->canClone()) {
809+
// Infer an identical attribute.
810+
abi = api->clone(ctx);
811+
abi->setImplicit(true);
812+
abiDecl->getAttrs().add(abi);
813+
814+
if (ctx.LangOpts.EnableABIInferenceRemarks) {
815+
SmallString<64> scratch;
816+
auto abiAttrAsString = printAttr(abi, abiDecl, scratch);
817+
818+
abiDecl->diagnose(diag::abi_attr_inferred_attribute,
819+
abiAttrAsString, api->isDeclModifier());
820+
noteAttrHere(api, apiDecl, /*isMatch=*/true);
821+
}
822+
}
823+
824+
// Other than the cloning behavior, Inferred behaves like Equivalent.
825+
LLVM_FALLTHROUGH;
826+
827+
case DeclAttribute::EquivalentInABIAttr:
828+
// Diagnose if API doesn't have attribute.
829+
if (!api) {
830+
diagnoseAndRemoveAttr(abi, diag::attr_abi_extra_attr,
831+
abi->getAttrName(), abi->isDeclModifier(),
832+
abi->isImplicit());
833+
return true;
834+
}
835+
836+
// Diagnose if ABI doesn't have attribute.
837+
if (!abi) {
838+
SmallString<64> scratch;
839+
auto apiAttrAsString = printAttr(api, apiDecl, scratch,
840+
/*toInsert=*/true);
841+
842+
ctx.Diags.diagnose(abiDecl, diag::attr_abi_missing_attr,
843+
api->getAttrName(), api->isDeclModifier())
844+
.fixItInsert(abiDecl->getAttributeInsertionLoc(api->isDeclModifier()),
845+
apiAttrAsString);
846+
noteAttrHere(api, apiDecl);
847+
return true;
848+
}
849+
850+
// Diagnose if two attributes are mismatched.
851+
if (!api->isEquivalent(abi, apiDecl)) {
852+
SmallString<64> scratch;
853+
auto apiAttrAsString = printAttr(api, apiDecl, scratch);
854+
855+
ctx.Diags.diagnose(abi->getLocation(), diag::attr_abi_mismatched_attr,
856+
abi->getAttrName(), abi->isDeclModifier(),
857+
apiAttrAsString)
858+
.fixItReplace(abi->getRangeWithAt(), apiAttrAsString);
859+
noteAttrHere(api, apiDecl);
860+
return true;
861+
}
862+
863+
return false;
864+
}
865+
866+
llvm_unreachable("unknown InABIAttrMask behavior");
867+
}
868+
656869
// MARK: @abi checking - types
657870

658871
bool checkType(Type api, Type abi, SourceLoc apiLoc, SourceLoc abiLoc) {

lib/Sema/TypeCheckDeclPrimary.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,6 +2173,10 @@ void swift::diagnoseAttrsAddedByAccessNote(SourceFile &SF) {
21732173

21742174
evaluator::SideEffect
21752175
ApplyAccessNoteRequest::evaluate(Evaluator &evaluator, ValueDecl *VD) const {
2176+
// Access notes don't apply to ABI-only attributes.
2177+
if (!ABIRoleInfo(VD).providesAPI())
2178+
return {};
2179+
21762180
AccessNotesFile &notes = VD->getModuleContext()->getAccessNotes();
21772181
if (auto note = notes.lookup(VD))
21782182
applyAccessNote(VD, *note.get(), notes);

test/attr/Inputs/attr_abi.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
void implementation1(void);
2+
void implementation2(void);
3+
void implementation3(void);

0 commit comments

Comments
 (0)