Skip to content

Commit cf49214

Browse files
authored
Merge pull request swiftlang#1874 from DougGregor/function-body-macros
[Macros] Implement function body macros
2 parents aac81f1 + edf12c8 commit cf49214

File tree

10 files changed

+551
-10
lines changed

10 files changed

+551
-10
lines changed

Sources/SwiftCompilerPluginMessageHandling/Macros.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import SwiftBasicFormat
1414
import SwiftDiagnostics
1515
import SwiftOperators
1616
import SwiftSyntax
17-
import SwiftSyntaxMacroExpansion
18-
import SwiftSyntaxMacros
17+
@_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacroExpansion
18+
@_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros
1919

2020
extension CompilerPluginMessageHandler {
2121
/// Get concrete macro type from a pair of module name and type name.
@@ -166,6 +166,8 @@ private extension MacroRole {
166166
case .conformance: self = .extension
167167
case .codeItem: self = .codeItem
168168
case .extension: self = .extension
169+
case .preamble: self = .preamble
170+
case .body: self = .body
169171
}
170172
}
171173
}

Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ public enum PluginMessage {
124124
case conformance
125125
case codeItem
126126
case `extension`
127+
@_spi(ExperimentalLanguageFeature) case preamble
128+
@_spi(ExperimentalLanguageFeature) case body
127129
}
128130

129131
public struct SourceLocation: Codable {

Sources/SwiftSyntaxMacroExpansion/FunctionParameterUtils.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ extension FunctionParameterSyntax {
99
///
1010
/// The parameter names for these three parameters are `a`, `b`, and `see`,
1111
/// respectively.
12-
var parameterName: TokenSyntax? {
12+
@_spi(Testing)
13+
public var parameterName: TokenSyntax? {
1314
// If there were two names, the second is the parameter name.
1415
if let secondName {
1516
if secondName.text == "_" {

Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import SwiftBasicFormat
1414
import SwiftSyntax
15-
@_spi(MacroExpansion) import SwiftSyntaxMacros
15+
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros
1616

1717
public enum MacroRole {
1818
case expression
@@ -24,6 +24,8 @@ public enum MacroRole {
2424
case conformance
2525
case codeItem
2626
case `extension`
27+
@_spi(ExperimentalLanguageFeature) case preamble
28+
@_spi(ExperimentalLanguageFeature) case body
2729
}
2830

2931
extension MacroRole {
@@ -38,18 +40,23 @@ extension MacroRole {
3840
case .conformance: return "ConformanceMacro"
3941
case .codeItem: return "CodeItemMacro"
4042
case .extension: return "ExtensionMacro"
43+
case .preamble: return "PreambleMacro"
44+
case .body: return "BodyMacro"
4145
}
4246
}
4347
}
4448

4549
/// Simple diagnostic message
46-
private enum MacroExpansionError: Error, CustomStringConvertible {
50+
enum MacroExpansionError: Error, CustomStringConvertible {
4751
case unmatchedMacroRole(Macro.Type, MacroRole)
4852
case parentDeclGroupNil
4953
case declarationNotDeclGroup
5054
case declarationNotIdentified
55+
case declarationHasNoBody
5156
case noExtendedTypeSyntax
5257
case noFreestandingMacroRoles(Macro.Type)
58+
case moreThanOneBodyMacro
59+
case preambleWithoutBody
5360

5461
var description: String {
5562
switch self {
@@ -65,12 +72,20 @@ private enum MacroExpansionError: Error, CustomStringConvertible {
6572
case .declarationNotIdentified:
6673
return "declaration is not a 'Identified' syntax"
6774

75+
case .declarationHasNoBody:
76+
return "declaration is not a type with an optional code block"
77+
6878
case .noExtendedTypeSyntax:
6979
return "no extended type for extension macro"
7080

7181
case .noFreestandingMacroRoles(let type):
7282
return "macro implementation type '\(type)' does not conform to any freestanding macro protocol"
7383

84+
case .moreThanOneBodyMacro:
85+
return "function can not have more than one body macro applied to it"
86+
87+
case .preambleWithoutBody:
88+
return "preamble macro cannot be applied to a function with no body"
7489
}
7590
}
7691
}
@@ -125,7 +140,7 @@ public func expandFreestandingMacro(
125140
expandedSyntax = Syntax(CodeBlockItemListSyntax(rewritten))
126141

127142
case (.accessor, _), (.memberAttribute, _), (.member, _), (.peer, _), (.conformance, _), (.extension, _), (.expression, _), (.declaration, _),
128-
(.codeItem, _):
143+
(.codeItem, _), (.preamble, _), (.body, _):
129144
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
130145
}
131146
return expandedSyntax.formattedExpansion(definition.formatMode, indentationWidth: indentationWidth)
@@ -288,6 +303,38 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
288303
$0.formattedExpansion(definition.formatMode, indentationWidth: indentationWidth)
289304
}
290305

306+
case (let attachedMacro as PreambleMacro.Type, .preamble):
307+
guard let declToPass = Syntax(declarationNode).asProtocol(SyntaxProtocol.self) as? (DeclSyntaxProtocol & WithOptionalCodeBlockSyntax)
308+
else {
309+
// Compiler error: declaration must have a body.
310+
throw MacroExpansionError.declarationHasNoBody
311+
}
312+
313+
let preamble = try attachedMacro.expansion(
314+
of: attributeNode,
315+
providingPreambleFor: declToPass,
316+
in: context
317+
)
318+
return preamble.map {
319+
$0.formattedExpansion(definition.formatMode, indentationWidth: indentationWidth)
320+
}
321+
322+
case (let attachedMacro as BodyMacro.Type, .body):
323+
guard let declToPass = Syntax(declarationNode).asProtocol(SyntaxProtocol.self) as? (DeclSyntaxProtocol & WithOptionalCodeBlockSyntax)
324+
else {
325+
// Compiler error: declaration must have a body.
326+
throw MacroExpansionError.declarationHasNoBody
327+
}
328+
329+
let body = try attachedMacro.expansion(
330+
of: attributeNode,
331+
providingBodyFor: declToPass,
332+
in: context
333+
)
334+
return body.map {
335+
$0.formattedExpansion(definition.formatMode, indentationWidth: indentationWidth)
336+
}
337+
291338
default:
292339
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
293340
}

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import SwiftOperators
1515
@_spi(MacroExpansion) import SwiftParser
1616
import SwiftSyntax
1717
import SwiftSyntaxBuilder
18-
@_spi(MacroExpansion) import SwiftSyntaxMacros
18+
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros
1919

2020
// MARK: - Public entry function
2121

@@ -349,6 +349,72 @@ private func expandExtensionMacro(
349349
return "\(raw: indentedSource)"
350350
}
351351

352+
/// Expand a preamble macro into a list of code items.
353+
private func expandPreambleMacro(
354+
definition: PreambleMacro.Type,
355+
attributeNode: AttributeSyntax,
356+
attachedTo decl: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
357+
in context: some MacroExpansionContext,
358+
indentationWidth: Trivia
359+
) -> CodeBlockItemListSyntax? {
360+
guard
361+
let expanded = expandAttachedMacro(
362+
definition: definition,
363+
macroRole: .preamble,
364+
attributeNode: attributeNode.detach(
365+
in: context,
366+
foldingWith: .standardOperators
367+
),
368+
declarationNode: DeclSyntax(decl.detach(in: context)),
369+
parentDeclNode: nil,
370+
extendedType: nil,
371+
conformanceList: nil,
372+
in: context,
373+
indentationWidth: indentationWidth
374+
)
375+
else {
376+
return []
377+
}
378+
379+
// Match the indentation of the statements if we can, and put newlines around
380+
// the preamble to separate it from the rest of the body.
381+
let indentation = decl.body?.statements.indentationOfFirstLine ?? (decl.indentationOfFirstLine + indentationWidth)
382+
let indentedSource = "\n" + expanded.indented(by: indentation) + "\n\n"
383+
return "\(raw: indentedSource)"
384+
}
385+
386+
private func expandBodyMacro(
387+
definition: BodyMacro.Type,
388+
attributeNode: AttributeSyntax,
389+
attachedTo decl: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
390+
in context: some MacroExpansionContext,
391+
indentationWidth: Trivia
392+
) -> CodeBlockSyntax? {
393+
guard
394+
let expanded = expandAttachedMacro(
395+
definition: definition,
396+
macroRole: .body,
397+
attributeNode: attributeNode.detach(
398+
in: context,
399+
foldingWith: .standardOperators
400+
),
401+
declarationNode: DeclSyntax(decl.detach(in: context)),
402+
parentDeclNode: nil,
403+
extendedType: nil,
404+
conformanceList: nil,
405+
in: context,
406+
indentationWidth: indentationWidth
407+
)
408+
else {
409+
return nil
410+
}
411+
412+
// Wrap the body in braces.
413+
let beforeBody = decl.body == nil ? " " : "";
414+
let indentedSource = beforeBody + "{\n" + expanded.indented(by: decl.indentationOfFirstLine + indentationWidth) + "\n}\n"
415+
return "\(raw: indentedSource)" as CodeBlockSyntax
416+
}
417+
352418
// MARK: - MacroSystem
353419

354420
/// Describes the kinds of errors that can occur within a macro system.
@@ -567,14 +633,21 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
567633
case .notAMacro:
568634
break
569635
}
570-
if let declSyntax = node.as(DeclSyntax.self),
636+
if var declSyntax = node.as(DeclSyntax.self),
571637
let attributedNode = node.asProtocol(WithAttributesSyntax.self),
572638
!attributedNode.attributes.isEmpty
573639
{
640+
// Apply body and preamble macros.
641+
if let nodeWithBody = node.asProtocol(WithOptionalCodeBlockSyntax.self),
642+
let declNodeWithBody = nodeWithBody as? any DeclSyntaxProtocol & WithOptionalCodeBlockSyntax
643+
{
644+
declSyntax = DeclSyntax(visitBodyAndPreambleMacros(declNodeWithBody))
645+
}
646+
574647
// Visit the node, disabling the `visitAny` handling.
575-
skipVisitAnyHandling.insert(node)
648+
skipVisitAnyHandling.insert(Syntax(declSyntax))
576649
let visitedNode = self.visit(declSyntax)
577-
skipVisitAnyHandling.remove(node)
650+
skipVisitAnyHandling.remove(Syntax(declSyntax))
578651

579652
let attributesToRemove = self.macroAttributes(attachedTo: visitedNode).map(\.attributeNode)
580653

@@ -584,6 +657,66 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
584657
return nil
585658
}
586659

660+
/// Visit for both the body and preamble macros.
661+
func visitBodyAndPreambleMacros<Node: DeclSyntaxProtocol & WithOptionalCodeBlockSyntax>(
662+
_ node: Node
663+
) -> Node {
664+
// Expand preamble macros into a set of code items.
665+
let preamble = expandMacros(attachedTo: DeclSyntax(node), ofType: PreambleMacro.Type.self) { attributeNode, definition in
666+
expandPreambleMacro(
667+
definition: definition,
668+
attributeNode: attributeNode,
669+
attachedTo: node,
670+
in: context,
671+
indentationWidth: indentationWidth
672+
)
673+
}
674+
675+
// Expand body macro.
676+
let expandedBodies = expandMacros(attachedTo: DeclSyntax(node), ofType: BodyMacro.Type.self) { attributeNode, definition in
677+
expandBodyMacro(
678+
definition: definition,
679+
attributeNode: attributeNode,
680+
attachedTo: node,
681+
in: context,
682+
indentationWidth: indentationWidth
683+
).map { [$0] }
684+
}
685+
686+
// Dig out the body.
687+
let body: CodeBlockSyntax
688+
switch expandedBodies.count {
689+
case 0 where preamble.isEmpty:
690+
// Nothing changes
691+
return node
692+
693+
case 0:
694+
guard let existingBody = node.body else {
695+
// Any leftover preamble statements have nowhere to go, complain and
696+
// exit.
697+
context.addDiagnostics(from: MacroExpansionError.preambleWithoutBody, node: node)
698+
699+
return node
700+
}
701+
702+
body = existingBody
703+
704+
case 1:
705+
body = expandedBodies[0]
706+
707+
default:
708+
context.addDiagnostics(from: MacroExpansionError.moreThanOneBodyMacro, node: node)
709+
body = expandedBodies[0]
710+
}
711+
712+
// If there's no preamble, swap in the new body.
713+
if preamble.isEmpty {
714+
return node.with(\.body, body)
715+
}
716+
717+
return node.with(\.body, body.with(\.statements, preamble + body.statements))
718+
}
719+
587720
override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
588721
var newItems: [CodeBlockItemSyntax] = []
589722
func addResult(_ node: CodeBlockItemSyntax) {

Sources/SwiftSyntaxMacros/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
add_swift_syntax_library(SwiftSyntaxMacros
1010
MacroProtocols/AccessorMacro.swift
1111
MacroProtocols/AttachedMacro.swift
12+
MacroProtocols/BodyMacro.swift
1213
MacroProtocols/CodeItemMacro.swift
1314
MacroProtocols/DeclarationMacro.swift
1415
MacroProtocols/ExpressionMacro.swift
@@ -19,6 +20,7 @@ add_swift_syntax_library(SwiftSyntaxMacros
1920
MacroProtocols/MemberAttributeMacro.swift
2021
MacroProtocols/MemberMacro.swift
2122
MacroProtocols/PeerMacro.swift
23+
MacroProtocols/PreambleMacro.swift
2224

2325
AbstractSourceLocation.swift
2426
MacroExpansionContext.swift
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
3+
// Licensed under Apache License v2.0 with Runtime Library Exception
4+
//
5+
// See https://swift.org/LICENSE.txt for license information
6+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
7+
//
8+
//===----------------------------------------------------------------------===//
9+
10+
import SwiftSyntax
11+
12+
/// Describes a macro that can create the body for a function that does not
13+
/// have one.
14+
@_spi(ExperimentalLanguageFeature)
15+
public protocol BodyMacro: AttachedMacro {
16+
/// Expand a macro described by the given custom attribute and
17+
/// attached to the given declaration and evaluated within a
18+
/// particular expansion context.
19+
///
20+
/// The macro expansion can introduce a body for the given function.
21+
static func expansion(
22+
of node: AttributeSyntax,
23+
providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
24+
in context: some MacroExpansionContext
25+
) throws -> [CodeBlockItemSyntax]
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
3+
// Licensed under Apache License v2.0 with Runtime Library Exception
4+
//
5+
// See https://swift.org/LICENSE.txt for license information
6+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
7+
//
8+
//===----------------------------------------------------------------------===//
9+
10+
import SwiftSyntax
11+
12+
/// Describes a macro that can introduce "preamble" code into an existing
13+
/// function body.
14+
@_spi(ExperimentalLanguageFeature)
15+
public protocol PreambleMacro: AttachedMacro {
16+
/// Expand a macro described by the given custom attribute and
17+
/// attached to the given declaration and evaluated within a
18+
/// particular expansion context.
19+
///
20+
/// The macro expansion can introduce code items that form a preamble to
21+
/// the body of the given function. The code items produced by this macro
22+
/// expansion will be inserted at the beginning of the function body.
23+
static func expansion(
24+
of node: AttributeSyntax,
25+
providingPreambleFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
26+
in context: some MacroExpansionContext
27+
) throws -> [CodeBlockItemSyntax]
28+
}

0 commit comments

Comments
 (0)