Can swift macro create separate implementation of a declaration?

Is there any way that a macro can create an implementation of a protocol?
For example, we have the protocol:

protocol Foo {
   var value: String { get }
   func foo()
}

Can we create a macro AutoImplemented, and add it to the protocol:

@AutoImplemented
protocol Foo { ... }

and as a result, receive and implementation of this protocol, something like it:

@AutoImplemented
protocol Foo { ... }

// Code generated by macro AutoImplemented
struct AutoImplementedFoo: Foo {
   var value: String { fatalError() }
  
   func foo() {
      fatalError()
   }
}

?

I've been playing around with this idea and I cant quite get it to work. I'm yet to fully grasp how all Syntax types work and everything.

I've tried different approaches, but I just don't know how to access the right properties to achieve it.

I've tried:

  • Receiving a type as parameter
@MyMacro(MyProtocol.self)
class MyClass {}
  • Trying to read the protocol conformance
@MyMacro
class MyClass: MyProtocol {}
  • Using a conformance macro
@MyMacro(MyProtocol.self)
class MyClass {}

In all cases I can't really figure out exactly how to get the function syntax members of the protocol, as casting to Function Syntax always fails.

This is a language feature that already exists: default protocol implementations. No need for macros:

protocol Foo {
   var value: String { get }
   func foo()
}

extension Foo {
   var value: String { fatalError() }
  
   func foo() {
      fatalError()
   }
}

// Usage
struct AutoImplementedFoo: Foo { }
AutoImplementedFoo().value // raises a fatal error
AutoImplementedFoo().foo() // raises a fatal error
1 Like

This video has been a good starting point for me: WWDC23: Write Swift macros.

But it may depends whether you "learn by studying" or "learn by doing".

Thanks a lot for your contribution to this thread @gwendal.roue!

I'm aware of this feature in the language. Probably the example in my question was a bit misleading. :confused: Default protocol implementations is a great thing but I would love to create an new declaration, add to it much more than default implementation of interface (maybe some properties, dependencies etc. etc.).
Maybe something like this:

@AutoImplemented
protocol Foo { ... }

// Code generated by macro AutoImplemented
struct AutoImplementedFoo: Foo {
   var autoGeneratedPropertyCorrespondingToValueProperty: {value property type}

   var value: String { ... }
  
   var autoGeneratedPropertyCorrespondingToFooMethod: {foo metod type }

   func foo() { ... }
}

I believe that an @attached(peer, names: arbitrary) macro ought to be able to do this, but keep in mind that the implementation will be pretty complex. You’ll need to create a new StructDeclSyntax, give it its name, and add the conformance to it; then you’ll need to walk over the requirements of the protocol and transform them into versions with implementations that can be inserted into the StructDeclSyntax.

There are going to be lots of edge cases; for instance, if the requirement is a computed property, you’ll have to give each accessor an implementation. The sheer number of complications might mean this isn’t the best choice for your first macro. But with enough effort, I think it would be possible to do this.

3 Likes

I really appreciate your respond. Thank you! :heart:

I wasn't sure if attached macros can add any code outside of the scope of the declaration. In the documentation I've find this sentence:

Attached macros modify the declaration that they’re attached to. They add code to that declaration, like defining a new method or adding conformance to a protocol.

and I've thought that this might imply that code can only be added only inside. As we know it's not possible to nested types like structs inside protocols so before diving deeper into the subject I've wanted to consult it. So I'm really glad to hear that it is possible. :heart:

Thank you very much for the advice and I really appreciate the kind words!

1 Like

