Not effortlessly supporting composition (multiple such macros on a single function) seems like a major limitation to me. Take two of the given examples - @Logged and @Traced - that could very well be desired simultaneously. Along with countless other common wrapper patterns from Python, Java, etc, like @Backoff, @Register, @ReportExceptions, @Cache, @AuthRequired, etc.
Would it be better to tackle these cases separately from actual body generation as in the @Remote example?
If only one body generator were permitted at a time - and it took only a function declaration, not a definition - then that greatly simplifies that use-case. Being able to have an arbitrary number of nested wrappers can deal with most others. And the two compose perfectly well together.
Actually mutating the body is a third, orthogonal operation, I think. It raises way more and way more complicated questions regarding type-checking and all the rest that have been touched on already in this thread. (and I say that not as a compiler or macro author so much as a user, that has to have some intuition and comprehension of how the macros I use will actually behave)
I get the appeal of having fewer, more universal tools, but from the design of Swift macros so far it seems like there's no overwhelming desire to have few macro types - if there were, it'd inevitably trend towards a single type of macro which can arbitrarily modify the entire program's AST. I appreciate the benefits of having distinct types of macros that elegantly solve specific subsets of the compile-time code generation / mutation space.
Plus, nothing prevents a "one function modifier macro type to rule them all" from being added later and superseding prior, more focused macro types. If and when all the challenges around it are figured out (and perhaps, only after seeing that more narrowly-purposed macro types aren't sufficient in practice).
I donât see any reason why that shouldnât work out-of-the box. Getting the type of a variable uses the sourcekitd cursorinfo request which is backed by the compiler and shared between Xcode and SourceKit-LSP, so if it works in one, it should work in the other.
Other than that, to see the actual macro expansion, you can currently replace a macro by its expanded source using the âInline Macroâ refactoring. Thereâs on open SourceKit-LSP issue to also show the expanded code without modifying the source but thatâs not specific to function body macros.
Wait, could this generalize the result builder transform then, since it can be applied to properties and computed gets? Would you be able to apply it like this?
public func takesCloure<T>(@MacroBuilder _ someClosure: () -> T)
I do not feel that this is a good direction for macros. Part of the design philosophy of macros is that you can see the macro where the expansion occurs---whether via the @ or # syntax. Putting a macro on a function parameter and having that macro apply implicitly at the call site doesn't match that philosophy.
Perhaps another more focused macro role would help with type checking: body wrapper macros. A lot of function macros simply wrap the content of body in another function with a closure. Thus, the expansion could be similar to a freestanding macro accepting a closure.
// bodyWrapper macros expect the final argument to be a closure.
@attached(bodyWrapper)
macro Traced<T>(_ name: String? = nil, body: (_ span: Span) -> T) = #externalMacro(...)
// === Example --------
@Traced("Doing complicated math", spanName: "span")
func myMath(a: Int, b: Int) -> Int {
span.attributes["operation"] = "addition" // note: would not type-check by itself
return a + b
}
// Expands to:
func myMath(a: Int, b: Int) -> Int {
// NOTE: $body is type-checked *once*
let $body = { (span: Span) -> Int in
span.attributes["operation"] = "addition" // note: would not type-check by itself
return a + b
}
// Code expansion from `@Traced`
withSpan("Doing complicated math", $body)
}
Thanks for the clarification, I was confused about what "pattern" the proposal referred to. Still, the disadvantage of introducing this macro rule seems limited; would it not be a suitable to have it as a future direction?
About "Eliminating preamble macros": they could be expressed with the same withSomething(...) { ... } pattern (let's call it "wrapper").
Why do you think implementing "wrapper" as a kind of macro is better than doing it in the same way as result builders do?
We can have parametrized and static "wrappers". We can provide function signature and arguments to them. We can compose them the same way property wrappers compose (i.e. "nesting"). Type-checking works as usual.
This sounds great, especially in the form suggested in the alternatives section: that is, if preamble macros were replaced with âwrapperâ macros (maybe we could borrow from Python and call them âdecoratorâ macros?)
Besides what others have said, it sounds like implementing the decorator style could remove the need for preamble macros entirely â itâs very possible Iâm missing something but arenât the latter effectively a subset of the former? Seems to me that if users wanted to add a preamble they could return { preamble; return h-impl }()
In summary I think the ideal state would be keeping two macro types from this proposal: decorator (canât rewrite the body but can compose) and body (can rewrite, canât compose).
Expansion of a preamble macro adds new statements to the body. That could change whether an implicit return was allowed. Would that result in an error, or would the compiler insert the return automatically when "splicing" the code?
(It looks like the macro would lack the context and ability to insert the return itself.)
e.g. If the author writes:
@Logged
func g(a: Int, b: Int) -> Int {
a + b
}
The output would need to be something like
func g(a: Int, b: Int) -> Int {
// Start of macro expansionâŚ
log("Entering g(a: \(a), b: \(b))")
defer {
log("Exiting g")
}
// ⌠End of macro expansion
return a + b
// ^^^^^ Original body
//^^^^^^^ Additional explicit `return`
}
And a preambule macro can introduce other statements that can break the code with or without compilation errors. For example a new variable(which should be prohibited IMO) with the same name as a function parameter or a local or outer scope variable.
struct Foo {
var bar: String = "Hello"
@BadMacro
func sayToWorld() {
print(bar + " World")
}
// expands to
// func sayToWorld() {
// let bar = "Goodbye"
// print(bar + " World")
// }
}
I need exactly this kind of macros, very welcome pitch!
Instead of (UPDATE: I did not see the reference to swift-distributed-tracing and thought about a simpler func withSpan(_ title: String, function: () -> ())
func h(a: Int, b: Int) -> Int {
withSpan("Doing complicated math") { _ in
return a + b
}
}
shouldnât it be something like
func h(a: Int, b: Int) -> Int {
var result: Int?
withSpan("Doing complicated math") {
let body: () -> Int = {
return a + b
}
result = body()
}
return result!
}
...altough this force-unwrapping is not nice.I would only need it for functions returning nothing or optionals, turning
@Step
func h0(a: Int, b: Int, duringExecution execution: Execution) -> Int? {
return a + b
}
into
func h(a: Int, b: Int, duringExecution execution: Execution) -> Int? {
var result: Int?
// conditional execution with logging:
execution.effectuate(#function,#file) {
let body: () -> Int? = {
return a + b
}
result = body()
}
return result
}
If that's the case that's quite promising to stick to the kind of macros that can introduce names in the "full power, function body replacement" macros I suppose
Function body macros - #21 by wadetregaskis Does bring up good points about composition though... There truly isn't a great answer here huh. Could the body replacement macros declare if they're going to introduce new names or not perhaps? That could help choose the "excludes other body macros" or not expansion style...?
I did not see the reference to swift-distributed-tracing. You are right. I have thought about a simpler func withSpan(_ title: String, function: () -> ()). My fault.