How to expand freestanding declaration macro with a defer block?

I have implemented a custom macro to simplify signposting in methods. It worked fine all the time. Today I updated to Xcode 15.3 with Swift 5.10 and suddenly it results in a compiler error.

Usage:

#signpost

Declaration:

@freestanding(declaration, names: named(signpostID), named(signpostIntervalState))
public macro signpost() = #externalMacro(module: "RedactedLoggingMacros", type: "SignpostMacro")

Expansion:

let signpostID = signposter.makeSignpostID()
let signpostIntervalState = signposter.beginInterval(#function, id: signpostID)

defer {
    signposter.endInterval(#function, signpostIntervalState)
}

Xcode indicates the following error on the defer block.

Expected macro expansion to produce a declaration

That has not happened before. Is this a bug or rather the fix for a previously existing bug? :sweat_smile: I have this macro in use all over the codebase and now it causes compiler errors. :grimacing:

There appears to be no alternative. A freestanding expression macro will nag about the declarations because an expression was expected. :see_no_evil:

2 Likes

I had something similar. Wrapping into an "expression" worked:
_ = { /* your code */ }()

Your defer may not now be at the spot you want, but that worked to compile.

I filed an issue for this in the Swift GitHub project now: Freestanding Declaration Macro Expanding Defer Block Causes Compiler Error starting with Swift 5.10 · Issue #72307 · apple/swift · GitHub

Instead of a declaration macro, you could make an expression macro that takes a trailing closure:

@freestanding(expression)
public macro signpost<R>(_ body: () -> R) -> R = #externalMacro(module: "RedactedLoggingMacros", type: "SignpostMacro")

It would be callable like so:

#signpost {
  // body here
}

And could expand to:

{
  let signpostID = signposter.makeSignpostID()
  let signpostIntervalState = signposter.beginInterval(#function, id: signpostID)
  defer {
    signposter.endInterval(#function, signpostIntervalState)
  }
  return \(bodyExpr)()
}()

This could maybe even be a function without needing a macro at all.

Indeed, and that is similar to what the os framework offers, too.

Meanwhile I found a different way without a macro based on a struct to reduce it to this:

func whatever() async throws {
    let signpost = signposter.begin()

    defer {
        signpost.end()
    }

    // Do something slow and risky here.
}

However, nothing beats the compactness of the original macro which I could use in 95% of the cases.

If you're going to do go that direction, it would be better to put a method that takes a closure on signposter instead, because that prevents the mistake of someone forgetting to call end() when they're done.