Relaxing restrictions on macros producing extension declarations

As I wrote in another post, there’s an unfortunate design choice in that macros are not allowed to introduce extension declarations. I don’t really understand the motivations for these restrictions:

extension declarations can never be produced by a macro. The effect of an extension declaration is wide-ranging, with the ability to add conformances, members, and so on. These capabilities are meant to be introduced in a more fine-grained manner.

My use case was reducing the boilerplate needed for creating focused commands in SwiftUI, which requires at a minimum declaring a new struct and extending two others. What could have been one line of code becomes three:

#focusedCommand("Duplicate")
extension FocusedCommandValues { #focusedCommandValue("Duplicate") }
extension View { #focusedCommandMethod("Duplicate") }

This usage does not seem unreasonable, and I am currently getting by by expanding the macro and copy/pasting the generated code, then commenting out the macro invocation.

Surely the compiler can effectively do the same thing, caching the result to avoid multiple expansions (an argument against allowing this I had seen somewhere)?

Note that there are other common SwiftUI constructs that require similar extensions, and they’re all boilerplate.

2 Likes

My understanding is that the issue isn't inefficiency due to lack of caching or repeated expansion, but that allowing arbitrary extensions in macros causes type checking to require eagerly evaluating all macros visible to a file just in case one of them might introduce some extension on a type relevant to that file, even if they'd otherwise go completely unused.

Limiting macros to specific types of functionality (e.g. introducing protocol conformances) means that it's possible to significantly limit which macros you might need to expand from a given scope while type checking, and that macros can be evaluated on an as-needed basis.

1 Like

Could we mitigate the eager expansion issue by letting extension macros specify what types they're going to extend? For example, @JetForMe wants to write a macro that always extends the same two types, FocusCommandValues and View. It seems like it should be possible to optimize using that knowledge.

1 Like

To be specific, I'm thinking of something like this:

@attached(extension, extends: [FocusCommandValues, View])
public macro MyMacro() = #externalMacro(···)

The alternative to doing this via macro is doing it manually. Does that not also incur the same cost (assuming the expansion is cached the first time it’s done)?

It's not that the cost of evaluating the macro is different, it's that if arbitrary extensions are allowed in macros, the compiler can't know which macros need to be expanded in order to parse code.

For example:

struct S {
    func foo() { ... }
}

let s = S()
s.boo() // oops

boo() here is a typo for foo(), but the compiler can't know that for sure until it goes ahead and expands all arbitrary extension macros everywhere visible from the calling code, just in case one of them adds

extension S {
    func boo() { ... }
}

When you expand your macro manually, the individual expansions aren't any more/less expensive — but you are only expanding the individual usages you know you need, and eagerly, instead of waiting for a usage of Duplicate to cause your build to fail, then expanding all macros just in case.

That does seem feasible, and I can't think of an obvious drawback that other types of macros don't already have, but someone with knowledge of the macro system would need to weigh in here.

Eh, I guess I would still say okay, go ahead and expand them all. I'd rather the compiler do the work than me.