This feature looks like a good addition, and would be necessary to create macros that can reproduce the behavior of builtins like #file and #line. There are some build system interactions I'd like to understand better, though.
My understanding right now regarding macros and dependencies is the following. Imagine you have the following:
- module M declares a macro implemented in MP
- module A imports M and uses the macro
- module B imports A, but not M
The plugin MP must be passed to the compiler when building module M and module A, for obvious reasons. The plugin does not need to be passed when building module B, because A's serialized module contains the result of already expanding the macro. As far as B is concerned, that macro may as well have never been there.
This appears to be true when emitting the textual .swiftinterface file for a module that uses a macro as well: the macro is expanded in the output. (I did observe a bug involving macros and inlinable functions, so I'm not sure what the correct behavior is supposed to be in that case.)
Expand for example source and swiftinterface snippets
If I write this source code:
import Observation
@Observable public class Foo {
public var x: Int = 0
public init() {}
}
then my textual interface shows the expansion:
public class Foo {
public var x: Swift.Int {
get
set
}
public init()
@objc deinit
}
extension lib.Foo : Observation.Observable {
}
This property is helpful for pruning dependency graphs, because it means we don't end up in a situation where all the macro plugins used anywhere in your dependency graph must also be passed to every compiler invocation higher up in the graph. The build system only needs to pass the plugins that belong to the libraries you directly depend on.
The feature proposed here, by necessity, breaks this property. Since a default argument expression must be expanded at each caller, then the serialized module or textual interface can only contain a reference to the macro. This means that it's not sufficient to only pass the macro plugin to the compiler when compiling that module; you must also pass it to the compiler when compiling any module that imports it. If you take the example from the top of the post and change A's usage of the macro to a default argument expression, then the plugin must also be passed when compiling module B.
So what I'd like to understand is, is it sufficient that the macro plugin only needs to be passed to the module declaring the macro, then one using it as a default argument, and then any module that directly imports that module? Or are there situations where this could recursively expand further?
I don't think any of this is cause to hold up the feature, because I don't think it's a flaw (I can't imagine how the feature would work any other way); I just want to better understand the consequences for build systems that will eventually need to support this. It's opening a small leak in what has so far been a relatively simple build model.