My Experience in Developing Swift Macros
I have developed a collection of Swift Macros in this repo recently. This article will summarise my experience and hopefully it would be helpful for your tasks.
Design the macro
In essence, Swift Macro is a way to expand code based on the source information. The first step to develop them is to setup the proper goal. Then analyse what information we need for this goal, which will help us to decide the macro type. For example, if all the information needed comes from macro function itself, it could be a freestanding macro. On the other hand, other than the macro itself, we still need to base on some declaration information to generate the code, it will be an attached macro. Unlike KSP in Kotlin, where we have the freedom to generate any code we want based on annotated code, Swift Macro have limitations on the code generated at the moment. So design the goal carefully. Sometimes, one goal needs to be implemented by multiple macros.
Investigate the source information
The source information that macro can use is with a strong typed format. For details please check out https://github.com/apple/swift-syntax.git. However, it’s a pretty big task to go through this repo to learn. If we only care about the types for one macro, we can just reply on the debug print of the type instance.
To do this, we need to write unit test first like below:
final class FormatDateTests: XCTestCase {
let testMacros: [String: Macro.Type] = [
"formatDate": FormatDate.self,
]
func testFormateDateMacro() {
assertMacroExpansion(
"""
let date = #formatDate(Date(), dateStyle: .full)
""",
expandedSource:
"""
let date = {
let formatter = DateFormatter()
formatter.dateStyle = .full
return formatter.string(from: Date())
}()
""",
macros: testMacros
)
}
}
In this example, we are going to develop a freestanding expression macro called FormatDate . With this unit test in place, we define this FormatDate like below:
public struct FormatDate: ExpressionMacro {
public static func expansion<Node: FreestandingMacroExpansionSyntax,
Context: MacroExpansionContext>(of node: Node,
in context: Context) throws -> ExprSyntax {
""
}
}
As you can see here, the expansion method returns an empty string, but it’s enough for now. If we put a breakpoint at this line and run the unit test, the unit test process will stop here allowing us to print out the source information like below:
Here we print out node as an example. We can do the same for other arguments as well.
Investigate the destination code
Moving onto the destination code, when we write unit test, we already know the destination code we are expecting. The issue here is how to generate them. Basically we have two ways of generating them:
- String interpolation
As you’ve already seen in the above screenshot, we can return string for ExprSyntax . If we can manage to implement the macro logic purely relying on string manipulation, it’s perfectly fine. But be careful, it’s easy to make mistakes this way especially for generating complicated code blocks.
- Syntax builder method
Another way is to utilise Syntax builder tools in the swift-syntax package. Again, it’s a big task as there are many types in this package. In order to limit the types we need to learn, we can use the following tools:
Go to this website and paste the destination code in, it will generate AST like below. Now we know which type we should use.
Swift Command line
We can also use following Swift Command line to achieve the same result. Just put the code snippet into one Swift file.
Swift -frontend -emit-syntax fileName.swift
This will generate result in JSON format like below. Copy it to a JSON formatter to make it readable.
Implement the expansion method
Once we know the source information and the destination code syntax, the rest is basically a usual Swift coding task.
However, we do have some special aspects to consider — — what if the macro is misused and how do we inform user to fix it? The answer is to use Exceptions and Diagnostics . For some simple cases, you might even just want to return empty syntax. We are not going to cover these in details in this article, please go to this repo for some examples.
Sharing is caring!