From 596da3121d5fd7d629b9ddfca4d71a81e2ffd1bb 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 d1fad97e01e8b..a593772e306cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,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][]: @@ -9833,6 +9845,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 98f14bc53600a..3d7fe0b9ba918 100644 --- a/include/swift/AST/Decl.h +++ b/include/swift/AST/Decl.h @@ -8701,6 +8701,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 80b02fa9a6e15..832a2acba2070 100644 --- a/include/swift/AST/TypeCheckRequests.h +++ b/include/swift/AST/TypeCheckRequests.h @@ -3338,10 +3338,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 4f4226dfe2f23..9041db3650211 100644 --- a/include/swift/AST/TypeCheckerTypeIDZone.def +++ b/include/swift/AST/TypeCheckerTypeIDZone.def @@ -363,7 +363,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 baa78d15a3345..f462542918be9 100644 --- a/lib/AST/Attr.cpp +++ b/lib/AST/Attr.cpp @@ -1393,7 +1393,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 ca528cdb784b8..971ba5557eef7 100644 --- a/lib/AST/ConformanceLookupTable.cpp +++ b/lib/AST/ConformanceLookupTable.cpp @@ -506,7 +506,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 79a854ef22f40..2aaca689c8525 100644 --- a/lib/AST/Decl.cpp +++ b/lib/AST/Decl.cpp @@ -10886,15 +10886,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 a3043d8595d8a..c49cdd347458b 100644 --- a/lib/Sema/TypeCheckAttr.cpp +++ b/lib/Sema/TypeCheckAttr.cpp @@ -6948,7 +6948,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 ec6c390a0f05f..5313ab221e60e 100644 --- a/lib/Sema/TypeCheckMacros.cpp +++ b/lib/Sema/TypeCheckMacros.cpp @@ -1548,12 +1548,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; @@ -1625,22 +1669,9 @@ swift::expandExtensions(CustomAttr *attr, MacroDecl *macro, 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); @@ -1830,9 +1861,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 a7f6252d450ea..1af18b331bd0c 100644 --- a/test/Macros/Inputs/syntax_macro_definitions.swift +++ b/test/Macros/Inputs/syntax_macro_definitions.swift @@ -2004,3 +2004,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 { }