Macro: "Multiple matching functions XYZ" error (though the expanded code has no duplicate functions and compiles)

I'm writing a macro Foo, which expands code

@Foo
struct Bar {
    var value: String
}

to

struct Bar {
    var value: String
}

extension Bar: Comparable {
    public static func < (lhs: Bar, rhs: Bar) -> Bool {
        lhs.value < rhs.value
    }
}

A key requirement is that, if user already defines < function, the macro should skip generating the function. Since macro doesn't have built-in support for this, I borrow the code from Observation framework. The code itself works well. If user already defines < function, the expanded code is as expected and compiles, but unfortunately the unexpanded code doesn't compile, with the following error:

Type 'Bar' does not conform to protocol 'Comparable'
Multiple matching functions named '<' with type '(Bar, Bar) -> Bool'

My further experiments show that, if I use the same approach to add CustomStringConvertible conformance and skip generating description computed variable if user already defined it, it works well and doesn't have this issue. I have yet to figure out what's the difference between the two cases (maybe function vs variable?). I wonder if anyone observed this issue too?

The issue can be consistently reproduced on my machine with the following code. Note I simplified the code so it doesn't even actually generate < function in macro implementation, but just claims "<" function name in macro interface. But still it caused the error.

Macro implementation:

public enum FooMacro: ExtensionMacro {
    public static func expansion(
        of node: AttributeSyntax,
        attachedTo declaration: some DeclGroupSyntax,
        providingExtensionsOf type: some TypeSyntaxProtocol,
        conformingTo protocols: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -> [ExtensionDeclSyntax] {
        let comparableExtension = try ExtensionDeclSyntax(
                """
                extension \(type.trimmed): Comparable {
                }
                """
            )

        return [comparableExtension]
    }
}

Macro interface:

// Note: chaning the name argument to "named(<(lhs:,rhs:))" has the same error
@attached(extension, conformances: Comparable, names: named(<))
public macro Foo() = #externalMacro(module: "impl", type: "FooMacro")

Test:

// Compile error:
//   Type 'Name' does not conform to protocol 'Comparable'
//   Multiple matching functions named '<' with type '(Name, Name) -> Bool'
// But the expanded code compiles.
@Foo
struct Name {
    var value: String

    public static func < (lhs: Name, rhs: Name) -> Bool {
        lhs.value < rhs.value
    }
}

My environment: Xcode 15.0, macOS 13.6. I don't think it's likely to be caused by my environment and I'm going to file a bug in Swift repo, but I'd appreciate if anyone can help to verify it on your machine.

UPDATE: I submited bug #70087

I encountered a similar issue with Equatable.

@anAwesomeAttachedMacro
struct Foo: Equatable {
  let value: Int
  var bar: Bar
  init() {
    self.init(value: 1)
  }
  static func == (lhs: Self, rhs: Self) -> Bool {
    return lhs.value == rhs.value
  }
}

Compiler output looks like this:

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

Since macro doesn't have built-in support for this, I borrow the code from Observation framework.

There is a similar use case in the macro I'm developing. Could you please provide some detail about it, like which source file to look into? I thought it was impossible for macros to do this other than just checking the StructDecl itself.

See addIfNeeded in this file. It checks existing items in type declaration, so it doesn't work if the item is inherited from protocol or defined in a separate extension.

I got a minimal reproducible demo for my issue.

Compiler version:

Apple Swift version 6.0-dev (LLVM aa5a99efe7c5734, Swift 2f6e9b392b15bf0)
Target: x86_64-apple-macosx13.0

Macro interface:

@attached(member, names: arbitrary)
public macro foo() = #externalMacro(module: "SwiftMacroPlaygroundMacros", type: "FooMacro")

Macro implementation:

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct FooMacro: MemberMacro {
    public static func expansion(
      of node: AttributeSyntax,
      providingMembersOf declaration: some DeclGroupSyntax,
      in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        
        let typeName: TokenSyntax = "Bar"
        var inheritanceClause: InheritanceClauseSyntax?

        // 🛑 flipping the condition to false makes the compiler happy.
        if true {
            let p: TypeSyntax = "Equatable"
            inheritanceClause = InheritanceClauseSyntax {
                InheritedTypeListSyntax {
                    InheritedTypeSyntax(type: p)
                }
            }
        }
        
        let barDecl = DeclSyntax(
            StructDeclSyntax(
              name: typeName,
              inheritanceClause: inheritanceClause
            ) { }
        )
        
        let barStorage: DeclSyntax = "var bar: Bar = Bar()"
        return [barDecl, barStorage]
    }
}

@main
struct SwiftMacroPlaygroundPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        FooMacro.self,
    ]
}

Macro client:

import SwiftMacroPlayground

@foo
struct S: Equatable {
  let value: Int
    func hash(into hasher: inout Hasher) {
        hasher.combine(value)
    }
    static func == (lhs: S, rhs: S) -> Bool {
        return lhs.value == rhs.value
    }
}

