Multiple protocol conformances in extension macro

I'm trying to implement an extension macro that is capable of declaring conformances to two different protocols. SE-0402 states:

  • [The] protocols are specified in the conformances: list of the @attached(conformances:)

But I couldn't figure out how to do this for more than one protocol.

In code:

protocol A { }
protocol B { }

@attached(extension, conformances: A) // How do I add B as well?
public macro MyMacro() = #externalMacro(...)

I've tried [A, B], (A, B), A, B, and even typealias C = A & B, but of course none worked. Help!

You could do this through a protocol C that conforms to A and B:

protocol C: A, B {}

@attached(extension, conformances: C)
public macro MyMacro() = #externalMacro(...)

You just add B like you did on your third attempt: conformances: A, B

Here's an example with Equatable and Hashable (which is a redundant conformance but not the point):

@attached(extension, conformances: Equatable, Hashable)
public macro equatable() = #externalMacro(...)

public struct EquatableMacro: ExtensionMacro {
    public static func expansion(...) throws -> [ExtensionDeclSyntax] {
        return [try ExtensionDeclSyntax("extension \(type.trimmed): Equatable, Hashable {}")]
    }
}

// usage

@equatable
struct MyStruct { }

// expands

extension MyStruct: Equatable, Hashable {}
1 Like

Thank you @Pippin! That works indeed.

It seems my problem is caused by something else. In my project, protocol B inherits from protocol A:

public protocol A { }
public protocol B : A { }

And my experiments shows:

  1. I can do this:
/// adds `extension MyStruct: B { }`
@attached(extension, conformances: B)
public macro MacroB() = #externalMacro(..., type: "Conform_B")
  1. a. But I can't do this, where Xcode would tell me "Type 'MyStruct' does not conform to protocol 'A'":
/// adds `extension MyStruct: B { }`
@attached(extension, conformances: A, B)
public macro MacroB() = #externalMacro(..., type: "Conform_B")
  1. b. (I can still do this though):
/// adds `extension MyStruct: A { }`
@attached(extension, conformances: A, B)
public macro MacroA() = #externalMacro(..., type: "Conform_A")
  1. To keep conformances: A, B, I have to use this:
/// adds `extension MyStruct: A, B { }`
@attached(extension, conformances: A, B)
public macro MacroAB() = #externalMacro(..., type: "Conform_A_B")

Is this behavior documented/mentioned somewhere?

Thank you for the reply! Unfortunately I don't always want to conform to both the protocols, so I couldn't use this method.

Maybe I actually did use a relevant example. Since B inherits A, you only need to add that it conforms to B. For example, I could have removed the Equatable conformance and just used Hashable, which inherits from Equatable.

Then MyStruct isn't properly conforming to A, which it got from conforming to B. Are you sure the names are added correctly to the macro interface? I'm frankly unsure that about options 2 and 3, they seem to me like caching issues from the previous attempts. At this point, I don't know if this is a macro issue.


The following works, but note that the protocol requirements for variables must not have a setter because variables are declared in an extension. I haven't read the threads that went over the evolution of these specific macros, but that seems like something that should be looked at (somehow allowing conformances for protocols with stored properties).

public protocol A {
    var foo: Int { get }
}
public protocol B: A { }

@attached(extension, conformances: B, names: named(foo))
public macro Conforming() = ...

public struct ConformingMacro: ExtensionMacro {
    public static func expansion(...) throws -> [ExtensionDeclSyntax] {
        // provide the requirements for A
        return [try ExtensionDeclSyntax("extension \(type.trimmed): B { var foo: Int { 420 } }")]
    }
}

// usage

@Conforming
struct MyStruct { }

// expands

extension MyStruct: B {
    var foo: Int {
        420
    }
}

Edit: Actually you can get away with a get/set for a protocol requirement with the good ol' private variable for the storage, been a while. While this may not have been your intention, just something for me to work on while making a presentation on macros.

Example P2
public protocol A {
    var foo: Int { get set }
}
public protocol B: A { }

@attached(extension, conformances: B, names: named(foo))
@attached(member, names: named(_foo))
public macro equatable() = ...

public struct EquatableMacro: ExtensionMacro, MemberMacro {
    
    public static func expansion(...) throws -> [DeclSyntax] {
        return [DeclSyntax("private var _foo: Int = 420")]
    }
    
    public static func expansion(...) throws -> [ExtensionDeclSyntax] {
        return [try ExtensionDeclSyntax("extension \(type.trimmed): B { var foo: Int { get { _foo } set { _foo = newValue } } }")]
    }
}

Haha indeed, I realized that right after I posted my reply.

But unfortunately, I don't always want to conform to protocol B. The macro decides whether to conform to B or just A according to the declaration it's added to. Therefore, I still need the conformances: A, B.

So, going back to my last post,

I tested the code using a fresh macro template, and A and B are actual protocol as declared in the project, so they have no requirement at all. I also tested your code (P2), and indeed it works. But it breaks as soon as I change

@attached(extension, conformances: B, names: named(foo))

to

@attached(extension, conformances: A, B, names: named(foo))

Could you please test this in you environment and see if it behaves the same way?

That actually is interesting. It does break when I add A, however that's only when I expand to B in the macro. When expanding to extension Bar: A, B, it works.

I am unsure if this is a bug or intended behavior. All named() declarations don't need to be in the expansion (I don't think it's checked) but to me it seems safer that the protocol conformances as declared must be in the expansion. I didn't read any language in the evolutions that said that this was required or missed it. However I am not that knowledgeable of macro implementation to know that much.

So at least right now, I don't think the macro system can do what you want. Personally, I would just have two macros that have separate A and B conformances which are used together.

Thanks for checking! That's exactly the same behavior I'm observing.

For the time being, I just make the macro add extension MyStruct: A, B { } every time I need something to conform to B. Having two separate macros would in fact be a more reasonable choice, but I'm sticking with a single macro for now in order to introduce as few names as possible from my package.

I believe this could be a bug, since it's okay to have a macro that expands to extension MyStruct: A { }, or even an empty ExtensionDeclSyntax list, when the interface is @attached(extension, conformances: A, B). Also the error is not properly reported, it's either "Type 'MyStruct' does not conform to protocol 'A", which is a false claim, or in many cases, just a non-zero return value.

I'll post a new topic later if I still think it's a bug after some more tests. Thank you again @Pippin for your help!