[Pitch] Attribute macros and macro delegation

Swift attached macros currently have no way of applying attributes to the declaration that they are attached to. To fix this, I am pitching attached(attribute) macros which return a list of attributes to apply to the annotated declaration.

This is useful because it allows macros to invoke other macros. For example, a macro that modifies a class to add some functions that require the type to be Observable. The macro could add @Observable to fix the conformance without forcing the user to add both macros separately. This would also fix the cryptic errors when a macro assumes that it is already Observable and tries to use these APIs. This idea was mentioned here.

Attribute macros could also be used to apply attributes like @available(...) to functions when modifying them using body and preamble macros:

@attached(body)
macro WithMyMutex()

@available(macOS 15.0, *) // Mutex only works past macOS 15
let myMutex = Mutex<String>("Some string")

@WithMyMutex
func printValue() {
    print(myMutex)
}

// Turns into this
@available(macOS 15.0, *) // Needs this to safely use the Mutex
func printValue() {
    myMutex.withLock { myMutex in
        print(myMutex)
    }
}

Another issue that I ran into that this would solve is modifying C APIs on import using the MacrosOnImports experimental feature. I wanted a macro that would wrap a C function that returns an error code with a function that throws a Swift error, but I kept running into issues with overload resolution. The non-sugared version conflicted with my wrapped version. The only fix for this that I know of is to add the @_disfavoredOverload attribute to the imported C function. Attribute macros would allow a macro applied to a C function to provide a sugared wrapper that overrides the non-sugared version.

The implementation of these macros would look something like this:

struct MyMacro: AttributeMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingAttributesOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [AttributeSyntax] {
        return [
            ...
        ]
    }
}
4 Likes

Another option here would be to add the swift_private C attribute (also spelled as the NS_REFINED_FOR_SWIFT macro) which prefixes the Swift name with two underscores to free you up to write a replacement.

That was my next thought too: I added ‘SwiftPrivate: true’ to my apinotes. The problem here is that macros are run after the underscores are added. And since peer macros cannot output arbitrarily-named declarations to the global scope, I cannot remove the ‘__’ from the name of the wrapper.

If I remember right, ‘SwiftPrivate’ also doesn’t change the overload resolution rules, it just hides it in the editor, so it would still fail even if I was fine with an __ prefix on my wrapper.

Right, it doesn’t change the overload resolution rules (since I presume the intent is to move the name aside to something you would never accidentally type). Seems like what’s really needed here is an option for the macro to say it wants to add the SwiftPrivate attribute and have that wired up so the macro is allowed to generate symbols based on the original C name and then call into the underscored variant as the implementation.

I've had a similar need recently (macros that should only be applied to types conforming to a specific protocol, where the protocol conformance is synthesized by a macro — similar to how @Observable works), but in my case auto-applying the "required" macro would not be a valid solution.

This is going to be the case every time macros that take arguments are involved. Say I have a @Foo macro, which requires the type to conform to a protocol, for which the conformance is added by another macro @Bar(...), that has one or more required parameters. Then @Foo can't automatically apply @Bar(...), because the latter requires some parameters and @Foo wouldn't know which ones to use.

Ideally, I'd want some way of emitting a diagnostic (i.e. "@Foo requires type to conform to BarProtocol....") and a fix-it ("Use macro @Bar(...) to conform to BarProtocol...") if, after all macro expansions, the resulting type doesn't conform to the protocol I was expecting it to.

(I realize this is tricky, because macros don't see the expansions of other macros, and AFAICT this is by design —it was explicitly called out in this WWDC video— and indeed, it neatly avoids requiring macros to be applied in a specific order).

There's probably other issues to be solved if macros can apply other attributes to the declaration they are attached to. What if macros result in duplicated attributes (i.e. @Observable @Observable)? What if they cause conflicting declarations (i.e. nonisolated @MainActor)?

You can actually partially do this now. While you can’t directly check if the type conforms to BarProtocol (if it is done in an extension) or see other macros’ expansions, you can use declaration.attributes to check whether the @Bar macro is already provided or not.

This is an interesting case.

For @Observable @Observable, this is already fixed. Extension macros that define a conformance are passed which protocols are unsatisfied:

@attached(extension, conformances: Observable)

Here @Observable will be called once with conformingTo: [Observable] and once with conformingTo: [], so the conformance should only be added once.

For cases where macros add mutually exclusive attributes like @MainActor and @MyGlobalActor, the compiler would already emit an error, and the fact that the attributes are added by a macro would already be included as the source location for any errors on the attributes: In expansion of macro ‘...’ on ...

1 Like