|
| 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 | +} |
0 commit comments