I was able to get surprisingly far with just an expression macro that would "wrap" the function body in a trailing closure, which at least makes it easy to capture the return value and do something with it before returning. But yeah, printing the arguments isn't possible unless you pass them to the macro explicitly. By abusing the heck out of closures and implicit single-expression return
s, I was able to write this:
func adder(_ a: Int, _ b: Int) -> Int {
#trace(a, b) {
let result = a + b
return result
}
}
which gets transformed to this:
func adder(_ a: Int, _ b: Int) -> Int {
{
print("enter: \(#function) <- ", terminator: "");
{print("a = \(a), ", terminator: "");print("b = \(b), ", terminator: "");}()
print()
let __macro_local_0 = {
let result = a + b
return result
}()
print("exit: \(#function) -> \(__macro_local_0)")
return __macro_local_0
}()
}
with this macro:
@expression public macro trace<Result>(
_ args: Any..., body: () -> Result
) -> Result = #externalMacro(module: "MacroExamplesPlugin", type: "TraceMacro")
Then, if I invoke let x = adder(100, 200)
, I get this output:
enter: adder(_:_:) <- a = 100, b = 200,
exit: adder(_:_:) -> 300
Expand for plug-in implementation
import SwiftSyntax
import SwiftSyntaxBuilder
import _SwiftSyntaxMacros
public struct TraceMacro: ExpressionMacro {
public static func expansion(
of macro: MacroExpansionExprSyntax,
in context: inout MacroExpansionContext
) throws -> ExprSyntax {
guard let closure = macro.trailingClosure else {
throw CustomError.message("#trace must have a trailing closure")
}
let argPrinterClosure = ClosureExprSyntax(
statements: CodeBlockItemListSyntax {
for arg in macro.argumentList {
let argString = String(describing: arg.withTrailingComma(false))
let printExpr: ExprSyntax =
"""
print("\(raw: argString) = \\(\(arg.expression)), ", terminator: "")
"""
CodeBlockItemSyntax(item: .expr(printExpr), semicolon: .semicolonToken())
}
}
)
let resultIdentifier = context.createUniqueLocalName()
return """
{
print("enter: \\(#function) <- ", terminator: "");
\(argPrinterClosure)()
print()
let \(resultIdentifier) = \(closure)()
print("exit: \\(#function) -> \\(\(resultIdentifier))")
return \(resultIdentifier)
}()
""" as ExprSyntax
}
}
Note: I had to use the toolchain from this comment above from Doug to get around expanded variable declaration issues.
I'm not exactly proud of the implementation of that macro, but it was a really fun exercise to see how far I could get with just the expression features.