A powerful toolkit for creating concise and expressive Swift macros
Disclaimer
This library is still very much WIP so the API is subject to change. Currently I'd say the library is in a proof of concept phase where not all of SwiftSyntax is wrapped, and APIs such as destructuring and normalisation haven't been brought to all the places that they could be. However, you can always access the underlying SwiftSyntax syntax nodes without hassle when you find something that's missing.
Also, I'm a bit busy so development won't be consistent, but I'll always be available to review PRs! (contributions are extremely welcome)
Motivation
To set the scene, take a read of this motivating example from the readme.
Did you know that
-0xF_ep-0_2
is a valid floating point literal in Swift? Well you probably didn't (it's equal to -63.5), and as a macro author you shouldn't even have to care! Among many things, Macro Toolkit shields you from edge cases so that users can use your macros in whatever weird (but correct) manners they may desire.
The SwiftSyntax API understandably exposes all the nuances of Swift's syntax. But for a macro developer, these exposed nuances usually just lead to bugs or fragile macros!
Consider a greet
macro which prints a greeting for a specified person a given number of times: #greet("stackotter", 2)
-> print("Hi stackotter\nHi stackotter")
. If you implemented that, would your implementation handle being supplied a count of 0xFF
or 1_000
? Probably not. And would it correctly handle the name "st\u{61}ckotter"
? Again probably not (unless you went out of your way to parse unicode escape sequences). Swift Macro Toolkit handles these edge cases for you simply with the handy intLiteral.value
and stringLiteral.value
!
Destructuring
One of the features that I'm most pleased with is destructuring. It's best explained with an example; let's say you're writing a macro to transform a result-returning function into a throwing-function. Now consider how you might parse and validate the function's return type.
Without Macro Toolkit
// We're expecting the return type to look like `Result<A, B>`
guard
let simpleReturnType = returnType.as(SimpleTypeIdentifierSyntax.self),
simpleReturnType.name.description == "Result",
let genericArguments = (simpleReturnType.genericArgumentClause?.arguments).map(Array.init),
genericArguments.count == 2
else {
throw MacroError("Invalid return type")
}
let successType = genericArguments[0]
let failureType = genericArguments[1]
With Macro Toolkit
// We're expecting the return type to look like `Result<A, B>`
guard case let .simple("Result", (successType, failureType))? = destructure(returnType) else {
throw MacroError("Invalid return type")
}
Much simpler!
Diagnostics
Another great quality of life improvement is the DiagnosticBuilder
API for succinctly creating diagnostics. I'm sure that I speak for many developers when I say that I'd be much more likely to write macro implementations with rich diagnostics if diagnostics didn't take 23 lines to create.
Without Macro Toolkit
let diagnostic = Diagnostic(
node: Syntax(funcDecl.funcKeyword),
message: SimpleDiagnosticMessage(
message: "can only add a completion-handler variant to an 'async' function",
diagnosticID: MessageID(domain: "AddCompletionHandlerMacro", id: "MissingAsync"),
severity: .error
),
fixIts: [
FixIt(
message: SimpleDiagnosticMessage(
message: "add 'async'",
diagnosticID: MessageID(domain: "AddCompletionHandlerMacro", id: "MissingAsync"),
severity: .error
),
changes: [
FixIt.Change.replace(
oldNode: Syntax(funcDecl.signature),
newNode: Syntax(newSignature)
)
]
),
]
)
With Macro Toolkit
let diagnostic = DiagnosticBuilder(for: function._syntax.funcKeyword)
.message("can only add a completion-handler variant to an 'async' function")
.messageID(domain: "AddCompletionHandlerMacro", id: "MissingAsync")
.suggestReplacement(
"add 'async'",
old: function._syntax.signature,
new: newSignature
)
.build()
2.5x shorter, and much more expressive!
Contributing
I'm looking for contributors to help me bring this library to a semi-stable release! If you want to help out with development and want ideas for what to work on don't hesitate to contact me (@stackotter, stackotter@stackotter.dev, or by opening a GitHub issue). I'm always available to help contributors even when I don't have enough time to develop the project myself :)
Main tasks
I may have forgotten some, but here are the main tasks that I have in mind for the near future.
- Create a robust type normalisation API (e.g.
Int?
->Optional<Int>
,()
->Void
, and(Int)
->Int
). Macro developers often need to write tedious code simply to check if a type is equivalent toInt?
and this should help alleviate that. - Bring type destructuring to the rest of the type types (e.g. destructuring of tuple types should be possible as seen below)
guard case let .tuple((firstType, secondType))? = destructure(tupleType) else { ...
- Create nicer APIs for common code generation patterns (at the moment I've mostly just implemented random helper methods that I found useful; see examples)
- Use protocols to reduce boilerplate within the library itself (I rapidly prototyped the library, and there is still a significant amount of boilerplate code repeated for types wrapping similar types of syntax)