Skip to content

Macro protocols lack async from expansion function requirements despite proposals stating they should have it #2803

Closed
@roopekv

Description

@roopekv

Issue

Swift's concurrency features cannot be fully utilized in macros without being able to implement expansion function requirements of macro protocols as async functions.

Cause

While the Swift Evolution proposals for expression and attached macros state that the expansion function requirements of macro protocols should be async, the macro protocol declarations in swift-syntax are missing the async keyword from their expansion function requirements (see comparison below), making it impossible to implement them as async functions.

Solution

Changing the declarations in swift-syntax to async shouldn't break existing macros, as async function requirements can be implemented as non-async functions.

Example

protocol Macro {
    func expansion()
}

struct SyncMacro : Macro {
    func expansion() {} // OK ✅
}

struct AsyncMacro : Macro {
    func expansion() async {} // ERROR ❌
}
protocol Macro {
    func expansion() async
}

struct SyncMacro : Macro {
    func expansion() {} // OK ✅
}

struct AsyncMacro : Macro {
    func expansion() async {} // OK ✅
}

I acknowledge that additional changes would have to be made within the swift-syntax package (changes at the expansion function call sites and possible further structural changes to facilitate asynchronous code), but depending on how they are implemented, the changes don't have to affect the user-facing parts of the package in a source breaking way.

Differences between Swift Evolution proposals and swift-syntax

ExpressionMacro

Proposal

public protocol ExpressionMacro: FreestandingMacro {
  /// Expand a macro described by the given freestanding macro expansion
  /// within the given context to produce a replacement expression.
  static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) async throws -> ExprSyntax
}

swift-syntax

public protocol ExpressionMacro: FreestandingMacro {
/// Expand a macro described by the given freestanding macro expansion
/// within the given context to produce a replacement expression.
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax
}

PeerMacro

Proposal

public PeerMacro: AttachedMacro {
  /// Expand a macro described by the given attribute to
  /// produce "peer" declarations of the declaration to which it
  /// is attached.
  ///
  /// The macro expansion can introduce "peer" declarations that 
  /// go alongside the given declaration.
  static func expansion(
    of node: AttributeSyntax,
    providingPeersOf declaration: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) async throws -> [DeclSyntax]
}

swift-syntax

public protocol PeerMacro: AttachedMacro {
/// Expand a macro described by the given custom attribute and
/// attached to the given declaration and evaluated within a
/// particular expansion context.
///
/// The macro expansion can introduce "peer" declarations that sit alongside
/// the given declaration.
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}

MemberMacro

Proposal

protocol MemberMacro: AttachedMacro {
  /// Expand a macro described by the given attribute to
  /// produce additional members of the given declaration to which
  /// the attribute is attached.
  static func expansion(
    of node: AttributeSyntax,
    providingMembersOf declaration: some DeclGroupSyntax,
    in context: some MacroExpansionContext
  ) async throws -> [DeclSyntax]
}

swift-syntax

public protocol MemberMacro: AttachedMacro {
/// Expand an attached declaration macro to produce a set of members.
///
/// - Parameters:
/// - node: The custom attribute describing the attached macro.
/// - declaration: The declaration the macro attribute is attached to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: the set of member declarations introduced by this macro, which
/// are nested inside the `attachedTo` declaration.
///
/// - Warning: This is the legacy `expansion` function of `MemberMacro` that is provided for backwards-compatiblity.
/// Use ``expansion(of:providingMembersOf:conformingTo:in:)-1sxoe`` instead.
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
/// Expand an attached declaration macro to produce a set of members.
///
/// - Parameters:
/// - node: The custom attribute describing the attached macro.
/// - declaration: The declaration the macro attribute is attached to.
/// - conformingTo: The set of protocols that were declared
/// in the set of conformances for the macro and to which the declaration
/// does not explicitly conform. The member macro itself cannot declare
/// conformances to these protocols (only an extension macro can do that),
/// but can provide supporting declarations, such as a required
/// initializer or stored property, that cannot be written in an
/// extension.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: the set of member declarations introduced by this macro, which
/// are nested inside the `attachedTo` declaration.
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}
private struct UnimplementedExpansionMethodError: Error, CustomStringConvertible {
var description: String {
"""
Types conforming to `MemberMacro` must implement either \
expansion(of:providingMembersOf:in:) or \
expansion(of:providingMembersOf:conformingTo:in:)
"""
}
}

AccessorMacro

Proposal

protocol AccessorMacro: AttachedMacro {
  /// Expand a macro described by the given attribute to
  /// produce accessors for the given declaration to which
  /// the attribute is attached.
  static func expansion(
    of node: AttributeSyntax,
    providingAccessorsOf declaration: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) async throws -> [AccessorDeclSyntax]
}

swift-syntax

public protocol AccessorMacro: AttachedMacro {
/// Expand a macro that's expressed as a custom attribute attached to
/// the given declaration. The result is a set of accessors for the
/// declaration.
static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax]
}

MemberAttributeMacro

Proposal

protocol MemberAttributeMacro: AttachedMacro {
  /// Expand a macro described by the given custom attribute to
  /// produce additional attributes for the members of the type.
  static func expansion(
    of node: AttributeSyntax,
    attachedTo declaration: some DeclGroupSyntax,
    providingAttributesOf member: some DeclSyntaxProtocol,
    in context: some MacroExpansionContext
  ) async throws -> [AttributeSyntax]
}

swift-syntax

public protocol MemberAttributeMacro: AttachedMacro {
/// Expand an attached declaration macro to produce an attribute list for
/// a given member.
///
/// - Parameters:
/// - node: The custom attribute describing the attached macro.
/// - declaration: The declaration the macro attribute is attached to.
/// - member: The member declaration to attach the resulting attributes to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: the set of attributes to apply to the given member.
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax]
}

Additional quotes from Expression Macros proposal

The macro expansion operation is asynchronous, to account for potentially-asynchronous operations that will eventually be added to MacroExpansionContext. For example, operations that require additional communication with the compiler to get types of subexpressions, access files in the program, and so on.

* Make the ExpressionMacro.expansion(of:in:) requirement async.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions