From 61a975c6777efa1abe81d16d1e477aec6c29f8e3 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 4 Aug 2023 16:36:34 -0700 Subject: [PATCH] [SE-0407] Provide member macros with information about "missing" conformances Provide member macros with similar information about conformances to what extension macros receive, allowing member macros to document which conformances they care about (e.g., Decodable) and then receiving the list of conformances that aren't already available for the type in question. For example, a macro such as @attached(member, conformances: Decodable, Encodable, names: named(init(from:), encode(to:))) macro Codable() = ... Expanded on a type that is not already Decodable/Encodable would be provided with Decodable and Encodable (via the new `missingConformancesTo:` argument to the macro implementation) when the type itself does not conform to those types. Member macros still cannot produce conformances, so this is likely to be used in conjunction with extension macros most of the time. The extension macro declares the conformance, and can also declare any members that shouldn't be part of the primary type definition---such as initializers that shouldn't suppress the memberwise initializer. On the other hand, the member macro will need to define any members that must be in the primary definition, such as required initializers, members that must be overridable by subclasses, and stored properties. Codable synthesis is an example that benefits from member macros with conformances, because for classes it wants to introduce a required initializer for decoding and an overridable encode operation, and these must be members of the nominal type itself. Specifically, the `Codable` macro above is likely to have two attached member roles: @attached(member, conformances: Decodable, Encodable, names: named(init(from:), encode(to:))) @attached(extension, conformances: Decodable, Encodable, names: named(init(from:), encode(to:))) macro Codable() = ... where the "extension" role is responsible for defining the conformance (always), and the "member" creates the appropriate members for classes (`init` vs. `required init`). Tracked by rdar://112532829. --- CHANGELOG.md | 13 ++++ include/swift/AST/Decl.h | 1 + include/swift/AST/TypeCheckRequests.h | 8 +-- include/swift/AST/TypeCheckerTypeIDZone.def | 2 +- lib/AST/Attr.cpp | 2 +- lib/AST/ConformanceLookupTable.cpp | 3 +- lib/AST/Decl.cpp | 5 +- lib/Sema/TypeCheckAttr.cpp | 2 +- lib/Sema/TypeCheckMacros.cpp | 69 ++++++++++++++----- .../Inputs/syntax_macro_definitions.swift | 50 ++++++++++++++ ...acro_expand_member_with_conformances.swift | 22 ++++++ 11 files changed, 148 insertions(+), 29 deletions(-) create mode 100644 test/Macros/macro_expand_member_with_conformances.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index d407b27563c68..2c1fc06f6783f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ _**Note:** This is in reverse chronological order, so newer entries are added to the top._ +## Swift 5.9.2 + +* [SE-0407][]: + + Member macros can specify a list of protocols via the `conformances` argument to the macro role. The macro implementation will be provided with those protocols that are listed but have not already been implemented by the type to which the member macro is attached, in the same manner as extension macros. + + ```swift + @attached(member, conformances: Decodable, Encodable, names: named(init(from:), encode(to:))) +@attached(extension, conformances: Decodable, Encodable, names: named(init(from:), encode(to:))) +macro Codable() = #externalMacro(module: "MyMacros", type: "CodableMacro") + ``` + ## Swift 5.9 * [SE-0382][], [SE-0389][], [SE-0394][], [SE-0397][]: @@ -9830,6 +9842,7 @@ using the `.dynamicType` member to retrieve the type of an expression should mig [SE-0389]: https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md [SE-0394]: https://github.com/apple/swift-evolution/blob/main/proposals/0394-swiftpm-expression-macros.md [SE-0397]: https://github.com/apple/swift-evolution/blob/main/proposals/0397-freestanding-declaration-macros.md +[SE-0407]: https://github.com/apple/swift-evolution/blob/main/proposals/0407-member-macro-conformances.md [#64927]: [#42697]: [#42728]: diff --git a/include/swift/AST/Decl.h b/include/swift/AST/Decl.h index 38b8a45cd9ae7..2665a203ad81d 100644 --- a/include/swift/AST/Decl.h +++ b/include/swift/AST/Decl.h @@ -8621,6 +8621,7 @@ class MacroDecl : public GenericContext, public ValueDecl { /// be added if this macro does not contain an extension role. void getIntroducedConformances( NominalTypeDecl *attachedTo, + MacroRole role, SmallVectorImpl &conformances) const; /// Returns a DeclName that represents arbitrary names. diff --git a/include/swift/AST/TypeCheckRequests.h b/include/swift/AST/TypeCheckRequests.h index 00695f1b496de..740956b3d39c7 100644 --- a/include/swift/AST/TypeCheckRequests.h +++ b/include/swift/AST/TypeCheckRequests.h @@ -3337,10 +3337,10 @@ class ResolveMacroRequest void noteCycleStep(DiagnosticEngine &diags) const; }; -/// Returns the resolved constraint types that an extension macro -/// adds conformances to. -class ResolveExtensionMacroConformances - : public SimpleRequest(const MacroRoleAttr *, const Decl *), RequestFlags::Cached> { public: diff --git a/include/swift/AST/TypeCheckerTypeIDZone.def b/include/swift/AST/TypeCheckerTypeIDZone.def index ea024e9f1d2d4..a6e7c755bc71f 100644 --- a/include/swift/AST/TypeCheckerTypeIDZone.def +++ b/include/swift/AST/TypeCheckerTypeIDZone.def @@ -360,7 +360,7 @@ SWIFT_REQUEST(TypeChecker, ResolveImplicitMemberRequest, SWIFT_REQUEST(TypeChecker, ResolveMacroRequest, ConcreteDeclRef(UnresolvedMacroReference, const Decl *), Cached, NoLocationInfo) -SWIFT_REQUEST(TypeChecker, ResolveExtensionMacroConformances, +SWIFT_REQUEST(TypeChecker, ResolveMacroConformances, ArrayRef(const MacroRoleAttr *, const Decl *), Cached, NoLocationInfo) SWIFT_REQUEST(TypeChecker, ResolveTypeEraserTypeRequest, diff --git a/lib/AST/Attr.cpp b/lib/AST/Attr.cpp index 2111ad11d7090..7a85ccb343103 100644 --- a/lib/AST/Attr.cpp +++ b/lib/AST/Attr.cpp @@ -1387,7 +1387,7 @@ bool DeclAttribute::printImpl(ASTPrinter &Printer, const PrintOptions &Options, // Print conformances, if present. auto conformances = evaluateOrDefault( D->getASTContext().evaluator, - ResolveExtensionMacroConformances{Attr, D}, + ResolveMacroConformances{Attr, D}, {}); if (!conformances.empty()) { Printer << ", conformances: "; diff --git a/lib/AST/ConformanceLookupTable.cpp b/lib/AST/ConformanceLookupTable.cpp index 85d2b523c1e34..74fb5d2c6df5b 100644 --- a/lib/AST/ConformanceLookupTable.cpp +++ b/lib/AST/ConformanceLookupTable.cpp @@ -503,7 +503,8 @@ void ConformanceLookupTable::addMacroGeneratedProtocols( MacroRole::Extension, [&](CustomAttr *attr, MacroDecl *macro) { SmallVector conformances; - macro->getIntroducedConformances(nominal, conformances); + macro->getIntroducedConformances( + nominal, MacroRole::Extension, conformances); for (auto *protocol : conformances) { addProtocol(protocol, attr->getLocation(), source); diff --git a/lib/AST/Decl.cpp b/lib/AST/Decl.cpp index 7b0ca4f344028..6388f154b2893 100644 --- a/lib/AST/Decl.cpp +++ b/lib/AST/Decl.cpp @@ -10826,15 +10826,16 @@ void MacroDecl::getIntroducedNames(MacroRole role, ValueDecl *attachedTo, void MacroDecl::getIntroducedConformances( NominalTypeDecl *attachedTo, + MacroRole role, SmallVectorImpl &conformances) const { - auto *attr = getMacroRoleAttr(MacroRole::Extension); + auto *attr = getMacroRoleAttr(role); if (!attr) return; auto &ctx = getASTContext(); auto constraintTypes = evaluateOrDefault( ctx.evaluator, - ResolveExtensionMacroConformances{attr, this}, + ResolveMacroConformances{attr, this}, {}); for (auto constraint : constraintTypes) { diff --git a/lib/Sema/TypeCheckAttr.cpp b/lib/Sema/TypeCheckAttr.cpp index 4620488ff6599..c81fddb1f4c12 100644 --- a/lib/Sema/TypeCheckAttr.cpp +++ b/lib/Sema/TypeCheckAttr.cpp @@ -7151,7 +7151,7 @@ void AttributeChecker::visitMacroRoleAttr(MacroRoleAttr *attr) { (void)evaluateOrDefault( Ctx.evaluator, - ResolveExtensionMacroConformances{attr, D}, + ResolveMacroConformances{attr, D}, {}); } diff --git a/lib/Sema/TypeCheckMacros.cpp b/lib/Sema/TypeCheckMacros.cpp index 07eeebbc10baf..408c9c0b07ebf 100644 --- a/lib/Sema/TypeCheckMacros.cpp +++ b/lib/Sema/TypeCheckMacros.cpp @@ -1533,12 +1533,56 @@ swift::expandAttributes(CustomAttr *attr, MacroDecl *macro, Decl *member) { return macroSourceFile->getBufferID(); } +// Collect the protocol conformances that the macro asked about but were +// not already present on the declaration. +static TinyPtrVector getIntroducedConformances( + NominalTypeDecl *nominal, MacroRole role, MacroDecl *macro, + SmallVectorImpl *potentialConformances = nullptr) { + SmallVector potentialConformancesBuffer; + if (!potentialConformances) + potentialConformances = &potentialConformancesBuffer; + macro->getIntroducedConformances(nominal, role, *potentialConformances); + + TinyPtrVector introducedConformances; + for (auto protocol : *potentialConformances) { + SmallVector existingConformances; + nominal->lookupConformance(protocol, existingConformances); + + bool hasExistingConformance = llvm::any_of( + existingConformances, + [&](ProtocolConformance *conformance) { + return conformance->getSourceKind() != + ConformanceEntryKind::PreMacroExpansion; + }); + + if (!hasExistingConformance) { + introducedConformances.push_back(protocol); + } + } + + return introducedConformances; +} + llvm::Optional swift::expandMembers(CustomAttr *attr, MacroDecl *macro, Decl *decl) { + auto nominal = dyn_cast(decl); + if (!nominal) { + auto ext = dyn_cast(decl); + if (!ext) + return llvm::None; + + nominal = ext->getExtendedNominal(); + if (!nominal) + return llvm::None; + } + auto introducedConformances = getIntroducedConformances( + nominal, MacroRole::Member, macro); + // Evaluate the macro. auto macroSourceFile = ::evaluateAttachedMacro(macro, decl, attr, - /*passParentContext=*/false, MacroRole::Member); + /*passParentContext=*/false, MacroRole::Member, + introducedConformances); if (!macroSourceFile) return llvm::None; @@ -1611,22 +1655,9 @@ llvm::Optional swift::expandExtensions(CustomAttr *attr, return llvm::None; } - // Collect the protocol conformances that the macro can add. The - // macro should not add conformances that are already stated in - // the original source. - SmallVector potentialConformances; - macro->getIntroducedConformances(nominal, potentialConformances); - - SmallVector introducedConformances; - for (auto protocol : potentialConformances) { - SmallVector existingConformances; - nominal->lookupConformance(protocol, existingConformances); - if (existingConformances.empty()) { - introducedConformances.push_back(protocol); - } - } - + auto introducedConformances = getIntroducedConformances( + nominal, MacroRole::Extension, macro, &potentialConformances); auto macroSourceFile = ::evaluateAttachedMacro(macro, nominal, attr, /*passParentContext=*/false, role, introducedConformances); @@ -1815,9 +1846,9 @@ ConcreteDeclRef ResolveMacroRequest::evaluate(Evaluator &evaluator, } ArrayRef -ResolveExtensionMacroConformances::evaluate(Evaluator &evaluator, - const MacroRoleAttr *attr, - const Decl *decl) const { +ResolveMacroConformances::evaluate(Evaluator &evaluator, + const MacroRoleAttr *attr, + const Decl *decl) const { auto *dc = decl->getDeclContext(); auto &ctx = dc->getASTContext(); diff --git a/test/Macros/Inputs/syntax_macro_definitions.swift b/test/Macros/Inputs/syntax_macro_definitions.swift index 9fb6bd553669c..c47f37923eeb8 100644 --- a/test/Macros/Inputs/syntax_macro_definitions.swift +++ b/test/Macros/Inputs/syntax_macro_definitions.swift @@ -2018,3 +2018,53 @@ public struct InitWithProjectedValueWrapperMacro: PeerMacro { ] } } + +public struct RequiredDefaultInitMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo decl: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + if protocols.isEmpty { + return [] + } + + let decl: DeclSyntax = + """ + extension \(type.trimmed): DefaultInit { + } + + """ + + return [ + decl.cast(ExtensionDeclSyntax.self) + ] + } +} + +extension RequiredDefaultInitMacro: MemberMacro { + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + fatalError("old swift-syntax") + } + + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + let decl: DeclSyntax + if declaration.is(ClassDeclSyntax.self) && protocols.isEmpty { + decl = "required init() { }" + } else { + decl = "init() { }" + } + return [ decl ] + } +} diff --git a/test/Macros/macro_expand_member_with_conformances.swift b/test/Macros/macro_expand_member_with_conformances.swift new file mode 100644 index 0000000000000..a02e834464e79 --- /dev/null +++ b/test/Macros/macro_expand_member_with_conformances.swift @@ -0,0 +1,22 @@ +// REQUIRES: swift_swift_parser + +// RUN: %empty-directory(%t) +// RUN: %host-build-swift -swift-version 5 -emit-library -o %t/%target-library-name(MacroDefinition) -module-name=MacroDefinition %S/Inputs/syntax_macro_definitions.swift -g -no-toolchain-stdlib-rpath + +// RUN: %target-typecheck-verify-swift -enable-experimental-feature ExtensionMacros -swift-version 5 -load-plugin-library %t/%target-library-name(MacroDefinition) -module-name MacroUser -DTEST_DIAGNOSTICS -swift-version 5 -I %t +protocol DefaultInit { + init() +} + +@attached(extension, conformances: DefaultInit) +@attached(member, conformances: DefaultInit, names: named(init())) +macro DefaultInit() = #externalMacro(module: "MacroDefinition", type: "RequiredDefaultInitMacro") + +@DefaultInit +class C { } + +@DefaultInit +class D: C { } + +@DefaultInit +struct E { }