Skip to content

Commit 2d42e8d

Browse files
authored
Merge pull request #1412 from DougGregor/macro-expansion-nonexternal
[Macros] Add API for expanding a macro defined in terms of another macro
2 parents 68056e8 + ba00e61 commit 2d42e8d

File tree

4 files changed

+463
-0
lines changed

4 files changed

+463
-0
lines changed

Sources/SwiftSyntaxMacros/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ add_swift_host_library(SwiftSyntaxMacros
2121

2222
AbstractSourceLocation.swift
2323
BasicMacroExpansionContext.swift
24+
FunctionParameterUtils.swift
2425
MacroExpansionContext.swift
26+
MacroReplacement.swift
2527
MacroSystem.swift
2628
Syntax+MacroEvaluation.swift
2729
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import SwiftSyntax
2+
3+
extension FunctionParameterSyntax {
4+
/// Retrieve the name of the parameter as it is used in source.
5+
///
6+
/// Example:
7+
///
8+
/// func f(a: Int, _ b: Int, c see: Int) { ... }
9+
///
10+
/// The parameter names for these three parameters are `a`, `b`, and `see`,
11+
/// respectively.
12+
var parameterName: TokenSyntax? {
13+
// If there were two names, the second is the parameter name.
14+
if let secondName = secondName {
15+
if secondName.text == "_" {
16+
return nil
17+
}
18+
19+
return secondName
20+
}
21+
22+
if let firstName = firstName {
23+
if firstName.text == "_" {
24+
return nil
25+
}
26+
27+
return firstName
28+
}
29+
30+
return nil
31+
}
32+
}
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import SwiftDiagnostics
2+
import SwiftSyntax
3+
import SwiftSyntaxBuilder
4+
5+
enum MacroExpanderError: DiagnosticMessage {
6+
case undefined
7+
case definitionNotMacroExpansion
8+
case nonParameterReference(TokenSyntax)
9+
case nonLiteralOrParameter(ExprSyntax)
10+
11+
var message: String {
12+
switch self {
13+
case .undefined:
14+
return "macro expansion requires a definition"
15+
16+
case .definitionNotMacroExpansion:
17+
return "macro definition must itself by a macro expansion expression (starting with '#')"
18+
19+
case .nonParameterReference(let name):
20+
return "reference to value '\(name.text)' that is not a macro parameter in expansion"
21+
22+
case .nonLiteralOrParameter:
23+
return "only literals and macro parameters are permitted in expansion"
24+
}
25+
}
26+
27+
var diagnosticID: MessageID {
28+
.init(domain: "SwiftMacros", id: "\(self)")
29+
}
30+
31+
var severity: DiagnosticSeverity {
32+
.error
33+
}
34+
}
35+
36+
/// Provide the definition of a macro
37+
public enum MacroDefinition {
38+
/// An externally-defined macro, known by its type name and the module in
39+
/// which that type resides, which uses the deprecated syntax `A.B`.
40+
case deprecatedExternal(node: Syntax, module: String, type: String)
41+
42+
/// A macro that is defined by expansion of another macro.
43+
///
44+
/// The definition has the macro expansion expression itself, along with
45+
/// sequence of replacements for subtrees that refer to parameters of the
46+
/// defining macro. These subtrees will need to be replaced with the text of
47+
/// the corresponding argument to the macro, which can be accomplished with
48+
/// `MacroDeclSyntax.expandDefinition`.
49+
case expansion(MacroExpansionExprSyntax, replacements: [Replacement])
50+
}
51+
52+
extension MacroDefinition {
53+
/// A replacement that occurs as part of an expanded macro definition.
54+
public struct Replacement {
55+
/// A reference to a parameter as it occurs in the macro expansion expression.
56+
public let reference: IdentifierExprSyntax
57+
58+
/// The index of the parameter in the defining macro.
59+
public let parameterIndex: Int
60+
}
61+
}
62+
63+
fileprivate class ParameterReplacementVisitor: SyntaxAnyVisitor {
64+
let macro: MacroDeclSyntax
65+
var replacements: [MacroDefinition.Replacement] = []
66+
var diagnostics: [Diagnostic] = []
67+
68+
init(macro: MacroDeclSyntax) {
69+
self.macro = macro
70+
super.init(viewMode: .fixedUp)
71+
}
72+
73+
// Integer literals
74+
override func visit(_ node: IntegerLiteralExprSyntax) -> SyntaxVisitorContinueKind {
75+
.visitChildren
76+
}
77+
78+
// Floating point literals
79+
override func visit(_ node: FloatLiteralExprSyntax) -> SyntaxVisitorContinueKind {
80+
.visitChildren
81+
}
82+
83+
// nil literals
84+
override func visit(_ node: NilLiteralExprSyntax) -> SyntaxVisitorContinueKind {
85+
.visitChildren
86+
}
87+
88+
// String literals
89+
override func visit(_ node: StringLiteralExprSyntax) -> SyntaxVisitorContinueKind {
90+
.visitChildren
91+
}
92+
93+
// Array literals
94+
override func visit(_ node: ArrayExprSyntax) -> SyntaxVisitorContinueKind {
95+
.visitChildren
96+
}
97+
98+
// Dictionary literals
99+
override func visit(_ node: DictionaryExprSyntax) -> SyntaxVisitorContinueKind {
100+
.visitChildren
101+
}
102+
103+
// Tuple literals
104+
override func visit(_ node: TupleExprSyntax) -> SyntaxVisitorContinueKind {
105+
.visitChildren
106+
}
107+
108+
// Macro uses.
109+
override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
110+
.visitChildren
111+
}
112+
113+
// References to declarations. Only accept those that refer to a parameter
114+
// of a macro.
115+
override func visit(_ node: IdentifierExprSyntax) -> SyntaxVisitorContinueKind {
116+
let identifier = node.identifier
117+
118+
// FIXME: This will go away.
119+
guard case let .functionLike(signature) = macro.signature else {
120+
return .visitChildren
121+
}
122+
123+
let matchedParameter = signature.input.parameterList.enumerated().first { (index, parameter) in
124+
if identifier.text == "_" {
125+
return false
126+
}
127+
128+
guard let parameterName = parameter.parameterName else {
129+
return false
130+
}
131+
132+
return identifier.text == parameterName.text
133+
}
134+
135+
guard let (parameterIndex, _) = matchedParameter else {
136+
// We have a reference to something that isn't a parameter of the macro.
137+
diagnostics.append(
138+
Diagnostic(
139+
node: Syntax(identifier),
140+
message: MacroExpanderError.nonParameterReference(identifier)
141+
)
142+
)
143+
144+
return .visitChildren
145+
}
146+
147+
replacements.append(.init(reference: node, parameterIndex: parameterIndex))
148+
return .visitChildren
149+
}
150+
151+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
152+
if let expr = node.as(ExprSyntax.self) {
153+
// We have an expression that is not one of the allowed forms, so
154+
// diagnose it.
155+
diagnostics.append(
156+
Diagnostic(
157+
node: node,
158+
message: MacroExpanderError.nonLiteralOrParameter(expr)
159+
)
160+
)
161+
162+
return .skipChildren
163+
}
164+
165+
return .visitChildren
166+
}
167+
168+
}
169+
170+
extension MacroDeclSyntax {
171+
/// Check the definition of the given macro.
172+
///
173+
/// Macros are defined by an expression, which must itself be a macro
174+
/// expansion. Check the definition and produce a semantic representation of
175+
/// it or one of the "builtin"
176+
///
177+
/// Compute the sequence of parameter replacements required when expanding
178+
/// the definition of a non-external macro.
179+
///
180+
/// If there are an errors that prevent expansion, the diagnostics will be
181+
/// wrapped into a an error that prevents expansion, that error is thrown.
182+
public func checkDefinition() throws -> MacroDefinition {
183+
// Cannot compute replacements for an undefined macro.
184+
guard let originalDefinition = definition?.value else {
185+
let undefinedDiag = Diagnostic(
186+
node: Syntax(self),
187+
message: MacroExpanderError.undefined
188+
)
189+
190+
throw DiagnosticsError(diagnostics: [undefinedDiag])
191+
}
192+
193+
/// Recognize the deprecated syntax A.B. Clients will need to
194+
/// handle this themselves.
195+
if let memberAccess = originalDefinition.as(MemberAccessExprSyntax.self),
196+
let base = memberAccess.base,
197+
let baseName = base.as(IdentifierExprSyntax.self)?.identifier
198+
{
199+
let memberName = memberAccess.name
200+
return .deprecatedExternal(
201+
node: Syntax(memberAccess),
202+
module: baseName.trimmedDescription,
203+
type: memberName.trimmedDescription
204+
)
205+
}
206+
207+
// Make sure we have a macro expansion expression.
208+
guard let definition = originalDefinition.as(MacroExpansionExprSyntax.self) else {
209+
let badDefinitionDiag =
210+
Diagnostic(
211+
node: Syntax(originalDefinition),
212+
message: MacroExpanderError.definitionNotMacroExpansion
213+
)
214+
215+
throw DiagnosticsError(diagnostics: [badDefinitionDiag])
216+
}
217+
218+
let visitor = ParameterReplacementVisitor(macro: self)
219+
visitor.walk(definition)
220+
221+
if !visitor.diagnostics.isEmpty {
222+
throw DiagnosticsError(diagnostics: visitor.diagnostics)
223+
}
224+
225+
return .expansion(definition, replacements: visitor.replacements)
226+
}
227+
}
228+
229+
/// Syntax rewrite that performs macro expansion by textually replacing
230+
/// uses of macro parameters with their corresponding arguments.
231+
private final class MacroExpansionRewriter: SyntaxRewriter {
232+
let parameterReplacements: [IdentifierExprSyntax: Int]
233+
let arguments: [ExprSyntax]
234+
235+
init(parameterReplacements: [IdentifierExprSyntax: Int], arguments: [ExprSyntax]) {
236+
self.parameterReplacements = parameterReplacements
237+
self.arguments = arguments
238+
}
239+
240+
override func visit(_ node: IdentifierExprSyntax) -> ExprSyntax {
241+
guard let parameterIndex = parameterReplacements[node] else {
242+
return super.visit(node)
243+
}
244+
245+
// Swap in the argument for this parameter
246+
return arguments[parameterIndex].trimmed
247+
}
248+
}
249+
250+
extension MacroDeclSyntax {
251+
/// Expand the definition of this macro when provided with the given
252+
/// argument list.
253+
private func expand(
254+
argumentList: TupleExprElementListSyntax?,
255+
definition: MacroExpansionExprSyntax,
256+
replacements: [MacroDefinition.Replacement]
257+
) -> ExprSyntax {
258+
// FIXME: Do real call-argument matching between the argument list and the
259+
// macro parameter list, porting over from the compiler.
260+
let arguments: [ExprSyntax] =
261+
argumentList?.map { element in
262+
element.expression
263+
} ?? []
264+
265+
return MacroExpansionRewriter(
266+
parameterReplacements: Dictionary(
267+
uniqueKeysWithValues: replacements.map { replacement in
268+
(replacement.reference, replacement.parameterIndex)
269+
}
270+
),
271+
arguments: arguments
272+
).visit(definition)
273+
}
274+
275+
/// Given a freestanding macro expansion syntax node that references this
276+
/// macro declaration, expand the macro by substituting the arguments from
277+
/// the macro expansion into the parameters that are used in the definition.
278+
public func expand<Node: FreestandingMacroExpansionSyntax>(
279+
_ node: Node,
280+
definition: MacroExpansionExprSyntax,
281+
replacements: [MacroDefinition.Replacement]
282+
) -> ExprSyntax {
283+
return expand(
284+
argumentList: node.argumentList,
285+
definition: definition,
286+
replacements: replacements
287+
)
288+
}
289+
290+
/// Given an attached macro expansion syntax node that references this
291+
/// macro declaration, expand the macro by substituting the arguments from
292+
/// the expansion into the parameters that are used in the definition.
293+
public func expand(
294+
_ node: AttributeSyntax,
295+
definition: MacroExpansionExprSyntax,
296+
replacements: [MacroDefinition.Replacement]
297+
) -> ExprSyntax {
298+
// Dig out the argument list.
299+
let argumentList: TupleExprElementListSyntax?
300+
if case let .argumentList(argList) = node.argument {
301+
argumentList = argList
302+
} else {
303+
argumentList = nil
304+
}
305+
306+
return expand(
307+
argumentList: argumentList,
308+
definition: definition,
309+
replacements: replacements
310+
)
311+
}
312+
}

0 commit comments

Comments
 (0)