For my case at least (and perhaps OP's as well from the examples presented?), I was looking to create something like a Mocking library, that you can use for testing instead of having to create mocks manually and/or rely on a code generation tool for this.
Something you can use like:

func testThisWorksFine() {
   @Mocked let repository: MyRepositoryProtocol
   let sut = MyClass(repository: repository)
}

Either like that, or creating a

@Mocked(MyRepositoryProtocol)
class MockedRepository {}

or something that would simplify code around mocking. I realise that going through the effort of supporting every edge case is the path to insanity and to flakiness for future swift releases, so I see that as a no-go for now haha.

Maybe something simpler but still handy could be the way to go, like creating your class (maybe with a conformance macro still for protocol conformance) and a simpler macro to keep track of method call count and things like that, like:

@Mock
class MyMock: CoolProtocol {
   // macro could generate this prop
   var callStack: MyMockCallStack

   @Mock
   func coolAction() {
      // and the macro would generate:
      callStack.coolAction.record()
   }
}

so in a test you could:

func test() {
   let myMock = MyMock()
   // ....
   XCTAssertEqual(myMock.coolAction.callCount, 1)
}
1 Like

Hi! I've been playing around this during the last days and I've achieved promising results, the idea is to add using a peer macro and add it to a protocol definition

@Spy
protocol MyProtocol {
    func myFunction()
}

and then generate a spy class confirming to this protocol

class MyProtocolSpy: MyProtocol {
    var didCallMyFunction = false
    func myFunction() {
        didCallMyFunction = true
    }
}

Using this site was very helpful to just understand how to build the macro expansion using SwiftSyntax

2 Likes

Hi! I am trying exactly this too! My idea is to make a
@Mock class FooBar {}

or similar to auto implement protocol methods that call a fatalError stub. Problem is that I can’t get the tests to call the Conformance macro expansion therefor I can’t debug and iterate in this :sweat_smile:.

In main.swift file it generates something but for tests it doesn’t.

I managed to make other macros successfully but this one…

Does anybody have a working example? The GitHub - DougGregor/swift-macro-examples: Example macros for the Swift macros effort doesn’t have a Conformance example with tests…
Closest example is OptionSet. But does not have tests I can follow.

There are some examples of peer macros in the tests in the compiler swift/test/Macros/Inputs/syntax_macro_definitions.swift at f4672953d8ea17d92b114ec70ca0d502e0252fe7 · apple/swift · GitHub

What @miibpa describes looks like the simplest option to achieve what we want, however, I wouldn't want to generate, compile, or ship this code on my app, and I would prefer to keep gencode on my test targets.

I'm trying to do something like:

@Mock
class SimpleProtocolMock: SimpleProtocol {}

But I haven't been able to retrieve the DeclSyntax for the SimpleProtocol inheritedTypeCollection. Is this going to be possible?

Thank you so much @Keith for your answer.

I looked and even started a new Macro package. In tests, it simply doesn't call the expansion method.

Here's a barebones project:

Essentially:

I copied the EquatableMacro from the swift project and replaced the stringify placeholder with my EquatableMacro.
If you take a couple of minutes to download and build the package I attached above, you can see that it builds and expands as expected in the main.swift file (in the client).
But for tests, it simply won't call the expand method, and it returns the empty class only (if I change the name of the macro to something that doesn't exist, it keeps it in the output so it's definitely doing something but not calling the expand method.
What am I doing wrong or missing?

Also, this only builds for macOS, is that correct? :thinking:
Thank you very much!

I think this will make much more sense if you make a new package using the beta and name it Equatable

Then you end up with

Equatable: the package that you import which is named Equatable
EquatableMacros: the module that contains the implementation of your macros (the types)
EquatableMacro: The type that implements your macro.

It is a distraction that stringify has to be 'cleaned up' whenever we start a macro but it is easier to fight confusion once you see those three names and their meanings.

Thank you @griotspeak. Not expecting anything different, I did it and I have exactly the same issue.

GitHub - nunogoncalves/Equatable (thank goodness GitHub repos are cheap. :D )

Can I ask if you can test it out please? :pray:

I'm willing to put the effort. I'd love to have a tool like mockingbird or swiftymocky but without needing a separate codegen tool.

On the example above, if protocol inherited from other protocol, would we have a way to check the members of the protocol is inheriting from?

Sorry @griotspeak and thank you very much for your help but that doesn't compile in my case. It appears that you built this on top of my first implementation and I get External macro implementation type 'EquatableMacro.EquatableMacro' could not be found for macro 'Equatable'; the type must be public and provided via '-load-plugin-library' on main.swift because it can't find the Macro.

@attached(conformance)
public macro Equatable() = #externalMacro(
    module: "EquatableMacrosMacros", // changed from EquatableMacro
    type: "EquatableMacro"
)

Changing to this :point_up: , the only difference to your gist is that you added the Error type to the Macro.

Now the code compiles AND main.swift file works fine (prints true) but the tests do not call the expansion macro.

I even changed the test to:

final class EquatableTests: XCTestCase {
    func testMacro() {

        @Equatable
        struct SomeStruct {}
        XCTAssertTrue(SomeStruct() == SomeStruct()) // This passes

        assertMacroExpansion(
            """
            @Equatable
            struct SomeStruct { }
            """,
            expandedSource: """
            extension SomeStruct: Equatable { }
            """,
            macros: testMacros
        )
    }
}

XCTAssertTrue is passing, but the assertMacroExpansion is not:

failed - Actual output (+) differed from expected output (-):
–extension SomeStruct: Equatable { }
+
+struct SomeStruct {
+}

Also, changing the Equatable implementation to be a MemberMacro:

public struct EquatableMacro: MemberMacro {

    public static func expansion<Declaration, Context>(
        of node: AttributeSyntax,
        providingMembersOf declaration: Declaration,
        in context: Context
    ) throws -> [DeclSyntax]
    where Declaration : DeclGroupSyntax, Context : MacroExpansionContext {
        return ["""var foo: () -> () = {}"""]
    }

and

//@attached(conformance)
@attached(member, names: named(foo))

The test:

func testMacro() {
        assertMacroExpansion(
            """
            @Equatable
            struct SomeStruct {
            }
            """,
            expandedSource: """

            struct SomeStruct {
                var foo: () -> () = {
                }
            }
            """,
            macros: testMacros
        )
    }

Passes :white_check_mark:.

So either there's something wrong with the Conformance macro, or I'm doing/expecting something wrong in the tests.

It looks like support for conformance macros in assertMacroExpansion was added just a few days ago: [Macros] Implement expansion of conformance macros by DougGregor · Pull Request #1773 · apple/swift-syntax · GitHub. This is the PR for the 5.9 branch: [5.9] Expand conformance macros for testing by DougGregor · Pull Request #1780 · apple/swift-syntax · GitHub.

3 Likes