Skip to content

Commit 49f5b77

Browse files
[SE-0415] Narrow scope down to just "body" macros. (#2433)
* [SE-0415] Narrow scope down to just "body" macros. * Update proposals/0415-function-body-macros.md Co-authored-by: Remy Demarest <[email protected]> --------- Co-authored-by: Remy Demarest <[email protected]>
1 parent 27df778 commit 49f5b77

File tree

1 file changed

+21
-140
lines changed

1 file changed

+21
-140
lines changed

proposals/0415-function-body-macros.md

Lines changed: 21 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Authors: [Doug Gregor](https://github.com/DougGregor)
55
* Review Manager: [Tony Allevato](https://github.com/allevato)
66
* Status: **Returned for revision**
7-
* Implementation: [Pull request](https://github.com/apple/swift/pull/70034)
7+
* Implementation: Available on `main` behind the flag `-enable-experimental-feature BodyMacros`
88
* Feature Flag: `BodyMacros`
99
* Review: [pitch](https://forums.swift.org/t/function-body-macros/66471), [review](https://forums.swift.org/t/se-0415-function-body-macros/68847), [returned for revision](https://forums.swift.org/t/returned-for-revision-se-0415-function-body-macros/69114)
1010

@@ -17,13 +17,11 @@
1717
* [Implementing function body macros](#implementing-function-body-macros)
1818
* [Composing function body macros](#composing-function-body-macros)
1919
* [Type checking of functions involving function body macros](#type-checking-of-functions-involving-function-body-macros)
20-
* [Function body macros and implicit returns](#function-body-macros-and-implicit-returns)
2120
* [Source compatibility](#source-compatibility)
2221
* [Effect on ABI stability](#effect-on-abi-stability)
2322
* [Effect on API resilience](#effect-on-api-resilience)
2423
* [Future directions](#future-directions)
2524
* [Function body macros on closures](#function-body-macros-on-closures)
26-
2725
* [Alternatives considered](#alternatives-considered)
2826
* [Eliminating preamble macros](#eliminating-preamble-macros)
2927
* [Capturing the withSpan pattern in another macro role](#capturing-the-withspan-pattern-in-another-macro-role)
@@ -123,12 +121,12 @@ When using the shorthand syntax for get-only properties, a function body macro c
123121

124122
### Declaring function body macros
125123

126-
Function body macros are declared with the `body` or `preamble` role, which indicate that they can be attached to any kind of function, and can produce the contents of a function body. For example, here are declarations for the macros used above:
124+
Function body macros are declared with the `body` role, which indicate that they can be attached to any kind of function, and can produce the contents of a function body. For example, here are declarations for the macros used above:
127125

128126
```swift
129127
@attached(body) macro Remote() = #externalMacro(...)
130128

131-
@attached(preamble) macro Logged() = #externalMacro(...)
129+
@attached(body) macro Logged() = #externalMacro(...)
132130

133131
@attached(body) macro AssumeMainActor() = #externalMacro(...)
134132
```
@@ -158,99 +156,10 @@ public protocol BodyMacro: AttachedMacro {
158156

159157
That function may have a function body, which will be replaced by the code items produced from the macro implementation.
160158

161-
Preamble macros are implemented with a type that conforms to the `PreambleMacro` protocol:
162-
163-
```swift
164-
/// Describes a macro that can introduce "preamble" code into an existing
165-
/// function body.
166-
public protocol PreambleMacro: AttachedMacro {
167-
/// Expand a macro described by the given custom attribute and
168-
/// attached to the given declaration and evaluated within a
169-
/// particular expansion context.
170-
///
171-
/// The macro expansion can introduce code items that form a preamble to
172-
/// the body of the given function. The code items produced by this macro
173-
/// expansion will be inserted at the beginning of the function body.
174-
static func expansion(
175-
of node: AttributeSyntax,
176-
providingPreambleFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
177-
in context: some MacroExpansionContext
178-
) throws -> [CodeBlockItemSyntax]
179-
}
180-
```
181-
182-
Preamble macros don't provide a complete function body. Rather, they provide code items that will be introduced at the beginning of the existing function body. Preamble macros are useful for injecting code without changing anything else about the function body, for example to introduce logging or simple tracing facilities. The `defer` statement can be used to trigger code that will run once the function returns.
183-
184159
### Composing function body macros
185160

186161
At most one `body` macro can be applied to a given function. It receives the function declaration to which it is attached as it was written in the source code and produces a new function body.
187162

188-
Any number of `preamble` macros can be applied to a given function. Each sees the function declaration to which it is attached as it was written in the source code, and produces code items that will be introduced at the beginning of the function body. The code items produced by the `preamble` macros will be applied in the order that the preamble macro attributes are applied in the source.
189-
190-
For example, given:
191-
192-
```swift
193-
@A @B @C func f() { print("hello") }
194-
```
195-
196-
Where `A` and `B` are preamble macros and `C` is a function body macro, each macro will see the same declaration of `f`. The resulting function body for `f` will involve the code items produced by all three macros as follows:
197-
198-
```swift
199-
{
200-
// code items produced by preamble macro A
201-
// code items produced by preamble macro B
202-
// code items produced by function body macro C
203-
}
204-
```
205-
206-
Preamble macros may introduce new local declarations, which will be visible to later preamble macros (e.g., names introduced by `@A` will be visible in `@B` and `@C` as well) and the remainder of the function body. As with all macros that introduce new names into existing code, those names must be documented with a [`names` clause](https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md#specifying-newly-introduced-names). For example, a tracing macro for [swift-distributed-tracing](https://swiftpackageindex.com/apple/swift-distributed-tracing) might want to introduce a local variable `span` that can be used by the rest of the function body to add more information to the trace. Such a macro could be declared as a preamble macro as follows:
207-
208-
```swift
209-
@attached(preamble, names: named(span))
210-
macro Traced(_ name: String? = nil) = #externalMacro(...)
211-
```
212-
213-
The `Traced` macro could introduce a `span` local variable that can be used by the function body. For example:
214-
215-
```swift
216-
@Traced("Prepare dinner")
217-
func prepareDinner(guests: Int) async throws -> Meal {
218-
span.attributes["operation"] = "Making dinner"
219-
// ...
220-
}
221-
```
222-
223-
The `@Traced` preamble macro could expand this into:
224-
225-
```swift
226-
func prepareDinner(guests: Int) async throws -> Meal {
227-
// from "Traced" macro
228-
let span = DistributedTracing._getCurrentSpan()
229-
DistributedTracing._pushSpan()
230-
defer {
231-
DistributedTracing._popSpan()
232-
}
233-
234-
// Existing code
235-
span.attributes["operation"] = "Making dinner"
236-
// ...
237-
}
238-
```
239-
240-
It is possible that the newly-introduced `span` variable could shadow a variable from an outer scope, for example if `prepareDinner` was inside an actor with `span` property:
241-
242-
```swift
243-
actor Chef {
244-
var span: Int
245-
246-
@Traced("Prepare dinner")
247-
func prepareDinner(guests: Int) async throws -> Meal {
248-
print("Arm span is \(span) meters") // refers to 'span' from the Traced macro expansion
249-
// ...
250-
}
251-
}
252-
```
253-
254163
### Type checking of functions involving function body macros
255164

256165
When a function body macro is applied, the macro-expanded function body will need to be type checked when it is incorporated into the program. However, the function might already have a body that was written by the developer, which can be inspected by the macro implementation. The function body as written must be syntactically well-formed (i.e., it must conform to the [Swift grammar](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/summaryofthegrammar/)) but will *not* be type-checked, so it need not be semantically well-formed.
@@ -281,42 +190,9 @@ func employees(hiredIn year: Int) -> [String] {
281190

282191
The requirement for syntactic wellformedness should help rein in the more outlandish uses of function body macros, as well as making sure that existing tools that operate on source code will continue to work well even in the presence of body macros.
283192

284-
### Function body macros and implicit returns
285-
286-
Swift functions that contain only a single expression will implicitly return that expression, e.g.,
287-
288-
```swift
289-
func g(a: Int, b: Int) -> Int {
290-
a + b // return is implicit
291-
}
292-
```
293-
294-
The application of preamble macros to the function body does not affect the implicit return, so the following is well-formed
295-
296-
```swift
297-
@Logged
298-
func g(a: Int, b: Int) -> Int {
299-
a + b // return is implicit
300-
}
301-
```
302-
303-
and the expansion of the macro is treated like the following code:
304-
305-
```swift
306-
func g(a: Int, b: Int) -> Int {
307-
log("Entering g(a: \(a), b: \(b))")
308-
defer {
309-
log("Exiting g")
310-
}
311-
return a + b
312-
}
313-
```
314-
315-
Body macros are different: because they replace the entire function body, whether the original function body as written would have had an implicit return is not considered.
316-
317193
## Source compatibility
318194

319-
Function body macros introduce new macro roles into the existing attached macro syntax, and therefore do not have an impact on source compatibility.
195+
Function body macros introduce a new macro role into the existing attached macro syntax, and therefore does not have an impact on source compatibility.
320196

321197
## Effect on ABI stability
322198

@@ -338,7 +214,7 @@ Function body macros as presented in this proposal are limited to declared funct
338214
}
339215
```
340216

341-
This extension would involve extending the `PreambleMacro` and `BodyMacro` protocols with another `expansion` method that accepts closure syntax. The primary challenge with applying function body macros to closures is the interaction with type inference, because closures generally occur within an expression and some of the macro arguments themselves might be part of the expression. In the example above, the `z` value could come from an outer scope and be the subject of type inference:
217+
This extension would involve extending the `BodyMacro` protocol with another `expansion` method that accepts closure syntax. The primary challenge with applying function body macros to closures is the interaction with type inference, because closures generally occur within an expression and some of the macro arguments themselves might be part of the expression. In the example above, the `z` value could come from an outer scope and be the subject of type inference:
342218

343219
```swift
344220
f(0) { z in
@@ -350,21 +226,23 @@ f(0) { z in
350226

351227
Macros are designed to avoid [multiply instantiating the same macro](https://github.com/apple/swift-evolution/blob/main/proposals/0382-expression-macros.md#macro-expansion), and have existing limitations in place to prevent the type checker from getting into a position where it is not obvious which macro to expand or the same macro needs to be expanded multiple times. To extend function body macros to closures will require a solution to this type-checking issue, and might be paired with lifting other restrictions on (e.g.) freestanding declaration macros.
352228

353-
## Alternatives considered
229+
### Preamble macros
354230

355-
### Eliminating preamble macros
356-
357-
Preamble macros aren't technically necessary, because one could always write a function body macro that injects the preamble code into an existing body. However, preamble macros provide several end-user benefits over function body macros for the cases where they apply:
231+
The first reviewed revision of this proposal contained *preamble* macros, which let a macro introduce code at the beginning of a function without changing the rest of the function body. Preamble macros aren't technically necessary, because one could always write a function body macro that injects the preamble code into an existing body. However, preamble macros provide several end-user benefits over function body macros for the cases where they apply:
358232

359233
* Preamble macros can be composed, whereas function body macros cannot.
360234
* Preamble macros don't change the code as written by the user, so they provide a better user experience (e.g., for diagnostics, code completion, and so on).
361235

362-
### Capturing the `withSpan` pattern in another macro role
236+
Preamble macros would be expressed as its own attached macro role (`preamble`), implemented with a type that conforms to the `PreambleMacro` protocol. Details are available in [the prior revision](https://github.com/apple/swift-evolution/blob/f1b9da80315578666352a7d6d40a9f6cc936f69a/proposals/0415-function-body-macros.md).
237+
238+
Preamble macros have been moved out to Future Directions because they represent a possible future, but not an obviously right one: preamble macros might not add sufficient expressivity to cover the cost of the complexity they introduce, and another kind of macro (like the "wrapper" macro below) might provide a more reasonable tradeoff between expressivity and complexity.
363239

364-
An alternative formulation of the `Traced` macro (call it `@TracedBody`) makes use of its existing [`withSpan` API](https://swiftpackageindex.com/apple/swift-distributed-tracing/1.0.1/documentation/tracing) in a `body` macro, such that a function such as:
240+
### Wrapper macros
241+
242+
A number of use cases for body macros involve "wrapping" the existing body in additional logic. For example, consider an alternative formulation of the `Traced` macro (let's call it `@TracedWithSpan`) could make use of the [`withSpan` API](https://swiftpackageindex.com/apple/swift-distributed-tracing/1.0.1/documentation/tracing) such that a function such as:
365243

366244
```swift
367-
@TracedBody("Doing complicated math")
245+
@TracedWithSpan("Doing complicated math")
368246
func h(a: Int, b: Int) -> Int {
369247
return a + b
370248
}
@@ -380,7 +258,7 @@ func h(a: Int, b: Int) -> Int {
380258
}
381259
```
382260

383-
The `withSpan` function used here is one instance of a fairly general pattern in Swift, where a `with<something>` function accepts a closure argument and runs it with some extra contextual parameters. As we did with the `preamble` macro role, we could introduce a special macro role that describes this pattern: the macro would not see the function body that was written by the developer at all, but would instead have a function value representing the body that it could call opaquely. For example, the `TracedBody` example function `h` would expand to:
261+
This `withSpan` function used here is one instance of a fairly general pattern in Swift, where a function accepts a closure argument and runs it with some extra contextual parameters. As we with the `preamble` macro role mentioned above, we could introduce a special macro role that describes this pattern: the macro would not see the function body that was written by the developer at all, but would instead have a function value representing the body that it could call opaquely. For example, the `TracedWithSpan` example function `h` would expand to:
384262

385263
```swift
386264
func h(a: Int, b: Int) -> Int {
@@ -391,7 +269,7 @@ func h(a: Int, b: Int) -> Int {
391269
With this approach, the original function body for `h` would be type-checked prior to macro expansion, and then would be handed off to the macro as an opaque value `h-impl` to be called by `withSpan`. The macro could introduce its own closure wrapping that body as needed, e.g.,
392270

393271
```swift
394-
@TracedBody("Doing complicated math", { span in
272+
@TracedWithSpan("Doing complicated math", { span in
395273
span.attributes["operation"] = "addition"
396274
})
397275
func myMath(a: Int, b: Int) -> Int {
@@ -410,9 +288,9 @@ func myMath(a: Int, b: Int) -> Int {
410288
}
411289
```
412290

413-
The advantage of this approach over allowing a `body` macro to replace a body is that we can type-check the function body as it was written, and only need to do so once---then it becomes a value of function type that's passed along to the underying macro. Also like preamble macros, this approach can compose, because the result of one macro could produce another value of function type that can be passed along to another macro.
291+
The advantage of this approach over allowing a `body` macro to replace a body is that we can type-check the function body as it was written, and only need to do so once---then it becomes a value of function type that's passed along to the underying macro. Also like preamble macros, this approach can compose, because the result of one macro could produce another value of function type that can be passed along to another macro. [Python decorators](https://www.datacamp.com/tutorial/decorators-python) have been successful in that language for customizing the behavior of functions in a similar manner.
414292

415-
On the other hand, having a third kind of macro role for function body macros adds yet more language complexity, and introducing this role in lieu of allowing function body macros to replace an existing function body might be overfitting to today's use cases. Moreover, this only works for very specific examples. The `@AssumeMainActor` macro would not be able to use this feature, because the intent is that the code as written (in a `nonisolated` function) is processed in a different actor isolation context (the `@MainActor` closure).
293+
## Alternatives considered
416294

417295
### Type-checking bodies as they were written
418296

@@ -422,6 +300,9 @@ On the other hand, type-checking the function bodies before macro expansion has
422300

423301
## Revision history
424302

303+
* Revision 3:
304+
* Narrowed the focus down to `body` macros.
305+
* Moved preamble macros into Future Directions, added discussion of wrapper macros.
425306
* Revision 2:
426307
* Clarify that preamble macro-introduced local names can shadow names from outer scopes
427308
* Clarify the effect of function body macros on single-expression functions and implicit returns

0 commit comments

Comments
 (0)