SE-0382: Expression Macros

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 returns, 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: "");}()
    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: "");
        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.


I agree that writing them will be rare (thankfully more rare than in C) but using them is also difficult since every macro will have different rules. At least with result builders the basic syntax within the builder is the same whether or no the builder has implemented supported for all of the build* methods.

The magic of Codable and Equatable synthesis is slight, as you can hand write that logic (and we do) quite easily. Not really possible with generalized macros.

What can you possibly be basing that on? Swift's current debugging experience is unreliable at best and macros would need a new type of compile time debugging as well as runtime support (writer and user debugging). Personally, I've run out of patience with Swift's tooling.

I think this is a good point, but probably one that makes more sense for declaration macros or some sort of auto-protocol conformance macro. I hope it will be best practice to look for a user implementation of any auto-generated code and allow that to override what the macro would have written. I think there is a strong need to establish good best practices for cases like this.

I guess I'm not clear on what form of debugging this refers to. If talking about lldb, I hope it will be able to step through the output of -dump-macro-expansions.

Macro writers need to be able to set breakpoints in a macro during the build process when it's invoked so we can actually investigate the context and help build the macro. Macro users, if they have access to the macro source, may need to be able to do the same thing at runtime. I'm not sure how useful that is and it may already be possible, but the macro writer case seems impossible right now. In that way writing and debugging macros seems even more difficult than result builders.


Ok. That seems easily solvable with advances to tooling though. I get that it might take awhile for tooling to improve if it isn't prioritized, but I don't see anything that would prevent creation of better tooling. Hopefully it would be implemented as a way to debug expansions regardless of if macro source is present, but that might be considered private API... It would be nice to see more about debugging in the proposal even if just as a future direction.

There may be some workarounds–admittedly tedious–like passing in a closure to hold a breakpoint during development.

Overall, I see a need for macros in some contexts and either manipulating syntax trees or using templates is what every modern macro system does. I personally prefer the former. I agree that good tooling should be part of the picture, but I still think the proposal is a step in the right direction.

EDIT: Small correction

1 Like

My point is that I've lost faith that Apple will properly prioritize such tooling, so proposals that require it are automatic no votes unless the tooling is already being built in some way. Even then, the longevity of such tooling is questionable, so I'm generally negative on anything that needs it at all.


While no doubt it is valid to want an excellent tooling experience, as part of the Swift Evolution process we consider the implementability of proposals but do not evaluate either the quality of any actual implementation nor review any engineering roadmap.


I'm aware. Personally, implementability should include tooling and the ecosystem as a whole, since no one uses a feature in a vacuum. Apple has especially lost my trust over the last two years, from implementation to tooling to maintenance. The language workgroup can consider what they want here, but this is how I'll vote from now on, at least for proposals like this.


having worked with SwiftSyntax extensively over the summer, i’ve learned that the version of SwiftSyntax you are using is quite important, and getting it right is more, not less difficult than getting normal build dependency resolution right. especially if you are distributing libraries and therefore (likely) targeting multiple toolchain versions per release.

i won’t comment on the pros or cons of macros themselves, but i really do not think it is a good idea to start exposing this feature until the SwiftSyntax versioning story is more fleshed out.


I strongly dislike Codable and Result Builders, and part of that dislike is that they remind me of early-2000s boost libraries that found "clever" ways to do things which seemed like they should be impossible which resulted in utterly alien code. They're even worse than those libraries, though, because they didn't actually find a clever way to make things like CodingKeys work, and instead had compiler magic added specifically to enable their weird misuse of the language. My assumption is that the unstated context behind the macro vision document is that other SDK teams at Apple want to do similar things for their APIs, and macros are a way to address that demand without a steady stream of niche special compiler magic. If those are indeed the two options, I would certainly pick a macro system every day.

