Skip to content

Commit 24a2c86

Browse files
committed
[Macros] Implement function body macros
Introduce function body macros, which are comprised of two similar macro roles: - Preamble macros introduce "preamble" code into a user-written function body, e.g., to perform tracing, logging, check additional preconditions, etc. - Body macros: introduce a function body into a function that has none or wholesale replace the function body.
1 parent 576cbb6 commit 24a2c86

File tree

10 files changed

+525
-6
lines changed

10 files changed

+525
-6
lines changed

Sources/SwiftCompilerPluginMessageHandling/Macros.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
case preamble
128+
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: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public enum MacroRole {
2424
case conformance
2525
case codeItem
2626
case `extension`
27+
case preamble
28+
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 tooManyBodyMacros
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 .tooManyBodyMacros:
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: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,67 @@ 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 let expanded = expandAttachedMacro(
361+
definition: definition,
362+
macroRole: .preamble,
363+
attributeNode: attributeNode.detach(
364+
in: context,
365+
foldingWith: .standardOperators
366+
),
367+
declarationNode: DeclSyntax(decl.detach(in: context)),
368+
parentDeclNode: nil,
369+
extendedType: nil,
370+
conformanceList: nil,
371+
in: context,
372+
indentationWidth: indentationWidth
373+
) else {
374+
return []
375+
}
376+
377+
// Match the indentation of the statements if we can, and put newlines around
378+
// the preamble to separate it from the rest of the body.
379+
let indentation = decl.body?.statements.indentationOfFirstLine ?? (decl.indentationOfFirstLine + indentationWidth)
380+
let indentedSource = "\n" + expanded.indented(by: indentation) + "\n\n\n"
381+
return "\(raw: indentedSource)"
382+
}
383+
384+
private func expandBodyMacro(
385+
definition: BodyMacro.Type,
386+
attributeNode: AttributeSyntax,
387+
attachedTo decl: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
388+
in context: some MacroExpansionContext,
389+
indentationWidth: Trivia
390+
) -> CodeBlockSyntax? {
391+
guard let expanded = expandAttachedMacro(
392+
definition: definition,
393+
macroRole: .body,
394+
attributeNode: attributeNode.detach(
395+
in: context,
396+
foldingWith: .standardOperators
397+
),
398+
declarationNode: DeclSyntax(decl.detach(in: context)),
399+
parentDeclNode: nil,
400+
extendedType: nil,
401+
conformanceList: nil,
402+
in: context,
403+
indentationWidth: indentationWidth
404+
) else {
405+
return nil
406+
}
407+
408+
// Wrap the body in braces.
409+
let indentedSource = " {\n" + expanded.indented(by: decl.indentationOfFirstLine + indentationWidth) + "\n}\n"
410+
return "\(raw: indentedSource)" as CodeBlockSyntax
411+
}
412+
352413
// MARK: - MacroSystem
353414

354415
/// Describes the kinds of errors that can occur within a macro system.
@@ -567,14 +628,20 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
567628
case .notAMacro:
568629
break
569630
}
570-
if let declSyntax = node.as(DeclSyntax.self),
631+
if var declSyntax = node.as(DeclSyntax.self),
571632
let attributedNode = node.asProtocol(WithAttributesSyntax.self),
572633
!attributedNode.attributes.isEmpty
573634
{
635+
// Apply body and preamble macros.
636+
if let nodeWithBody = node.asProtocol(WithOptionalCodeBlockSyntax.self),
637+
let declNodeWithBody = nodeWithBody as? any DeclSyntaxProtocol & WithOptionalCodeBlockSyntax {
638+
declSyntax = DeclSyntax(visitBodyAndPreambleMacros(declNodeWithBody))
639+
}
640+
574641
// Visit the node, disabling the `visitAny` handling.
575-
skipVisitAnyHandling.insert(node)
642+
skipVisitAnyHandling.insert(Syntax(declSyntax))
576643
let visitedNode = self.visit(declSyntax)
577-
skipVisitAnyHandling.remove(node)
644+
skipVisitAnyHandling.remove(Syntax(declSyntax))
578645

579646
let attributesToRemove = self.macroAttributes(attachedTo: visitedNode).map(\.attributeNode)
580647

@@ -584,6 +651,68 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
584651
return nil
585652
}
586653

654+
/// Visit for both the body and preamble macros.
655+
func visitBodyAndPreambleMacros<Node: DeclSyntaxProtocol & WithOptionalCodeBlockSyntax>(
656+
_ node: Node
657+
) -> Node {
658+
// Expand preamble macros into a set of code items.
659+
let preamble = expandMacros(attachedTo: DeclSyntax(node), ofType: PreambleMacro.Type.self) { attributeNode, definition in
660+
expandPreambleMacro(
661+
definition: definition,
662+
attributeNode: attributeNode,
663+
attachedTo: node,
664+
in: context,
665+
indentationWidth: indentationWidth
666+
)
667+
}
668+
669+
// Expand body macro.
670+
let expandedBodies = expandMacros(attachedTo: DeclSyntax(node), ofType: BodyMacro.Type.self) { attributeNode, definition in
671+
expandBodyMacro(
672+
definition: definition,
673+
attributeNode: attributeNode,
674+
attachedTo: node,
675+
in: context,
676+
indentationWidth: indentationWidth
677+
).map { [$0] }
678+
}
679+
680+
// Dig out the body.
681+
let body: CodeBlockSyntax
682+
switch expandedBodies.count {
683+
case 0 where preamble.isEmpty:
684+
// Nothing changes
685+
return node
686+
687+
case 0:
688+
guard let existingBody = node.body else {
689+
// Any leftover preamble statements have nowhere to go, complain and
690+
// exit.
691+
context.addDiagnostics(from: MacroExpansionError.preambleWithoutBody, node: node)
692+
693+
return node
694+
}
695+
696+
body = existingBody
697+
698+
case 1:
699+
body = expandedBodies[0]
700+
701+
default:
702+
context.addDiagnostics(from: MacroExpansionError.tooManyBodyMacros, node: node)
703+
body = expandedBodies[0]
704+
}
705+
706+
// If there's no preamble, swap in the new body.
707+
if preamble.isEmpty {
708+
return node.with(\.body, body)
709+
}
710+
711+
var statements = body.statements
712+
statements.insert(contentsOf: preamble, at: statements.startIndex)
713+
return node.with(\.body, body.with(\.statements, statements))
714+
}
715+
587716
override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
588717
var newItems: [CodeBlockItemSyntax] = []
589718
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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
public protocol BodyMacro: AttachedMacro {
15+
/// Expand a macro described by the given custom attribute and
16+
/// attached to the given declaration and evaluated within a
17+
/// particular expansion context.
18+
///
19+
/// The macro expansion can introduce a body for the given function.
20+
static func expansion(
21+
of node: AttributeSyntax,
22+
providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
23+
in context: some MacroExpansionContext
24+
) throws -> [CodeBlockItemSyntax]
25+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
public protocol PreambleMacro: AttachedMacro {
15+
/// Expand a macro described by the given custom attribute and
16+
/// attached to the given declaration and evaluated within a
17+
/// particular expansion context.
18+
///
19+
/// The macro expansion can introduce code items that form a preamble to
20+
/// the body of the given function. The code items produced by this macro
21+
/// expansion will be inserted at the beginning of the function body.
22+
static func expansion(
23+
of node: AttributeSyntax,
24+
providingPreambleFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
25+
in context: some MacroExpansionContext
26+
) throws -> [CodeBlockItemSyntax]
27+
}

0 commit comments

Comments
 (0)