A lot of folks have been experimenting with macros (thank you!), and recent discussions have surfaced the observation that:
The conformance macro role is not useful on its own; it's pretty much always used in combination with the member macro role to implement the conformance it adds.
The extension capability that the conformance macro role has today is useful independent of conformances, because extensions have a number of semantic implications compared to code in a primary type or protocol declaration.
I've written up a quick pitch to generalize the conformance macro role as an extension macro role. An extension macro is an attached macro that can add an extension of the type it's attached to. You can read the proposal draft here.
This seems like a really nice improvement! One thing I would still like to see that I don't think is covered by this solution is being able to emit the extension inside of a compiler conditional. One of the first things I attempted to write with macros was some DI helper code like:
#if DEBUG
extension SomeType: SomeProtocol {}
public let getSomeType = // get the potentially mocked version
#else
public let getSomeType = // just return the real instance
#endif
And I was surprised I couldn't emit the extension with the other decls. Maybe this would be better served still by loosening the peer macro, but I could also see it being provided by this one if the return value was looser somehow.
This looks like a very welcome improvement of the macro system.
I just wanted to point out that another crucial aspect of extensions is that they can be declared on a type without modifying the declaration of the type (or even having access to the source code of the type, for that matter).
Given this powerful and prevalent use case of extensions, wouldn't it make sense to be able to attach extension macros to a type retroactively, instead of requiring access to the declaration of the type?
I'm all in favor on this pitch and its motivation section.
There is one mystery I don't understand. How those new extension macros (as well as the existing conformance macros) can generate syntactically correct extensions, given extensions are only allowed at the top level of a file?
enum Context {
@MyMacro struct Nested { }
// Wrong macro-generated code:
// ❌ Declaration is only valid at file scope
extension Nested: MyProtocol { ... }
}
func myFunc() {
@MyMacro struct Private { }
// Wrong macro-generated code
// ❌ Declaration is only valid at file scope
extension Private: MyProtocol { ... }
}
The correct forms need information that the macro does not have. In the case of nested types, we need the syntax node for file-level declarations (where the extension should be added), as well as the "path" to the extended type. And in the case of types declared inside a function, we need to know we're inside a function:
enum Context {
struct Nested { }
}
// <- macro does not know the syntactic node for file-level declarations
extension Context.Nested: MyProtocol { ... }
// ~~~~~~~ macro does not know the "path"
func myFunc() {
// <- macro does not know we're inside a function
struct Private: MyProtocol { ... }
}
Did I just list known cases that the macro system already handles well due to a special flavor of extensions that is not exposed at the language surface (we can't use them when we type code)?
Ah good question, the compiler already handles this with conformance macros. Notice that the macro does not provide the name of the type it extends (both with conformance macros and with extension macros in this pitch) because the compiler binds the extension directly without doing name lookup of the extended type. The compiler also inserts the extension into the correct scope, not the potentially-nested scope where the macro is applied.
I think this will need another pitch, but I find this very interesting as well. Some GRDB users are happy (1, 2) to retroactively conform their models to database record protocols, and thus I see a use case for such a pitch. Such "retroactive macros" will probably not be able to access the inner guts of the attached type, so they will be limited. Are we sure the freestanding macros aren't the correct tool for such a job?
Yeah, not being able to see the guts of the type is an expected limitation for extension-writing, even if it's done manually, so I don't think anyone's going to be upset about that. Also, a making an extension macro a declaration macro instead of an attached macro makes perfect sense, especially in light of the potential to have structural extensions in the future, where there is no one obvious type to extend.
The significant compiler rework may be a limiting factor, though. But given how powerful and useful retroactive conformances are, the compiler rework may end up being justified.
I think generalizing conformance macros to extension macros is a good idea, but it should also be possible to attach attributes and modifiers to the extension. Attributes in particular are crucial for declaring conformances with partial availability, a feature ConformanceMacro cannot currently support.
Better yet, the tuple should be replaced by a nominal type that would allow these kinds of changes in the future as new features are added to the language. The other expansion methods all return a complete syntax node per item, which means they effortlessly support future language features. But conformance/extension macros instead return a tuple of constituents of a syntax node; expanding that tuple will cause a source compatibility break. There are good reasons not to just return an ExtensionDeclSyntax, but that means we need to think about other ways to provide the flexibility it needs.
(Alternatively, just pass a TypeSyntax node to the expansion method and require it to return only ExtensionDeclSyntax nodes using that exact type syntax.)
Yeah, I agree with your points and I think this is the best approach. ExtensionDeclSyntax is already the type we have to describe extensions, including attached attributes, and we can verify that the resulting extension uses the right TypeSyntax for the extended type (and still bind the extension directly on the compiler side). I'll update the pitch, thanks!
Possibly. I could imagine a future addition to @attached(extension) that allows you to name the extended type in the macro attribute, e.g. @attached(extension, of: MyType), which defaults to the type the macro is attached to.
I'm not sure I understand the use case here. If you're adding functionality to a type outside of the primary type declaration, you're already writing an extension of that type. Why would you want to attach an extension macro that adds in another extension, instead of a macro that fills in the contents of an extension you already wrote?
No, that's not what I meant, sorry for the confusion. I merely meant that, as proposed, an extension macro can be used by attaching it to the primary type declaration:
@MyMacro
struct MyModel {
// ...
}
This means that only the author of MyModel can use extension-generating macros.
What I was suggesting was the ability to use an extension-generating macro without having access to the primary declaration of the type (syntax bikesheddable):
@MyMacro
extension MyModel { /* filled in by MyMacro */ }
It's crucial that the compiler understands what type is being extended prior to macro expansion. That means it either needs to be specified in the macro attribute with the extension role, in which case it'd be fixed, or it must be the type that the macro is attached to. It cannot be specified by arbitrary macro arguments, because the compiler cannot know what the macro does with those arguments without expanding the macro. For your suggested syntax to work, we would need to invent a new way for a macro role attribute to identify type parameters that are used in the macro parameter list. But personally, I think this use case is better off using an attached macro on an explicit extension. In any case, I think this is out of scope for this pitch.
Oh, that's what you meant! Sorry for the confusion! Even though it feels awkward to declare an extension with empty braces just so the macro can fill them in, it's still a reasonable approach, given the limitations you mentioned about the compiler having to know what is being declared.
I'm assuming, with this proposal, the macro will be able to fill in the extension and add a protocol conformance clause with a single role, right?
EDIT:
Would it be possible/practical to be able to omit the braces on an extension if it has an attached macro on it?
The only request I'd like to make for improvement is to take @beccadax's suggestion to have the macro implementation return the ExtensionDeclSyntax directly. Conformance macros were the only one that tried to return a tuple containing several different syntactic pieces, rather than a whole declaration/expression/statement/code-item or list thereof, and it became very limiting.
We might need to ban extension macros from being applied to local types, because we don't have a notion of extensions on local types and wouldn't be able to "expand" the macros to produce valid Swift code.
I was unclear. We could implement this in the compiler.
However, because it cannot currently be expressed in the base language (there is no syntax to extend a local type), I do not think we should allow macros to do it because it would mean that macros can do something, yet you couldn't expand the macro-generated code and still have it work.