Body macros on properties

According to SE-0415, body macros should be supported on variables and accessors:

Function body macros can be applied to accessors as well, in which case they go on the accessor itself, e.g.,

var area: Double {
  @Logged get {
    return length * width
  }
}

When using the shorthand syntax for get-only properties, a function body macro can be applied to the property itself:

@Logged var area: Double {
  return length * width
}

However, neither of these are currently supported ( Body macro can’t be attached to variables · Issue #75715 · swiftlang/swift · GitHub ). I put together an implementation here: Add support for body macros on variables and accessors by calda · Pull Request #87869 · swiftlang/swift · GitHub

There’s an open question in the implementation that we need to resolve that may have public API impacts, see below: Body macros on properties - #2 by cal


I also wonder if we should support body macros on properties without a body in the original source. This is supported for functions:

 @Remote
 func f(a: Int, b: String) async throws -> String

So it seems reasonable to also support generating a computed variable getter for a property.

A use case I personally have in mind is being able to generate an accessor that uses the name of the attached declaration:

@Generate var myCustomElement: ElementID

// Expands to:
var myCustomElement: ElementID { ElementID(name: "myCustomElement") }

This would nicely complement the behavior of augmenting an existing computed property getter.

What do folks think?

6 Likes

There is also an open question in the implementation that may have public API impacts: Add support for body macros on variables and accessors by calda · Pull Request #3298 · swiftlang/swift-syntax · GitHub

The existing signature of BodyMacro.expansion is:

static func expansion(
  of node: AttributeSyntax,
  providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
  in context: some MacroExpansionContext
) throws -> [CodeBlockItemSyntax]

This works for get / set accessors (AccessorDeclSyntax, conforms to WithOptionalCodeBlockSyntax) but doesn’t work out of the box for properties without explicit accessors (VariableDeclSyntax doesn’t conform to WithOptionalCodeBlockSyntax).

Should we introduce a new expansion signature for computed properties without accessors?

static func expansion(
  of node: AttributeSyntax,
  providingBodyFor declaration: VariableDeclSyntax, // new
  in context: some MacroExpansionContext
) throws -> [CodeBlockItemSyntax]

Or, should we make this work through the existing WithOptionalCodeBlockSyntax signature (implementation options discussion here)?

If we prefer a new signature, this seems like it would need to be ran as an amendment to SE-0415. Would love a suggestion from the LSG on the direction here.

I see, this is already supported using an accessor macro:

Makes sense that we don’t need to duplicate this capability via body macros.

The open question for how to support VariableDeclSyntax in BodyMacro.expansion remains, but may only be an implementation detail.

Right, I was about to say something about using accessor macros here.

I think what might be annoying about this in practice, though, is that I don't believe you can overload a macro solely based on its role. For example, if you wanted to be able to provide the same interface for generated functions and read-only properties:

@Generated func f()
@Generated var x: Int

and have @Generated generate a body for f and a getter for x, it's not possible to declare macro Generated with the same signature but only differing by @attached(body) vs. @attached(accessor). You'd have to rename one, which is unfortunate.

Or, you just have to write this instead:

@Generated func f()
var x: Int { @Generated get }

But from your initial post it sounds like that isn't supported yet.

1 Like

I see, yeah that’s an unfortunate consequence. Confirmed in a sample project that this doesn’t work. However, it seems perfectly reasonable to have a single body macro that supports being applied to both functions and properties.

I don’t really see a reason why body macros definitely shouldn’t support providing a getter body for a property without a body in the original source. It seems logical and feels like it composes well with the other capabilities of body macros.

Are there any downsides to supporting this in body macros? If not, maybe we can add the capability?

If you can already add a body macro to:

@MyBodyMacro
var x: Int {
  1
}

It seems almost confusingly inconsistent that you can’t apply it to:

@MyBodyMacro
var x: Int

Given this exact syntactical transformation (append a curly brace block to the existing partial declaration) is available for body macros on func declarations.