Attached Macros and Comparable Conformance Issues

I discovered an issue I'm having with attached member macros, and Comparable conformance. I distilled it down to the following case, but I'm not identifying the issue, and therefore I'm thinking that there may be a bug. As usual this example is coming from a more intricate version. This is being done through Xcode. I have the following:

@testForceConstantVarDecl()
struct Junk: Comparable {
    var ident: Int

#if true
    static public
    func < (lhs: Junk, rhs: Junk) -> Bool {
        lhs.ident < rhs.ident
    }
#endif
}

When I expand the macro in Xcode, I see--which is what was desired:

struct Junk: Comparable {
    var ident: Int

#if true
    static public
    func < (lhs: Junk, rhs: Junk) -> Bool {
        lhs.ident < rhs.ident
    }
#endif
    var hello: String
}

However in this case, the compiler complains with:

...: error: type 'Junk' does not conform to protocol 'Comparable'
struct Junk: Comparable {
       ^
Swift.Comparable:2:17: note: multiple matching functions named '<' with type '(Junk, Junk) -> Bool'
    static func < (lhs: Self, rhs: Self) -> Bool
                ^
...: note: candidate exactly matches
    func < (lhs: Junk, rhs: Junk) -> Bool {
         ^
...: note: candidate exactly matches
    func < (lhs: Junk, rhs: Junk) -> Bool {

When I comment out the macro, as in:

//@testForceConstantVarDecl()
struct Junk: Comparable {
    var ident: Int

#if true
    static public
    func < (lhs: Junk, rhs: Junk) -> Bool {
        lhs.ident < rhs.ident
    }
#endif
}

The compiler does not complain.

In the macro package the following are created in the appropriate files:

Macro Declaration

@attached(member, names: arbitrary)
public macro testForceConstantVarDecl() =
    #externalMacro(
        module: "PurGMCMacrosPlugin",
	type: "PurGMCTestForceConstantVarDecl")

Macro Reference

#if canImport(SwiftCompilerPlugin)
import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct PurGMCMacrosPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        ...,
	    PurGMCTestForceConstantVarDecl.self
    ]
}
#endif

Macro Implementation

import Foundation
import SwiftSyntax
import SwiftSyntaxMacros

public struct PurGMCTestForceConstantVarDecl: MemberMacro {

    public static
    func expansion(of node: AttributeSyntax,
                   providingMembersOf declaration: some DeclGroupSyntax,
                   in context: some MacroExpansionContext) throws ->
                                                               [DeclSyntax] {
        guard let attrsArgs = node.arguments,
              case let .argumentList(attrsArgsList) = attrsArgs,
              attrsArgsList.count == 0 //,                                       
              else {
            return []
        }
        return [
            """                                                                  
            \n    var hello: String                                              
            """
	]
    }
}

So I'm suspecting the compiler is automatically generating, or believing it should/has generated Comparable conformance when the macro invocation is not commented out. If it has generated it, I do not know how to see it.

When the macro is "turned on", and the #if true directive is changed to #if false, the compiler correctly (I believe) complains with:

Automatic synthesis of 'Comparable' is not supported for struct declarations

I'm therefore confused by the state where the macro and compiler directive are both "on". Is there something that I'm doing incorrectly? Although this is a trivial macro, which would have no real use, I'm a little hesitant calling this a bug, since I would think this would have been seen. This test case is derived from a parametrized macro which provides for proprietary boiler plate code generation. It all works except for this conformance issue exemplified by this test case. Maybe this is a known issue. Is it?

Work Around

I soon realized this was solvable via an extension. So the above test code becomes:

@testForceConstantVarDecl()
struct Junk {
    var ident: Int

#if true
    static public
    func < (lhs: Junk, rhs: Junk) -> Bool {
        lhs.ident < rhs.ident
    }
#endif
}

extension Junk: Comparable {
}

So yeah this works, and now I've convinced myself that yes this is a bug. Any incite? Even though the solution is fine, I thought I would post anyway, in case I learned something new from someone.

1 Like

From my experience, the current implementation (I'm using Xcode 15.1) doesn't work well when using macro and protocols having synthesized conformance together. I for one ran into a few odd issues (see #70086, #70087, #70181). Most of my issues were with extension macro and I used the same workaround as the one you found for a while. I gave up in the end because there are more issues with extension macro (see #69073 for example).

Now I mainly use member macros. In the places where I need extension macro, I use member macro this way:

@MyMemberMacro
extension Foo: FooProtocol {
}

The issue you reported is about member macro. I didn't run into it because (fortunately) I don't use compiler directive. It shows the issue with macro and synthesized conformance is general. I suspect the issue is well known by Apple engines and they have a long term plan on how to address it. But I haven't found any discussion about it.

Thanks for the references, updated info.. :slightly_smiling_face: