Skip to content

Commit 0ae92fd

Browse files
committed
[Macros] Add API for expanding a macro defined in terms of another macro
1 parent 20d5fb9 commit 0ae92fd

File tree

2 files changed

+343
-0
lines changed

2 files changed

+343
-0
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import SwiftDiagnostics
2+
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+
}
13+
14+
extension FunctionParameterSyntax {
15+
/// Retrieve the name of the parameter as it is used in source.
16+
///
17+
/// Example:
18+
///
19+
/// func f(a: Int, _ b: Int, c see: Int) { ... }
20+
///
21+
/// The parameter names for these three parameters are `a`, `b`, and `see`,
22+
/// respectively.
23+
var parameterName: TokenSyntax? {
24+
// If there were two names, the second is the parameter name.
25+
if let secondName = secondName {
26+
if secondName.text == "_" {
27+
return nil
28+
}
29+
30+
return secondName
31+
}
32+
33+
if let firstName = firstName {
34+
if firstName.text == "_" {
35+
return nil
36+
}
37+
38+
return firstName
39+
}
40+
41+
return nil
42+
}
43+
}
44+
45+
enum MacroExpanderError: DiagnosticMessage {
46+
case undefined
47+
case nonParameterReference(TokenSyntax)
48+
case nonLiteralOrParameter(ExprSyntax)
49+
50+
var message: String {
51+
switch self {
52+
case .undefined:
53+
return "macro expansion requires a definition"
54+
55+
case .nonParameterReference(let name):
56+
return "reference to value '\(name.text)' that is not a macro parameter in expansion"
57+
58+
case .nonLiteralOrParameter:
59+
return "only literals and macro parameters are permitted in expansion"
60+
}
61+
}
62+
63+
var diagnosticID: MessageID {
64+
.init(domain: "SwiftMacros", id: "\(self)")
65+
}
66+
67+
var severity: DiagnosticSeverity {
68+
.error
69+
}
70+
}
71+
72+
fileprivate class ParameterReplacementVisitor: SyntaxAnyVisitor {
73+
let macro: MacroDeclSyntax
74+
var replacements: [ParameterReplacement] = []
75+
var diagnostics: [Diagnostic] = []
76+
77+
init(macro: MacroDeclSyntax) {
78+
self.macro = macro
79+
super.init(viewMode: .fixedUp)
80+
}
81+
82+
// Integer literals
83+
override func visit(_ node: IntegerLiteralExprSyntax) -> SyntaxVisitorContinueKind {
84+
.visitChildren
85+
}
86+
87+
// Floating point literals
88+
override func visit(_ node: FloatLiteralExprSyntax) -> SyntaxVisitorContinueKind {
89+
.visitChildren
90+
}
91+
92+
// nil literals
93+
override func visit(_ node: NilLiteralExprSyntax) -> SyntaxVisitorContinueKind {
94+
.visitChildren
95+
}
96+
97+
// String literals
98+
override func visit(_ node: StringLiteralExprSyntax) -> SyntaxVisitorContinueKind {
99+
.visitChildren
100+
}
101+
102+
// Array literals
103+
override func visit(_ node: ArrayExprSyntax) -> SyntaxVisitorContinueKind {
104+
.visitChildren
105+
}
106+
107+
// Dictionary literals
108+
override func visit(_ node: DictionaryExprSyntax) -> SyntaxVisitorContinueKind {
109+
.visitChildren
110+
}
111+
112+
// Tuple literals
113+
override func visit(_ node: TupleExprSyntax) -> SyntaxVisitorContinueKind {
114+
.visitChildren
115+
}
116+
117+
// Macro uses.
118+
override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
119+
.visitChildren
120+
}
121+
122+
// References to declarations. Only accept those that refer to a parameter
123+
// of a macro.
124+
override func visit(_ node: IdentifierExprSyntax) -> SyntaxVisitorContinueKind {
125+
let identifier = node.identifier
126+
127+
// FIXME: This will go away.
128+
guard case let .functionLike(signature) = macro.signature else {
129+
return .visitChildren
130+
}
131+
132+
let matchedParameter = signature.input.parameterList.enumerated().first { (index, parameter) in
133+
if identifier.text == "_" {
134+
return false
135+
}
136+
137+
guard let parameterName = parameter.parameterName else {
138+
return false
139+
}
140+
141+
return identifier.text == parameterName.text
142+
}
143+
144+
guard let (parameterIndex, _) = matchedParameter else {
145+
// We have a reference to something that isn't a parameter of the macro.
146+
diagnostics.append(
147+
Diagnostic(
148+
node: Syntax(identifier),
149+
message: MacroExpanderError.nonParameterReference(identifier)
150+
)
151+
)
152+
153+
return .visitChildren
154+
}
155+
156+
replacements.append(.init(reference: node, parameterIndex: parameterIndex))
157+
return .visitChildren
158+
}
159+
160+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
161+
if let expr = node.as(ExprSyntax.self) {
162+
// We have an expression that is not one of the allowed forms,
163+
diagnostics.append(
164+
Diagnostic(
165+
node: node,
166+
message: MacroExpanderError.nonLiteralOrParameter(expr)
167+
)
168+
)
169+
170+
return .skipChildren
171+
}
172+
173+
return .visitChildren
174+
}
175+
176+
}
177+
178+
extension MacroDeclSyntax {
179+
/// Compute the sequence of parameter replacements required when expanding
180+
/// the definition of a non-external macro.
181+
@_spi(Testing)
182+
public func expansionParameterReplacements() -> (replacements: [ParameterReplacement], diagnostics: [Diagnostic]) {
183+
// Cannot compute replacements for an undefined macro.
184+
guard let definition = definition?.value else {
185+
let undefinedDiag = Diagnostic(
186+
node: Syntax(self),
187+
message: MacroExpanderError.undefined
188+
)
189+
190+
return (replacements: [], diagnostics: [undefinedDiag])
191+
}
192+
193+
let visitor = ParameterReplacementVisitor(macro: self)
194+
visitor.walk(definition)
195+
196+
return (replacements: visitor.replacements, diagnostics: visitor.diagnostics)
197+
}
198+
}
199+
200+
/// Syntax rewrite that performs macro expansion by textually replacing
201+
/// uses of macro parameters with their corresponding arguments.
202+
private final class MacroExpansionRewriter: SyntaxRewriter {
203+
let parameterReplacements: [IdentifierExprSyntax: Int]
204+
let arguments: [ExprSyntax]
205+
206+
init(parameterReplacements: [IdentifierExprSyntax: Int], arguments: [ExprSyntax]) {
207+
self.parameterReplacements = parameterReplacements
208+
self.arguments = arguments
209+
}
210+
211+
override func visit(_ node: IdentifierExprSyntax) -> ExprSyntax {
212+
guard let parameterIndex = parameterReplacements[node] else {
213+
return super.visit(node)
214+
}
215+
216+
// Swap in the argument for this parameter
217+
return arguments[parameterIndex].trimmed
218+
}
219+
}
220+
221+
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+
238+
// FIXME: Do real call-argument matching between the argument list and the
239+
// macro parameter list, porting over from the compiler.
240+
let arguments: [ExprSyntax] = node.argumentList.map { element in
241+
element.expression
242+
}
243+
244+
return MacroExpansionRewriter(
245+
parameterReplacements: Dictionary(
246+
uniqueKeysWithValues: replacements.map { replacement in
247+
(replacement.reference, replacement.parameterIndex)
248+
}
249+
),
250+
arguments: arguments
251+
).visit(definition!.value)
252+
}
253+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftDiagnostics
14+
import SwiftParser
15+
import SwiftSyntax
16+
import SwiftSyntaxBuilder
17+
@_spi(Testing) import SwiftSyntaxMacros
18+
import _SwiftSyntaxTestSupport
19+
import XCTest
20+
21+
final class MacroReplacementTests: XCTestCase {
22+
func testMacroDefinitionGood() {
23+
let macro: DeclSyntax =
24+
"""
25+
macro expand1(a: Int, b: Int) = #otherMacro(first: b, second: ["a": a], third: [3.14159, 2.71828], fourth: 4)
26+
"""
27+
28+
let (replacements, diags) = macro.as(MacroDeclSyntax.self)!
29+
.expansionParameterReplacements()
30+
XCTAssertEqual(diags.count, 0)
31+
XCTAssertEqual(replacements.count, 2)
32+
XCTAssertEqual(replacements[0].parameterIndex, 1)
33+
XCTAssertEqual(replacements[1].parameterIndex, 0)
34+
}
35+
36+
func testMacroDefinitionBad() {
37+
let macro: DeclSyntax =
38+
"""
39+
macro expand1(a: Int, b: Int) = #otherMacro(first: b + 1, c)
40+
"""
41+
42+
let (_, diags) = macro.as(MacroDeclSyntax.self)!
43+
.expansionParameterReplacements()
44+
XCTAssertEqual(diags.count, 2)
45+
XCTAssertEqual(
46+
diags[0].diagMessage.message,
47+
"only literals and macro parameters are permitted in expansion"
48+
)
49+
XCTAssertEqual(
50+
diags[1].diagMessage.message,
51+
"reference to value 'c' that is not a macro parameter in expansion"
52+
)
53+
}
54+
55+
func testMacroUndefined() {
56+
let macro: DeclSyntax =
57+
"""
58+
macro expand1(a: Int, b: Int)
59+
"""
60+
61+
let (_, diags) = macro.as(MacroDeclSyntax.self)!
62+
.expansionParameterReplacements()
63+
XCTAssertEqual(diags.count, 1)
64+
XCTAssertEqual(
65+
diags[0].diagMessage.message,
66+
"macro expansion requires a definition"
67+
)
68+
}
69+
70+
func testMacroExpansion() {
71+
let macro: DeclSyntax =
72+
"""
73+
macro expand1(a: Int, b: Int) = #otherMacro(first: b, second: ["a": a], third: [3.14159, 2.71828], fourth: 4)
74+
"""
75+
76+
let use: ExprSyntax =
77+
"""
78+
#expand1(a: 5, b: 17)
79+
"""
80+
81+
let expandedSyntax = try! macro.as(MacroDeclSyntax.self)!
82+
.expandDefinition(use.as(MacroExpansionExprSyntax.self)!)
83+
AssertStringsEqualWithDiff(
84+
expandedSyntax.description,
85+
"""
86+
#otherMacro(first: 17, second: ["a": 5], third: [3.14159, 2.71828], fourth: 4)
87+
"""
88+
)
89+
}
90+
}

0 commit comments

Comments
 (0)