Skip to content

Commit b934013

Browse files
authored
Expression macro as caller side default argument (#2279)
* Create nnnn-caller-side-default-argument-macro-expression.md * Minor formatting changes * Add pitch url * Move allow arbitrary expression to future directions * Switch to an example where. the call will fail to type check * Clarify type checking behavior
1 parent 19b5874 commit b934013

File tree

1 file changed

+198
-0
lines changed

1 file changed

+198
-0
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Expression macro as caller-side default argument
2+
3+
* Proposal: SE-NNNN
4+
* Authors: [Apollo Zhu](https://github.com/ApolloZhu)
5+
* Review Manager: TBD
6+
* Status: **Awaiting review**
7+
* Implementation: https://github.com/ApolloZhu/swift/tree/macro/expression-as-default-argument
8+
* Upcoming Feature Flag: `ExpressionMacroDefaultArguments`
9+
* Review: ([pitch](https://forums.swift.org/t/pitch-expression-macro-as-caller-side-default-argument/69019))
10+
11+
## Introduction
12+
13+
This proposal aims to lift the restriction afore set in [SE-0382 "Expression macros"](https://github.com/apple/swift-evolution/blob/main/proposals/0382-expression-macros.md) to allow non-built-in expression macros as caller-side default argument expressions.
14+
15+
## Motivation
16+
17+
Built-in magic identifiers like [#line](https://developer.apple.com/documentation/swift/line()) and [#fileID](https://developer.apple.com/documentation/swift/fileID()) are documented as expression macros in the official documentation, but if Swift developers try to implement a similar macro themselves and use it as the default argument for some function, the code will not compile:
18+
19+
```swift
20+
public struct MakeLabeledPrinterMacro: ExpressionMacro {
21+
public static func expansion(
22+
of node: some FreestandingMacroExpansionSyntax,
23+
in context: some MacroExpansionContext
24+
) throws -> ExprSyntax {
25+
return "{ value in print(\"\\(#fileID):\\(#line): \\(value)\") }"
26+
}
27+
}
28+
29+
public macro LabeledPrinter<T>() -> (T) -> Void
30+
= #externalMacro(module: ..., type: "MakeLabeledPrinterMacro")
31+
32+
public func greet<T>(
33+
_ thing: T,
34+
print: (T) -> Void = #LabeledPrinter
35+
// error: ^ non-built-in macro cannot be used as default argument
36+
) {
37+
print("Hello, \(thing)")
38+
}
39+
```
40+
41+
This is because built-in expression macros/magic identifiers have a special behavior: when used as default arguments, instead of been expanded at where the expressions are written like all other macros, they are expanded by the caller using the source-location information of the call site:
42+
43+
```swift
44+
// in MyLibrary.swift
45+
public func greet<T>(_ thing: T, file: String = #fileID) {
46+
print("\(fileID): Hello, \(thing)"
47+
}
48+
49+
// in main.swift
50+
greet("World")
51+
// prints "main.swift: Hello, World" instead of "MyLibrary.swift: ...
52+
```
53+
54+
This a useful existing behavior that should be supported, but could be surprising as it differs from all other macro expansions, and might not be desired for all expression macros.
55+
56+
## Proposed solution
57+
58+
The proposal lifts the restriction and makes non-built-in expression macros behave consistently as built-in magic identifier expression macros:
59+
60+
* if expression macros are used as default arguments, they’ll be expanded with caller side source location information and context;
61+
* if they are used as sub-expressions of default arguments, they’ll be expanded at where they are written
62+
63+
```swift
64+
// in MyLibrary.swift =======
65+
@freestanding(expression)
66+
macro MyFileID<T: ExpressibleByStringLiteral>() -> T = ...
67+
68+
public func callSiteFile(_ file: String = #MyFileID) { file }
69+
70+
public func declarationSiteFile(_ file: String = (#MyFileID)) { file }
71+
72+
public func alsoDeclarationSiteFile(
73+
file: String = callSiteFile(#MyFileID)
74+
) { file }
75+
76+
// in main.swift ============
77+
print(callSiteFile()) // print main.swift, the current file
78+
print(declarationSiteFile()) // always prints MyLibrary.swift
79+
print(alsoDeclarationSiteFile()) // always prints MyLibrary.swift
80+
```
81+
82+
Macro author can inquire the source location information using `context.location(of:)` just like before and implement `#fileID`, `#line`, and `#column` as shown below:
83+
84+
```swift
85+
public struct MyFileIDMacro: ExpressionMacro {
86+
public static func expansion(
87+
of node: some FreestandingMacroExpansionSyntax,
88+
in context: some MacroExpansionContext
89+
) -> ExprSyntax {
90+
context.location(
91+
of: node, at: .afterLeadingTrivia, filePathMode: .fileID
92+
)!.file
93+
}
94+
}
95+
96+
public struct MyLineMacro: ExpressionMacro {
97+
public static func expansion(
98+
of node: some FreestandingMacroExpansionSyntax,
99+
in context: some MacroExpansionContext
100+
) -> ExprSyntax {
101+
context.location(of: node)!.line
102+
}
103+
}
104+
105+
public struct MyColumnMacro: ExpressionMacro {
106+
public static func expansion(
107+
of node: some FreestandingMacroExpansionSyntax,
108+
in context: some MacroExpansionContext
109+
) -> ExprSyntax {
110+
context.location(of: node)!.column
111+
}
112+
}
113+
```
114+
115+
## Detailed design
116+
117+
### Type-checking default argument macro expressions
118+
119+
Since the macro expanded expression might reference declarations that are not available in the scope where the function is declared, macro expressions are not expanded at the primary function declaration. However, macro expression used as a default argument is type checked without expansion to make sure that
120+
121+
1. it is at least as visible as the function using it,
122+
2. its return type matches what that parameter expects, and
123+
3. its arguments, if any, are literals without string interpolation.
124+
125+
### Type-checking macro expanded expressions
126+
127+
For each call to a function that has an expression macro default argument, the macro will be expanded with each call-site’s source location and type-checked in the corresponding caller-side context, as if the macro expression is written at where it is expanded:
128+
129+
```swift
130+
@freestanding(expression)
131+
// expands to `foo + bar`
132+
public macro VariableReferences() -> String = ...
133+
134+
public func preferVariablesFromCallerSide(
135+
param: String = #VariableReferences
136+
) {
137+
print(param)
138+
}
139+
140+
// in another file ==========
141+
var foo = "hi "
142+
var bar = "caller"
143+
preferVariablesFromCallerSide() // prints: hi caller
144+
// ^ same as #VariableReferences written here
145+
```
146+
147+
## Source compatibility
148+
149+
As non-built-in macro expressions aren’t allowed as default argument, this change is purely additive and has no impact on existing code.
150+
151+
## ABI compatibility
152+
153+
This feature does not affect the ABI.
154+
155+
## Implications on adoption
156+
157+
This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility.
158+
159+
## Future directions
160+
161+
### Allow arguments to default argument macro expressions to be arbitrary expressions
162+
163+
If these arguments can be arbitrary expressions, type-checking the macro expression at function declaration will require any declarations referenced in these expressions to be also in scope:
164+
165+
```swift
166+
@freestanding(expression)
167+
// expands to: "Hello " + string
168+
public macro PrependHello(_ string: String) -> String = ...
169+
170+
// this is needed so it can be referenced in the default argument
171+
public var shadowedVariable: String = "World"
172+
173+
public func preferVariablesFromCallerSide(
174+
param: String = #PrependHello(shadowedVariable)
175+
) {
176+
print(param)
177+
}
178+
```
179+
180+
However, as the expanded expression is type-checked in the caller-side context, it’s rather unintuitive that one must add the public variable in the example above, yet it might not be what the macro expanded expressions use. For example, if there's a variable with the same name in scope on the caller side, that variable will be used, and the call to the function might fail to type-check:
181+
182+
```swift
183+
// in another file ==========
184+
var shadowedVariable: Int = 42
185+
preferVariablesFromCallerSide()
186+
// #PrependHello(shadowedVariable) expands to "Hello " + 42
187+
// error: binary operator '+' cannot be applied to operands of type 'String' and 'Int'
188+
```
189+
190+
## Alternatives considered
191+
192+
### Expand non-built-in expression macro default arguments at the primary declaration
193+
194+
While this allows all macro expansions to be expanded at where they are written, it creates an inconsistency for expression macros where they behave differently depending on whether they are built-in or not. Therefore, this alternative won’t be a solution for addressing the surprising behavior of built-in expression macros as caller-side default arguments, while the proposed solution unifies, and clarifies how to make expression macro default arguments expand at caller-side vs. at function declaration.
195+
196+
## Acknowledgments
197+
198+
Thanks to Doug Gregor, Richard Wei, and Holly Borla for early feedback and suggestions on design and implementation.

0 commit comments

Comments
 (0)