[Macros] Attached macros to methods and functions

Hi everyone,
I've played with macros quite a bit and I've found it to be an extremely powerful tool. Tho one thing I'd like to have is ability to have attached macros to func declarations. I've read the vision document from @Douglas_Gregor and I don't think anything like this is on the map. Is there a discussion I've missed that explicitly declined this or it just has not surfaced yet?

2 Likes

If I understand what you are saying correctly, this is now possible. See this example repo created by Doug for some excellent examples:

DougGregor/swift-macro-examples: Example macros for the Swift macros effort (github.com)

This is one example that is an attached macro that is used by decorating function declarations with @AddAsync:
swift-macro-examples/AddAsyncMacro.swift at main · DougGregor/swift-macro-examples · GitHub

Yup, I've used this repo to understand how to work with the new system. Unfortunately, the only attached macro for a function right now is the peer macro, which is not really what attached macros supply for members and types. For example, I would want to have an additional modifier for a member function (i.e. method) that would expand to some property wrapper on windows platform and another property wrapper on macOS.

2 Likes

Got it. Yes, that repo has been a fantastic help for me when learning as well.

Out of curiosity, can you give more details on what you are wanting to achieve? What does the source look like before and after this macro you want to create is expanded?

In the comment I'm talking about ephemeral case. Tho I have a real case which is a little bit more complex.
I have this DSL to safely instantiate C structures used to communicate with Vulkan API because it consumes a lot of raw pointers. Builder is capable producing the temporary struct to be consumed by Vulkan API or intermediate container for said struct to be consumed by "root" builder because Vulkan has this deep nesting in their API. Anyway, usage is pretty simple:

@Lava<VkImageViewCreateInfo>
public func builder(for image: Image) -> LavaContainer<VkImageViewCreateInfo> {
    \.flags <- flags
    \.image <- image
    \.viewType <- type
    \.format <- format
    \.components <- componentMapping
    \.subresourceRange <- subresourceRange
}

See in this declaration the structure VkImageViewCreateInfo is mentioned twice because I was not able to come up with any other way to keep strict static typing, which is the whole point of having this DSL.
So when I've seen first proposal from Doug about macros I was like "yes! this is what I need". Because this macro mechanism is literally compiler plugin that was complete access to the declaration including all the typing.
So what my heart desires is to rename Lava back to LavaBuilder and introduce new macro Lava that would convert this declaration

@Lava
public func builder(for image: Image) -> LavaContainer<VkImageViewCreateInfo> {
...
}

to this

@LavaBuilder<VkImageViewCreateInfo>
public func builder(for image: Image) -> LavaContainer<VkImageViewCreateInfo> {
...
}

preserving all the static typing, safety and convenience while improving readability by a lot.

The original declaration macros proposal included “body” macros, which could replace the body of a function and thus, for example, perform something like the result builder transform.

After a few days of discussion, Doug removed body macros from the pitch with the comment

  • Removed function-body macros... for the moment. We'll come back to them.

So you should probably expect to see them again.

3 Likes

Yup, that's the original vision document I've linked in my post. My question is more about parity of macros functionality for properties and methods

Hopefully. Using macros to augment function bodies would be useful.

Have you tried using @Lava at the class/struct/enum level with the @attached(memberAttribute) capability?

@attached(memberAttribute)
macro Lava() = #externalMacro(module: "Impl", type: "LavaMacro")

@Lava
class Flow {
  // before expansion
  public func builder(for image: Image) -> LavaContainer<VkImageViewCreateInfo> {}
 
  // after expansion
  @LavaBuilder<VkImageViewCreateInfo>
  public func builder(for image: Image) -> LavaContainer<VkImageViewCreateInfo> {}
}

One temporary solution to “replace” a function declaration is to create a macro with member-attribute and peer roles.

@ReplacerMacro func myFunc() {}

// Transformed to:

@available(*, unavailable) 
@_disfavoredOverload
func myFunc() {} // Original

func myFunc(/* new arguments, ... */) {
  // Transformed body
}

However, this (rightfully) confuses auto-completion.

1 Like

Have you tried using @Lava at the class/struct/enum level with the @attached(memberAttribute) capability?

Hmm, that's actually great idea. I could make it "smart"-ish by checking the return type of all members including properties and methods. Like @Observable. And add a macro to ignore member to have ability to opt-out.

1 Like

This implies that both type had an attached macro and a member itself. IMO that's more confusing that what @Steven0351 suggests

1 Like