I don't think the third option is really feasible, because it means that macro implementations would become much more deeply entwined in incremental compilation. And I worry that the first option is an anti-feature: one errant macro could blow up your incremental build times.
I suspect the middle bullet might lead toward the right answer. From an incremental build standpoint, if any if your parents changes directly---say, you're in a function that adds a new parameter, or an extension that gets a new conditional requirement---you're going to have to get recompiled no matter what. So what if we kept the "spine" on the parent nodes in tact, but removed all child nodes that could be independent? So if we started with this code:
struct A<T> { }
extension A where T: Addable {
func f() { }
func g(a: Int, b: Int) {
let a = 1
let b = #myMacro + a
}
}
we would give the implementation of myMacro
something like this:
extension A where T: Addable {
func g(a: Int, b: Int) {
#myMacro
}
}
this would let you ask syntactic questions about the parents of your macro expansion, without exposing parts of the source file that should be able to evolve independently of the code seen by the macro implementation. Now, this isn't going to solve all of the problems you enumerated. For example, you won't know whether A
is a class, struct, protocol, or whatever, and you won't know whether self
is accessible at all, but it does provide quite a bit of context.
I went ahead and write up a pitch for declaration macros so we can explore further down the path and, hopefully, learn more. That said, the Language Working Group (and in the past, the Core Team) has been clear that for large features that are split across several different proposals (like this one), it's absolutely willing to go back and review amendments to accepted proposals if we learn more while considering later proposals. We did this with concurrency two years back, and regular expressions last year, and will do the same for macros.
Yes, I think we can automate away a lot of the boilerplate checking of macro arguments. However, it won't be done by the compiler, because the compiler isn't going to be able to form calls to arbitrary functions like this when invoking the macro. Instead, I would view this as some improvements we can make to the SwiftSyntaxMacros
library itself. For example:
protocol SingleArgumentExpressionMacro: ExpressionMacro {
associatedtype ArgumentSyntax: ExprSyntaxProtocol
static func expansion(of node: MacroExpansionExprSyntax, in context: inout MacroExpansionContext,
argument: ArgumentType) throws -> ExprSyntax
}
extension ExpressionMacro where Self: SingleArgumentExpressionMacro {
static func expansion(of node: MacroExpansionExprSyntax, in context: inout MacroExpansionContext)
throws -> ExprSyntax {
guard let arg = node.argumentList.first?.element?.as(ArgumentSyntax.self) else {
throw BadArgumentError()
}
return try expansion(of: node, in: context, argument: arg)
}
}
If we had associated type packs, we could make this handle an arbitrary number of arguments, which would be... amazing. Anyway, I think this is a good direction, and it's something we should pursue within SwiftSyntaxMacros
. However, I don't think these improvements should be part of this proposal, or even part of the evolution process... we should treat it like any other API improvements and extensions to swift-syntax, through pull requests and discussion there.
I suppose it depends on what you want to do with the type. You could declare some local types, form a result from that, and have jsonObj
return an any <some protocol>
to hide the returned type. If you want to actually create types that are usable elsewhere and aren't erased, you'll need something more like my follow-up pitch for declaration macros.
This, again, might be something that would need declaration macros, if the goal is to have the macro parse the string and produce new type information for it. Alternatively, and I suppose I could cover this as a future direction, I could imagine an annotation for a macro parameter that says "arguments to this parameter are not type checked at all". Arguments to such parameters would lose all of the benefits discussed in the section on type-checking arguments, but it would let you form more-specific type information for use in checking those arguments later. I'm not sure how I feel about such a feature, but it is an extension that could fit into the model.
Neat! Thank you.
I'm going to backtrack a bit on my earlier enthusiasm for this change. Macros are so much more expression-like that allowing one to omit the ()
for macros, when it has a different behavior for functions, that introducing an inconsistency here feels worse than having macro authors decide between :
and ->
.
Doug