Skip to content

Commit d31be38

Browse files
committed
[Macros] Rework API of macro definition and expansion.
Split the API for macro definition checking and expansion into two distinct parts: a definition-checking operation that can fail (producing error diagnostics) and an expansion operation that cannot fail (for expansion). This better matches the structure that clients (i.e., the compiler) should use.
1 parent 0ae92fd commit d31be38

File tree

2 files changed

+166
-50
lines changed

2 files changed

+166
-50
lines changed

Sources/SwiftSyntaxMacros/MacroReplacement.swift

Lines changed: 125 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
import SwiftDiagnostics
22
import SwiftSyntax
3-
4-
/// The replacement of a parameter.
5-
@_spi(Testing)
6-
public struct ParameterReplacement {
7-
/// A reference to a parameter as it occurs in the macro expansion expression.
8-
public let reference: IdentifierExprSyntax
9-
10-
/// The index of the parameter
11-
public let parameterIndex: Int
12-
}
3+
import SwiftSyntaxBuilder
134

145
extension FunctionParameterSyntax {
156
/// Retrieve the name of the parameter as it is used in source.
@@ -44,6 +35,7 @@ extension FunctionParameterSyntax {
4435

4536
enum MacroExpanderError: DiagnosticMessage {
4637
case undefined
38+
case definitionNotMacroExpansion
4739
case nonParameterReference(TokenSyntax)
4840
case nonLiteralOrParameter(ExprSyntax)
4941

@@ -52,6 +44,9 @@ enum MacroExpanderError: DiagnosticMessage {
5244
case .undefined:
5345
return "macro expansion requires a definition"
5446

47+
case .definitionNotMacroExpansion:
48+
return "macro definition must itself by a macro expansion expression (starting with '#')"
49+
5550
case .nonParameterReference(let name):
5651
return "reference to value '\(name.text)' that is not a macro parameter in expansion"
5752

@@ -69,9 +64,36 @@ enum MacroExpanderError: DiagnosticMessage {
6964
}
7065
}
7166

67+
/// Provide the definition of a macro
68+
public enum MacroDefinition {
69+
/// An externally-defined macro, known by its type name and the module in
70+
/// which that type resides, which uses the deprecated syntax `A.B`.
71+
case deprecatedExternal(node: Syntax, module: String, type: String)
72+
73+
/// A macro that is defined by expansion of another macro.
74+
///
75+
/// The definition has the macro expansion expression itself, along with
76+
/// sequence of replacements for subtrees that refer to parameters of the
77+
/// defining macro. These subtrees will need to be replaced with the text of
78+
/// the corresponding argument to the macro, which can be accomplished with
79+
/// `MacroDeclSyntax.expandDefinition`.
80+
case expansion(MacroExpansionExprSyntax, replacements: [Replacement])
81+
}
82+
83+
extension MacroDefinition {
84+
/// A replacement that occurs as part of an expanded macro definition.
85+
public struct Replacement {
86+
/// A reference to a parameter as it occurs in the macro expansion expression.
87+
public let reference: IdentifierExprSyntax
88+
89+
/// The index of the parameter in the defining macro.
90+
public let parameterIndex: Int
91+
}
92+
}
93+
7294
fileprivate class ParameterReplacementVisitor: SyntaxAnyVisitor {
7395
let macro: MacroDeclSyntax
74-
var replacements: [ParameterReplacement] = []
96+
var replacements: [MacroDefinition.Replacement] = []
7597
var diagnostics: [Diagnostic] = []
7698

7799
init(macro: MacroDeclSyntax) {
@@ -159,7 +181,8 @@ fileprivate class ParameterReplacementVisitor: SyntaxAnyVisitor {
159181

160182
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
161183
if let expr = node.as(ExprSyntax.self) {
162-
// We have an expression that is not one of the allowed forms,
184+
// We have an expression that is not one of the allowed forms, so
185+
// diagnose it.
163186
diagnostics.append(
164187
Diagnostic(
165188
node: node,
@@ -176,24 +199,61 @@ fileprivate class ParameterReplacementVisitor: SyntaxAnyVisitor {
176199
}
177200

178201
extension MacroDeclSyntax {
202+
/// Check the definition of the given macro.
203+
///
204+
/// Macros are defined by an expression, which must itself be a macro
205+
/// expansion. Check the definition and produce a semantic representation of
206+
/// it or one of the "builtin"
207+
///
179208
/// Compute the sequence of parameter replacements required when expanding
180209
/// the definition of a non-external macro.
181-
@_spi(Testing)
182-
public func expansionParameterReplacements() -> (replacements: [ParameterReplacement], diagnostics: [Diagnostic]) {
210+
///
211+
/// If there are an errors that prevent expansion, the diagnostics will be
212+
/// wrapped into a an error that prevents expansion, that error is thrown.
213+
public func checkDefinition() throws -> MacroDefinition {
183214
// Cannot compute replacements for an undefined macro.
184-
guard let definition = definition?.value else {
215+
guard let originalDefinition = definition?.value else {
185216
let undefinedDiag = Diagnostic(
186217
node: Syntax(self),
187218
message: MacroExpanderError.undefined
188219
)
189220

190-
return (replacements: [], diagnostics: [undefinedDiag])
221+
throw DiagnosticsError(diagnostics: [undefinedDiag])
222+
}
223+
224+
/// Recognize the deprecated syntax A.B. Clients will need to
225+
/// handle this themselves.
226+
if let memberAccess = originalDefinition.as(MemberAccessExprSyntax.self),
227+
let base = memberAccess.base,
228+
let baseName = base.as(IdentifierExprSyntax.self)?.identifier
229+
{
230+
let memberName = memberAccess.name
231+
return .deprecatedExternal(
232+
node: Syntax(memberAccess),
233+
module: baseName.trimmedDescription,
234+
type: memberName.trimmedDescription
235+
)
236+
}
237+
238+
// Make sure we have a macro expansion expression.
239+
guard let definition = originalDefinition.as(MacroExpansionExprSyntax.self) else {
240+
let badDefinitionDiag =
241+
Diagnostic(
242+
node: Syntax(originalDefinition),
243+
message: MacroExpanderError.definitionNotMacroExpansion
244+
)
245+
246+
throw DiagnosticsError(diagnostics: [badDefinitionDiag])
191247
}
192248

193249
let visitor = ParameterReplacementVisitor(macro: self)
194250
visitor.walk(definition)
195251

196-
return (replacements: visitor.replacements, diagnostics: visitor.diagnostics)
252+
if !visitor.diagnostics.isEmpty {
253+
throw DiagnosticsError(diagnostics: visitor.diagnostics)
254+
}
255+
256+
return .expansion(definition, replacements: visitor.replacements)
197257
}
198258
}
199259

@@ -219,27 +279,18 @@ private final class MacroExpansionRewriter: SyntaxRewriter {
219279
}
220280

221281
extension MacroDeclSyntax {
222-
/// Given a freestanding macro expansion syntax node that references this
223-
/// macro declaration, expand the macro by substituting the arguments from
224-
/// the macro expansion into the parameters that are used in the definition.
225-
///
226-
/// If there are any errors, the function will throw with all diagnostics
227-
/// placed in a `DiagnosticsError`.
228-
public func expandDefinition(
229-
_ node: some FreestandingMacroExpansionSyntax
230-
) throws -> ExprSyntax {
231-
let (replacements, diagnostics) = expansionParameterReplacements()
232-
233-
// If there were any diagnostics, don't allow replacement.
234-
if !diagnostics.isEmpty {
235-
throw DiagnosticsError(diagnostics: diagnostics)
236-
}
237-
282+
/// Expand the definition of this macro when provided with the given
283+
/// argument list.
284+
private func expand(
285+
argumentList: TupleExprElementListSyntax?,
286+
definition: MacroExpansionExprSyntax,
287+
replacements: [MacroDefinition.Replacement]
288+
) -> ExprSyntax {
238289
// FIXME: Do real call-argument matching between the argument list and the
239290
// macro parameter list, porting over from the compiler.
240-
let arguments: [ExprSyntax] = node.argumentList.map { element in
291+
let arguments: [ExprSyntax] = argumentList?.map { element in
241292
element.expression
242-
}
293+
} ?? []
243294

244295
return MacroExpansionRewriter(
245296
parameterReplacements: Dictionary(
@@ -248,6 +299,44 @@ extension MacroDeclSyntax {
248299
}
249300
),
250301
arguments: arguments
251-
).visit(definition!.value)
302+
).visit(definition)
303+
}
304+
305+
/// Given a freestanding macro expansion syntax node that references this
306+
/// macro declaration, expand the macro by substituting the arguments from
307+
/// the macro expansion into the parameters that are used in the definition.
308+
public func expand(
309+
_ node: some FreestandingMacroExpansionSyntax,
310+
definition: MacroExpansionExprSyntax,
311+
replacements: [MacroDefinition.Replacement]
312+
) -> ExprSyntax {
313+
return try expand(
314+
argumentList: node.argumentList,
315+
definition: definition,
316+
replacements: replacements
317+
)
318+
}
319+
320+
/// Given an attached macro expansion syntax node that references this
321+
/// macro declaration, expand the macro by substituting the arguments from
322+
/// the expansion into the parameters that are used in the definition.
323+
public func expand(
324+
_ node: AttributeSyntax,
325+
definition: MacroExpansionExprSyntax,
326+
replacements: [MacroDefinition.Replacement]
327+
) -> ExprSyntax {
328+
// Dig out the argument list.
329+
let argumentList: TupleExprElementListSyntax?
330+
if case let .argumentList(argList) = node.argument {
331+
argumentList = argList
332+
} else {
333+
argumentList = nil
334+
}
335+
336+
return try expand(
337+
argumentList: argumentList,
338+
definition: definition,
339+
replacements: replacements
340+
)
252341
}
253342
}

Tests/SwiftSyntaxMacrosTest/MacroReplacementTests.swift

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,43 @@ import SwiftDiagnostics
1414
import SwiftParser
1515
import SwiftSyntax
1616
import SwiftSyntaxBuilder
17-
@_spi(Testing) import SwiftSyntaxMacros
17+
import SwiftSyntaxMacros
1818
import _SwiftSyntaxTestSupport
1919
import XCTest
2020

2121
final class MacroReplacementTests: XCTestCase {
22-
func testMacroDefinitionGood() {
22+
func testMacroDefinitionGood() throws {
2323
let macro: DeclSyntax =
2424
"""
2525
macro expand1(a: Int, b: Int) = #otherMacro(first: b, second: ["a": a], third: [3.14159, 2.71828], fourth: 4)
2626
"""
2727

28-
let (replacements, diags) = macro.as(MacroDeclSyntax.self)!
29-
.expansionParameterReplacements()
30-
XCTAssertEqual(diags.count, 0)
28+
let definition = try macro.as(MacroDeclSyntax.self)!.checkDefinition()
29+
guard case let .expansion(_, replacements) = definition else {
30+
XCTFail("not an expansion definition")
31+
fatalError()
32+
}
33+
3134
XCTAssertEqual(replacements.count, 2)
3235
XCTAssertEqual(replacements[0].parameterIndex, 1)
3336
XCTAssertEqual(replacements[1].parameterIndex, 0)
3437
}
3538

36-
func testMacroDefinitionBad() {
39+
func testMacroDefinitionBad() throws {
3740
let macro: DeclSyntax =
3841
"""
3942
macro expand1(a: Int, b: Int) = #otherMacro(first: b + 1, c)
4043
"""
4144

42-
let (_, diags) = macro.as(MacroDeclSyntax.self)!
43-
.expansionParameterReplacements()
45+
let diags: [Diagnostic]
46+
do {
47+
_ = try macro.as(MacroDeclSyntax.self)!.checkDefinition()
48+
XCTFail("should have failed with an error")
49+
fatalError()
50+
} catch let diagError as DiagnosticsError {
51+
diags = diagError.diagnostics
52+
}
53+
4454
XCTAssertEqual(diags.count, 2)
4555
XCTAssertEqual(
4656
diags[0].diagMessage.message,
@@ -52,22 +62,29 @@ final class MacroReplacementTests: XCTestCase {
5262
)
5363
}
5464

55-
func testMacroUndefined() {
65+
func testMacroUndefined() throws {
5666
let macro: DeclSyntax =
5767
"""
5868
macro expand1(a: Int, b: Int)
5969
"""
6070

61-
let (_, diags) = macro.as(MacroDeclSyntax.self)!
62-
.expansionParameterReplacements()
71+
let diags: [Diagnostic]
72+
do {
73+
_ = try macro.as(MacroDeclSyntax.self)!.checkDefinition()
74+
XCTFail("should have failed with an error")
75+
fatalError()
76+
} catch let diagError as DiagnosticsError {
77+
diags = diagError.diagnostics
78+
}
79+
6380
XCTAssertEqual(diags.count, 1)
6481
XCTAssertEqual(
6582
diags[0].diagMessage.message,
6683
"macro expansion requires a definition"
6784
)
6885
}
6986

70-
func testMacroExpansion() {
87+
func testMacroExpansion() throws {
7188
let macro: DeclSyntax =
7289
"""
7390
macro expand1(a: Int, b: Int) = #otherMacro(first: b, second: ["a": a], third: [3.14159, 2.71828], fourth: 4)
@@ -78,8 +95,18 @@ final class MacroReplacementTests: XCTestCase {
7895
#expand1(a: 5, b: 17)
7996
"""
8097

81-
let expandedSyntax = try! macro.as(MacroDeclSyntax.self)!
82-
.expandDefinition(use.as(MacroExpansionExprSyntax.self)!)
98+
let macroDecl = macro.as(MacroDeclSyntax.self)!
99+
let definition = try macroDecl.checkDefinition()
100+
guard case let .expansion(expansion, replacements) = definition else {
101+
XCTFail("not a normal expansion")
102+
fatalError()
103+
}
104+
105+
let expandedSyntax = macroDecl.expand(
106+
use.as(MacroExpansionExprSyntax.self)!,
107+
definition: expansion,
108+
replacements: replacements
109+
)
83110
AssertStringsEqualWithDiff(
84111
expandedSyntax.description,
85112
"""

0 commit comments

Comments
 (0)