Swift Macro Member Named Symbols Interfering with Compiler Protocol Conformance Check?

Hi! I'm seeing some unexpected behavior when adding named symbols to a member macro declaration. The name seems to be interfering with some Swift Protocol Conformance (Equatable) in an unexpected way.

I'm starting from the main branch of swift-syntax and hacking on DictionaryStorage (from Examples). I start by conforming a Point to Equatable:

@DictionaryStorage
struct Point: Equatable {
  var x: Int = 1
  var y: Int = 2
}

This leads to a compiler error (the compiler does not synthesize conformance because the underlying Dictionary does not conform to Equatable. This makes sense… and I can create a void implementation to pass the compiler check:

@DictionaryStorage
struct Point: Equatable {
  static func == (lhs: Point, rhs: Point) -> Bool { fatalError() }
  
  var x: Int = 1
  var y: Int = 2
}

This compiles. Suppose our DictionaryStorage were somehow smart enough to synthesize an == function for us. We could start by adding that function name to the macro declaration:

@attached(memberAttribute)
@attached(member, names: named(_storage), named(==))
public macro DictionaryStorage() = #externalMacro(module: "MacroExamplesImplementation", type: "DictionaryStorageMacro")

It is legit to add symbol names without actually generating those symbols:[1]

When a macro declaration includes the names: argument, the macro implementation must generate only symbol with names that match that list. That said, a macro need not generate a symbol for every listed name.

This now leads to a compiler error on the Point struct:

error: type 'Point' does not conform to protocol 'Equatable'
struct Point: Equatable {
       ^
Swift.Equatable:2:17: note: multiple matching functions named '==' with type '(Point, Point) -> Bool'
    static func == (lhs: Self, rhs: Self) -> Bool
                ^
/Users/rick/Developer/swift-syntax/Examples/Sources/MacroExamples/Playground/ComplexMacrosPlayground.swift:21:15: note: candidate exactly matches
  static func == (lhs: Point, rhs: Point) -> Bool { fatalError() }
              ^
/Users/rick/Developer/swift-syntax/Examples/Sources/MacroExamples/Playground/ComplexMacrosPlayground.swift:21:15: note: candidate exactly matches
  static func == (lhs: Point, rhs: Point) -> Bool { fatalError() }

Where did this come from? My macro is not generating an == function… only declaring that it might choose to generate an == function.

What about Hashable? Same problem?

@attached(memberAttribute)
@attached(member, names: named(_storage), named(hash))
public macro DictionaryStorage() = #externalMacro(module: "MacroExamplesImplementation", type: "DictionaryStorageMacro")

@DictionaryStorage
struct Point: Hashable {
  static func == (lhs: Point, rhs: Point) -> Bool { fatalError() }
  
  func hash(into hasher: inout Hasher) { fatalError() }
  
  var x: Int = 1
  var y: Int = 2
}

This code compiles without a problem… weird.

Why is that first example failing to compile? And why is that second example not failing to compile?

What about another protocol with a binary operator? Like Comparable?

@attached(memberAttribute)
@attached(member, names: named(_storage), named(<), named(hash))
public macro DictionaryStorage() = #externalMacro(module: "MacroExamplesImplementation", type: "DictionaryStorageMacro")

@DictionaryStorage
struct Point: Comparable {
  static func < (lhs: Point, rhs: Point) -> Bool { fatalError() }
  
  static func == (lhs: Point, rhs: Point) -> Bool { fatalError() }
  
  func hash(into hasher: inout Hasher) { fatalError() }
  
  var x: Int = 1
  var y: Int = 2
}

This code also fails (with a similar error to the Equatable example).

Any ideas about what could be causing this? Does this error look legit… or is there some strange bug happening here?

Here is a diff on swift-syntax to repro:

diff --git a/Examples/Sources/MacroExamples/Interface/ComplexMacros.swift b/Examples/Sources/MacroExamples/Interface/ComplexMacros.swift
index da160c99..46ca744e 100644
--- a/Examples/Sources/MacroExamples/Interface/ComplexMacros.swift
+++ b/Examples/Sources/MacroExamples/Interface/ComplexMacros.swift
@@ -21,7 +21,7 @@
 ///   * Member macro expansion, to add a `_storage` property with the actual
 ///     dictionary.
 @attached(memberAttribute)
-@attached(member, names: named(_storage))
+@attached(member, names: named(_storage), named(==), named(hash))
 public macro DictionaryStorage() = #externalMacro(module: "MacroExamplesImplementation", type: "DictionaryStorageMacro")
 
 @attached(accessor)
diff --git a/Examples/Sources/MacroExamples/Playground/ComplexMacrosPlayground.swift b/Examples/Sources/MacroExamples/Playground/ComplexMacrosPlayground.swift
index 8e02d839..9c8a7207 100644
--- a/Examples/Sources/MacroExamples/Playground/ComplexMacrosPlayground.swift
+++ b/Examples/Sources/MacroExamples/Playground/ComplexMacrosPlayground.swift
@@ -17,7 +17,11 @@ import MacroExamplesInterface
 // Move the storage from each of the stored properties into a dictionary
 // called `_storage`, turning the stored properties into computed properties.
 @DictionaryStorage
-struct Point {
+struct Point: Hashable {
+  static func == (lhs: Point, rhs: Point) -> Bool { fatalError() }
+  
+  func hash(into hasher: inout Hasher) { fatalError() }
+  
   var x: Int = 1
   var y: Int = 2
 }


  1. swift-book/TSPL.docc/ReferenceManual/Attributes.md at swift-5.10-fcs · apple/swift-book · GitHub ↩︎

It's a minefield to use macros and protocols having synthesized conformance together. I filed a bug about this issue a while back. See this post.

1 Like

@rayx Ahh… yes. Those look similar to what I saw. I tried searching before posting but I did not find those. Thank you for updating.

1 Like