Compiler output:

struct S: Equatable {
       ^
Swift.Equatable:2:17: note: multiple matching functions named '==' with type '(S, S) -> Bool'
    static func == (lhs: Self, rhs: Self) -> Bool
                ^
/Users/jiangzhaoxuan/Documents/projects/SwiftMacroPlayground/Sources/SwiftMacroPlaygroundClient/main.swift:9:17: note: candidate exactly matches
    static func == (lhs: S, rhs: S) -> Bool {
                ^
/Users/jiangzhaoxuan/Documents/projects/SwiftMacroPlayground/Sources/SwiftMacroPlaygroundClient/main.swift:9:17: note: candidate exactly matches
    static func == (lhs: S, rhs: S) -> Bool {
                ^

Ah I see, I thought there were some black magic that could make it work with extension, auto synthesize, etc.

I've run into the same (or similar) issue.
FWIW, I'm going to leave a comment of some experiment results.

Macro Implementation

Code
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

struct AddStaticPropertyMacro: MemberMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingMembersOf declaration: some DeclGroupSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax] {
    guard let classDecl = declaration.as(ClassDeclSyntax.self) else { fatalError() }
    return [
      """
      public static let something: \(classDecl.name) = .init()
      """
    ]
  }
}

@main
struct MultipleMatchingPlugin: CompilerPlugin {
  let providingMacros: [Macro.Type] = [
    AddStaticPropertyMacro.self
  ]
}

Macro Interface

@attached(member, names: arbitrary)
public macro AddStaticProperty() = #externalMacro(module: "MultipleMatchingMacros", type: "AddStaticPropertyMacro")

Macro Client

public class Foo: Equatable { // ✅ Of course, no problems.
  public static func ==(_ lhs: Foo, _ rhs: Foo) -> Bool { true }
  public static let something: Foo = .init() // Not from macro.
}

@AddStaticProperty
public class Bar {} // ✅ No problems.
extension Bar: Equatable {
  public static func ==(_ lhs: Bar, _ rhs: Bar) -> Bool { true }
}

@AddStaticProperty
public class Baz: Equatable {} // ⛔️ Type 'Baz' does not conform to protocol 'Equatable'
                               // note: multiple matching functions named '==' with type '(Baz, Baz) -> Bool'
extension Baz {
  public static func ==(_ lhs: Baz, _ rhs: Baz) -> Bool { true }
}

@AddStaticProperty
public class Qux: Equatable { // ⛔️ Type 'Qux' does not conform to protocol 'Equatable'
                              // note: multiple matching functions named '==' with type '(Qux, Qux) -> Bool'
  public static func ==(_ lhs: Qux, _ rhs: Qux) -> Bool { true }
}

Seems like I cannot edit my post after certain amount of time, just wanna make a correction that the flag should be set to false to trigger the compiler bug. Corrected macro implementation:

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct FooMacro: MemberMacro {
    public static func expansion(
      of node: AttributeSyntax,
      providingMembersOf declaration: some DeclGroupSyntax,
      in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        
        let typeName: TokenSyntax = "Bar"
        var inheritanceClause: InheritanceClauseSyntax?

        // 🛑 flipping the condition to true makes the compiler happy.
        if false {
            let p: TypeSyntax = "Equatable"
            inheritanceClause = InheritanceClauseSyntax {
                InheritedTypeListSyntax {
                    InheritedTypeSyntax(type: p)
                }
            }
        }
        
        let barDecl = DeclSyntax(
            StructDeclSyntax(
              name: typeName,
              inheritanceClause: inheritanceClause
            ) { }
        )
        
        let barStorage: DeclSyntax = "var bar: Bar = Bar()"
        return [barDecl, barStorage]
    }
}

@main
struct SwiftMacroPlaygroundPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        FooMacro.self,
    ]
}

I tried to understand the purpose of the FooMacro. It basically adds a variable decl to the applied type decl, using the subtype Bar declared by the macro at the same time. The branch under the comment "🛑 flipping the condition to true makes the compiler happy." controls the subtype Bar whether to conform to Equatable.

When the false flag keeps, the Bar does not conform to Equatable. For a given struct conforms to Equatable like:

@foo
struct Fee: Equatable {
  var foe: Int
}

It comes to be:

@foo
struct Fee: Equatable {
  var foe: Int
  var bar: Bar

  struct Bar {
 }
}

This piece of code does not compile when developers write codes like this. Because a part of an Equatable struct does not conform to Equatable, stopping the compiler's auto-synthesizing process for Equatable.

If the flag flips into true, then the Bar comes to conform to Equatable. For the example above, it comes to be:

@foo
struct Fee: Equatable {
  var foe: Int
  var bar: Bar

  struct Bar: Equatable {
 }
}

And this piece of code compiles when developers write codes like this.