I share many of the concerns around tooling. This proposal is implicitly promoting SwiftSyntex to being part of the language, and that's a very big deal for a library which is currently unstable, undocumented, and not really realistically usable by third-parties. I don't think it makes sense to send SwiftSyntax through the evolution process (I really can't imagine it getting much in the way of productive feedback), but there does need to be some sort of story for how SwiftSyntax is going to get to a state where it's reasonable for a language feature to depend on it.

Similarly, the question of if macros will be debuggable is very relevant to the question of if macros are a good idea or not. Macros which can be stepped through in a debugger and macros which can't be are categorically different in what sorts of problems they can cause, and it's very important to design a macro system which falls into the first category. "That's an implementation question" suggests that this macro system hasn't been designed to ensure that it's debuggable, and that's a pretty big problem.


To clarify, whether the design of a feature is conducive to or inhibits debugging is in scope for review, and any suggestions as to changes to the design which would improve the user debugging experience would be welcome.

“I’m only in favor of this feature if it can be implemented well”—in scope for Swift Evolution.

“I’m only in favor of this feature if it will be implemented well”—not up to us.


Does the dependence on SwiftSyntax mean that SwiftSyntax is now a part of official language specification?



  • What are the moduleName and fileName properties for?

  • Does the public init(moduleName:fileName:) exist only for testing?

I finally came around to working some more on the result builder macro. Here are the results:

As I see it, there are two types of result builders:

  • Type-erasing ones that always return the same type
  • Ones that derive the result type from the code structure such as SwiftUI's ViewBuilder

The feasibility of creating a macro that takes a type-erasing result builder type and a closure and produces a transformed closure has already been proven in this and this post. It is pretty simple and works as expected.

Doing the same for ViewBuilder-style result builders is more challenging. However, I have managed to implement a very rough proof-of-concept that does exactly this and works pretty well:

let very = true
let long = true

let viewClosure = #apply(resultBuilder: ViewBuilder.self) {
    if very {
    if long {
        Label("long", systemImage: "arrow.left.and.right")
        Text("(really long)")
    } else {
        let shortWord = "short"
    let lastWord = "sentence"

print(type(of: viewClosure()))
// prints TupleView<(Text, Text, Text, Optional<Text>, _ConditionalContent<TupleView<(Label<Text, Image>, Text)>, Text>, Text)>

The macro does not depend on nor is it specialized for SwiftUI or ViewBuilder (with one little caveat I will come to later). This means it should work for other result builder types. As far as I know, the macro produces the same function calls as the built-in feature.

Full implementation

In MacroExamplesLib:

@expression public macro apply<R>(resultBuilder: R.Type, to closure: () -> Void) -> (() -> any View) = #externalMacro(module: "MacroExamplesPlugin", type: "ResultBuilderMacro")

In MacroExamplesPlugin:

public struct ResultBuilderMacro: ExpressionMacro {
    public static func expansion(
        of node: MacroExpansionExprSyntax, in context: inout MacroExpansionContext
    ) throws -> ExprSyntax {
            let resultBuilderSelfExpr = node.argumentList.first?,
            let resultBuilderName = resultBuilderSelfExpr.base?.withoutTrivia().description,
            let originalClosure = node.argumentList.dropFirst().first? ?? node.trailingClosure
        else {
            throw SomeError()
        let originalStatements: [CodeBlockItemSyntax] = { $0.withoutTrivia() }
        return "{ () -> any View in\n\(raw: rewrittenStatements(forOriginalStatments: originalStatements, finalCallPrefix: "return ", resultBuilderName: resultBuilderName, context: &context))\n}"
    private static func rewrittenStatements(forOriginalStatments originalStatements: [CodeBlockItemSyntax], finalCallPrefix: String, finalCallSuffix: String = "", resultBuilderName: String, context: inout MacroExpansionContext) -> String {
        var localNames: [String] = []
        var newStatements: [String] = []
        for statement in originalStatements {
            switch statement.item {
            case .expr(let expr):
                let localName = context.createUniqueLocalName().description
                newStatements.append("let \(localName) = \(expr);")
            case .stmt(let stmt):
                let localName = context.createUniqueLocalName().description
                if let ifStmt = {
                    if case .codeBlock(let elseBody) = ifStmt.elseBody {
                        let tempIfName = context.createUniqueLocalName()
                        let tempElseName = context.createUniqueLocalName()
                            let \(localName) = {
                                if false {
                                    \(rewrittenStatements(forOriginalStatments: { $0.withoutTrivia() }, finalCallPrefix: "let \(tempIfName) = ", resultBuilderName: resultBuilderName, context: &context))
                                    \(rewrittenStatements(forOriginalStatments: { $0.withoutTrivia() }, finalCallPrefix: "let \(tempElseName) = ", resultBuilderName: resultBuilderName, context: &context))
                                    return true ? \(resultBuilderName).buildEither(first: \(tempIfName)) : \(resultBuilderName).buildEither(second: \(tempElseName))
                                if \(ifStmt.conditions) {
                                    \(rewrittenStatements(forOriginalStatments: { $0.withoutTrivia() }, finalCallPrefix: "return \(resultBuilderName).buildEither(first: ", finalCallSuffix: ")", resultBuilderName: resultBuilderName, context: &context))
                                } else {
                                    \(rewrittenStatements(forOriginalStatments: { $0.withoutTrivia() }, finalCallPrefix: "return \(resultBuilderName).buildEither(second: ", finalCallSuffix: ")", resultBuilderName: resultBuilderName, context: &context))
                    } else {
                            let \(localName) = {
                                if \(ifStmt.conditions) {
                                    \(rewrittenStatements(forOriginalStatments: { $0.withoutTrivia() }, finalCallPrefix: "return \(resultBuilderName).buildIf(", finalCallSuffix: ")", resultBuilderName: resultBuilderName, context: &context))
                                return \(resultBuilderName).buildIf(nil)
                } else {
        newStatements.append("\(finalCallPrefix)\(resultBuilderName).buildBlock(\(localNames.joined(separator: ", ")))\(finalCallSuffix)")
        let joinedStatements = newStatements.joined(separator: "\n")
        return joinedStatements

Here are the things I have learned along the way:

  • With the built-in result builders feature, closures are able to return an opaque result type, i.e. some View. With the current implementation, macros are not allowed to return opaque result types so the macro is forced to return a closure producing any View. I suppose this is due to the fact that the macro is type-checked before being expanded. @Douglas_Gregor: Is there any chance that macros could return an opaque result type? I think this feature would be very valuable.

  • The biggest problem in the macro implementation was to satisfy the type checker when transforming if statements. This is because a macro has no way of getting the type of subexpressions of the expression passed to the macro. I work around this by putting the generated if statement into a closure to use the automatic return type inference. I am not sure if putting the code into a closure can be a problem in some situations.

    For simple if statements, this is pretty straight-forward. For ones that also have an else clause, things get pretty messy. This is because ViewBuilder-style result builders have two methods:

    public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent>
    public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent>

    This means the return type depends on both branches. Having no access to the type of subexpressions, the macro has to use a hack to use the automatic type inference. It looks like this in pseudo code:

    let result = {
        if false {
           ... // Code that builds result of if body
           ... // Code that builds result of else body
           return true ? ViewBuilder.buildEither(first: /*Result of if body*/)
                       : ViewBuilder.buildEither(second: /*Result of else body*/)
        if long  {
            ... // Code that builds result of if body
            return ViewBuilder.buildEither(first: /*Result of if body*/)
        } else {
            ... // Code that builds result of else body
            return ViewBuilder.buildEither(second: /*Result of else body*/)

    The if false { ... } is obviously never executed and only there to satisfy the type checker. It means that the whole code is duplicated. Of course, this is very inefficient. It could lead to problems if both if and else branch declare variables of the same name. This could probably be mitigated by renaming the variables but it feels like a brittle solution.

    Possible solutions could be:

    1. Giving macros access to type information. I think for this case, it would not be enough to provide the type of subexpressions, but to give access to the function signatures of the result builder types (via static reflection) or alternatively allow macros to query the type of arbitrary expressions.
    2. Introducing multi-statement if and case expressions that are smart enough to piece together the return type from both branches.
    3. Making type inference smart enough to infer the return type of the closure if the if false { ... } is dropped.

    The current macro implementation neither supports if statements with else ifs nor switch statements. I think that it could be extended to support them. However, the type inference hacks would probably need to get even worse.

  • If a user of the macro makes a mistake, the error produced is not so great. One example would be the error Static method 'buildBlock' requires that 'Int' conform to 'View'. The message can be helpful if the user knows a little about result builders. However, with the current implementation the error does not point to any code location. So the user must look through every line of code to find the error. I think the compiler could be improved to point to the location of the macro. However, it could not know, which subexpression is at fault.

    To provide a good error message, the macro itself would need to diagnose the error. However, to do that, again, the macro would need access to (a) the type of subexpressions and (b) static reflection of the result builder type (that includes static functions).

  • Currently, writing code using the result builder macro generates warnings such as Result of 'Text' initializer is unused, because in the original closure, the expression results are unused. I think the compiler should generate such warnings only after macro expansion.

  • This is the caveat I was alluding to earlier: The current implementation produces an error if a macro wants to return a closure whose return type depends on a generic argument:

    @expression public macro apply<R: ResultBuilder>(simpleResultBuilder: R.Type, to closure: () -> Void) -> (() -> R.FinalResult) = #externalMacro(module: "MacroExamplesPlugin", type: "SimpleResultBuilderMacro")
    public protocol ResultBuilder {
        associatedtype FinalResult
    Full error
    checked decl cannot have error type
    (macro_decl range=[/Users/kocki/workspace/swift-macro-examples/MacroExamplesLib/Macros.swift:33:20 - line:33:208] "apply(simpleResultBuilder:to:)" <R : ResultBuilder> interface type='(R.Type, () -> Void) -> <<error type>>' access=public)
    Please submit a bug report ( and include the crash backtrace.
    Stack dump:
    0.	Program arguments: /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2023-01-07-a.xctoolchain/usr/bin/swift-frontend -frontend -emit-module -experimental-skip-non-inlinable-function-bodies-without-types /Users/kocki/workspace/swift-macro-examples/MacroExamplesLib/Macros.swift -target arm64-apple-macos12.0 -Xllvm -aarch64-use-tbi -enable-objc-interop -sdk /Applications/ -I /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Products/Debug -F /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Products/Debug -no-color-diagnostics -enable-testing -g -module-cache-path /Users/kocki/Library/Developer/Xcode/DerivedData/ModuleCache.noindex -swift-version 5 -enforce-exclusivity=checked -Onone -D DEBUG -enable-experimental-feature Macros -load-plugin-library /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Products/Debug/libMacroExamplesPlugin.dylib -serialize-debugging-options -enable-bare-slash-regex -empty-abi-descriptor -resource-dir /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2023-01-07-a.xctoolchain/usr/lib/swift -enable-anonymous-context-mangled-names -Xcc -I/Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -Xcc -iquote -Xcc /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -Xcc -I/Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -Xcc -I/Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -Xcc -iquote -Xcc /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -Xcc -I/Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Products/Debug/include -Xcc -I/Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -Xcc -I/Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -Xcc -I/Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -Xcc -DDEBUG=1 -Xcc -working-directory/Users/kocki/workspace/swift-macro-examples -module-name MacroExamplesLib -disable-clang-spi -target-sdk-version 13.1 -emit-module-doc-path /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -emit-module-source-info-path /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -emit-objc-header-path /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -serialize-diagnostics-path /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -emit-dependencies-path /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -parse-as-library -o /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/ -emit-abi-descriptor-path /Users/kocki/Library/Developer/Xcode/DerivedData/MacroExamples-esfyllwzbxrrazbckyixrjqgygek/Build/Intermediates.noindex/
    1.	Apple Swift version 5.8-dev (LLVM d0e30b7f831ad7b, Swift 1b92f52a9a0714a)
    2.	Compiling with the current language version
    Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
    0  swift-frontend           0x00000001074072b4 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) + 56
    1  swift-frontend           0x0000000107406538 llvm::sys::RunSignalHandlers() + 128
    2  swift-frontend           0x00000001074078f4 SignalHandler(int) + 304
    3  libsystem_platform.dylib 0x000000018d1042a4 _sigtramp + 56
    4  libsystem_pthread.dylib  0x000000018d0d5cec pthread_kill + 288
    5  libsystem_c.dylib        0x000000018d00f2c8 abort + 180
    6  swift-frontend           0x0000000107860a80 (anonymous namespace)::Verifier::verifyChecked(swift::VarDecl*) (.cold.1) + 0
    7  swift-frontend           0x0000000103d32234 (anonymous namespace)::Verifier::verifyChecked(swift::ValueDecl*) + 284
    8  swift-frontend           0x0000000103d2c31c (anonymous namespace)::Verifier::walkToDeclPost(swift::Decl*) + 4344
    9  swift-frontend           0x0000000103d33af4 (anonymous namespace)::Traversal::doIt(swift::Decl*) + 288
    10 swift-frontend           0x0000000103d339c8 swift::Decl::walk(swift::ASTWalker&) + 32
    11 swift-frontend           0x0000000103eeb0fc swift::SourceFile::walk(swift::ASTWalker&) + 236
    12 swift-frontend           0x0000000103d21ff4 swift::verify(swift::SourceFile&) + 96
    13 swift-frontend           0x0000000103ff0ca4 swift::TypeCheckSourceFileRequest::cacheResult(std::__1::tuple<>) const + 76
    14 swift-frontend           0x0000000103b41bd4 llvm::Expected<swift::TypeCheckSourceFileRequest::OutputType> swift::Evaluator::getResultCached<swift::TypeCheckSourceFileRequest, (void*)0>(swift::TypeCheckSourceFileRequest const&) + 160
    15 swift-frontend           0x0000000103b3f974 swift::TypeCheckSourceFileRequest::OutputType swift::evaluateOrDefault<swift::TypeCheckSourceFileRequest>(swift::Evaluator&, swift::TypeCheckSourceFileRequest, swift::TypeCheckSourceFileRequest::OutputType) + 44
    16 swift-frontend           0x0000000102caf204 bool llvm::function_ref<bool (swift::SourceFile&)>::callback_fn<swift::CompilerInstance::performSema()::$_7>(long, swift::SourceFile&) + 16
    17 swift-frontend           0x0000000102cab0d0 swift::CompilerInstance::forEachFileToTypeCheck(llvm::function_ref<bool (swift::SourceFile&)>) + 160
    18 swift-frontend           0x0000000102cab010 swift::CompilerInstance::performSema() + 76
    19 swift-frontend           0x0000000102b52c50 withSemanticAnalysis(swift::CompilerInstance&, swift::FrontendObserver*, llvm::function_ref<bool (swift::CompilerInstance&)>, bool) + 60
    20 swift-frontend           0x0000000102b45cb4 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 3204
    21 swift-frontend           0x00000001029b1f14 swift::mainEntry(int, char const**) + 3304
    22 dyld                     0x000000018cdabe50 start + 2544

    Therefore, I have to currently hard-code the return type any View into the macro. I suppose this is only a bug in the implementation. If not, this would be an unfortunate limitation.

  • Overloading macros does not seem to work in this case:

    @expression public macro apply<R: ResultBuilder>(resultBuilder: R.Type, to closure: () -> Void) -> (() -> String) = #externalMacro(module: "MacroExamplesPlugin", type: "ResultBuilderMacro")
    @expression public macro apply<R>(resultBuilder: R.Type, to closure: () -> Void) -> (() -> String) = #externalMacro(module: "MacroExamplesPlugin", type: "ResultBuilderMacro2")

    It produces the error Invalid redeclaration of 'apply(resultBuilder:to:). @Douglas_Gregor: Is this the expected behavior or just a limitation of the current implementation? I would expect overloading macros to work just like overloading functions.

All in all, I think the general directions laid out in this proposal is great and I am surprised that I came this far with the proposed feature. What I missed where (a) type information about subexpressions and (b) static reflection. I fear that without these features users of macros (macro users, not macro implementors) could face a very rough user experience because the macro often cannot anticipate errors. It must leave it up to the compiler to generate an error message that might or might not be helpful to the user.

I think it's a good first step. It doesn't bring us the whole way, but it bridges the gap until macros gain an introspection API, wich is inevitable in my opinion.


Yeah, that's fair. It would be silly to insist that the proposal only be accepted if the implementation will be bug-free after all.

The scenario I'm thinking of that I think it currently not well-handled is when you're using a library which provides a macro, your use of the macro is not expanding to what you expect it to be, and you want to step through the expansion in a debugger to figure out what you're doing wrong. The proposed workflow of invoking the macro executable on just the macro to expand and debugging that is a very natural way to go about developing the macro, but seems quite clunky to set up for a user of the macro.

The ideal experience here is what DrRacket does, where debugging macro expansion is identical to debugging ordinary code. That is largely made possible by the fact that in Racket macro expansion is just one of the phases of execution rather than a "compile time" thing, but it's theoretically possible that Xcode could do something sufficiently clever to fake this. If Xcode isn't planning to do something clever, then what's the best we can do? One idea would be to attach by name to the macro executable and then hit build in Xcode. This probably works well if you have exactly one use of the macro, but doesn't if you want to debug the second one. Maybe there could be a way to tell Swift to perform expansion on a specific use of a macro first?

1 Like

Hmm, this seems to add a lot of new syntax. Perhaps we could reuse some idioms we already have, like importing macros from modules instead of having to do separate declarations for them. And I think they would be better suited as functions or callable structs. Just my two cents.


One concern I have is as macro authors how can we write test cases for macro?
I would like this to be addressed by this proposal as well.


Generally, you write test cases for the syntactic transforms. The sample repository has just the one simple one, and there's a whole lot more interesting ones here in the swift-syntax repository.



@Douglas_Gregor I would appreciate it if you could respond to the questions in my previous post before the review deadline. I would especially want to know if the problems I encountered where bugs or features. Then we will be able to discuss the implications of that. Thank you.

And there is one more concern I want to mention: Up until recently, if you wanted to use SwiftSyntax in a library (e.g. for code generation in a swift package plugin), you had to fulfill one of these requirements:

  1. Depend on the exact SwiftSyntax version matching the toolchain of the library user. This is not feasible because different users use different versions of Xcode, etc.
  2. Supply a dylib for every toolchain-platform combination with the library.

Is this still a requirement? The SwiftSyntax changelog seems to suggest, it isn't and I didn't have time to check. If this is still a requirement, it will be very problematic for libraries vending macros.

1 Like

Very cool!

I can certainly see how it would be valuable. Opaque result types as they exist today for functions/properties/subscripts have a notion of identity that is tied to the function/property/subscript returning them, where the identity of the opaque type is determined by that function/property/subscript and any generic arguments to it. That notion of identity doesn't work for macros, because macros get the source code of the arguments and everything that you can access via the macro expansion context, so there's no way to establish identity. That's the logic I followed in banning opaque result types for macros.

However, we could say that every use of a macro that has an opaque result type produces a completely unique opaque type, similar to what happens when we open an existential. From a compiler perspective, we know the opaque result type after macro expansion.

That's all a very long-winded way to say that I think we can lift the restriction, and allow opaque result types for macros. @Joe_Groff does the above seem plausible to you?

I think this is my preferred solution, which we've talked about at various times as being a "join" of the types of each of the returns.

The compiler model here does have some of this information, but it's not being surfaced well to users. When a macro is expanded, the compiler creates an internal buffer containing the source code produced by the macro implementation. If something goes wrong when type-checking that code, the compiler will print an error message pointing into that buffer, with a follow-up note showing the location where the macro expansion was triggered. The result looks something like this (extracted from a compiler test case):

macro:addBlocker:/swift/test/Macros/macro_expand.swift:81:7-81:27:1:4: error: binary operator '-' cannot be applied to two 'OnlyAdds' operands
oa - oa
~~ ^ ~~
/swift/test/Macros/macro_expand.swift:81:7: note: in expansion of macro 'addBlocker' here
  _ = #addBlocker(oa + oa)

The addBlocker macro is my silly example which replaces + with -, so the oa - oa code is the result of macro expansion of #addBlocker(oa + oa). That weird macro:addBlocker: file name is the name of the internal buffer, showing that the macro-expanded source code is there in a compiler, but it's only showing one line of it. We can do better here, for example by leveraging the nifty formatter in swift-syntax to show more of the macro expansion buffer in the compiler's output. (This idea just came to me; I haven't tried it yet)

If you're using Xcode, you'll have to look into the build log to see the compiler output. Clang and Swift use a different serialized format for diagnostics when talking to IDEs, so all of this information is lost right now. I have an RFC on the LLVM Discourse to extend that serialized format to include macro-expansion buffers. Swift's compiler already implements it, but IDEs would need to pick up the new APIs to provide a good experience here.

That looks like a bug in the compiler implementation, rather than a limitation of the design.

It's a limitation of the current implementation, thanks!

Thank you for diving deep into result builders-via-macros! It's great to see what can be achieved with macros, what is still out of reach, and where the pain points are.

Sorry 'bout the delay. Answers above.

Nope! With the advent of the new Swift parser, the swift-syntax package is all-Swift and fully standalone.