Skip to content

[Macros] Implement function body macros #1874

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public let COMMON_NODES: [Node] = [
kind: .codeBlock,
base: .syntax,
nameForDiagnostics: "code block",
parserFunction: "parseCodeBlock",
traits: [
"Braced",
"WithStatements",
Expand Down
8 changes: 7 additions & 1 deletion CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ public let DECL_NODES: [Node] = [
base: .decl,
nameForDiagnostics: "accessor",
parserFunction: "parseAccessorDecl",
traits: ["WithAttributes"],
traits: [
"WithOptionalCodeBlock",
"WithAttributes",
],
children: [
Child(
name: "attributes",
Expand Down Expand Up @@ -493,6 +496,7 @@ public let DECL_NODES: [Node] = [
traits: [
"WithAttributes",
"WithModifiers",
"WithOptionalCodeBlock",
],
children: [
Child(
Expand Down Expand Up @@ -879,6 +883,7 @@ public let DECL_NODES: [Node] = [
"WithAttributes",
"WithGenericParameters",
"WithModifiers",
"WithOptionalCodeBlock",
],
children: [
Child(
Expand Down Expand Up @@ -1217,6 +1222,7 @@ public let DECL_NODES: [Node] = [
"WithAttributes",
"WithGenericParameters",
"WithModifiers",
"WithOptionalCodeBlock",
],
children: [
Child(
Expand Down
6 changes: 6 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/Traits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ public let TRAITS: [Trait] = [
Child(name: "modifiers", kind: .node(kind: .declModifierList))
]
),
Trait(
traitName: "WithOptionalCodeBlock",
children: [
Child(name: "body", kind: .node(kind: .codeBlock), isOptional: true)
]
),
Trait(
traitName: "WithStatements",
children: [
Expand Down
4 changes: 4 additions & 0 deletions Release Notes/511.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
- Description: Enum to exhaustively switch over all different syntax nodes of each base type.
- Pull Request: https://github.com/apple/swift-syntax/pull/2351

- `WithOptionalCodeBlock`
- Description: A trait for syntax nodes that have an optional code block, such as `FunctionDeclSyntax` and `InitializerDeclSyntax`.
- Pull Request: https://github.com/apple/swift-syntax/pull/2359

## API Behavior Changes

## Deprecations
Expand Down
6 changes: 4 additions & 2 deletions Sources/SwiftCompilerPluginMessageHandling/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import SwiftBasicFormat
import SwiftDiagnostics
import SwiftOperators
import SwiftSyntax
import SwiftSyntaxMacroExpansion
import SwiftSyntaxMacros
@_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacroExpansion
@_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros

extension CompilerPluginMessageHandler {
/// Get concrete macro type from a pair of module name and type name.
Expand Down Expand Up @@ -166,6 +166,8 @@ private extension MacroRole {
case .conformance: self = .extension
case .codeItem: self = .codeItem
case .extension: self = .extension
case .preamble: self = .preamble
case .body: self = .body
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ public enum PluginMessage {
case conformance
case codeItem
case `extension`
@_spi(ExperimentalLanguageFeature) case preamble
@_spi(ExperimentalLanguageFeature) case body
}

public struct SourceLocation: Codable {
Expand Down
18 changes: 18 additions & 0 deletions Sources/SwiftParser/generated/LayoutNodes+Parsable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ extension CodeBlockItemSyntax: SyntaxParseable {
}
}

extension CodeBlockSyntax: SyntaxParseable {
public static func parse(from parser: inout Parser) -> Self {
// Keep the parser alive so that the arena in which `raw` is allocated
// doesn’t get deallocated before we have a chance to create a syntax node
// from it. We can’t use `parser.arena` as the parameter to
// `Syntax(raw:arena:)` because the node might have been re-used during an
// incremental parse and would then live in a different arena than
// `parser.arena`.
defer {
withExtendedLifetime(parser) {
}
}
let node = parser.parseCodeBlock()
let raw = RawSyntax(parser.parseRemainder(into: node))
return Syntax(raw: raw, rawNodeArena: raw.arena).cast(Self.self)
}
}

extension DeclSyntax: SyntaxParseable {
public static func parse(from parser: inout Parser) -> Self {
// Keep the parser alive so that the arena in which `raw` is allocated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ These articles are intended for developers wishing to contribute to SwiftSyntax
- <doc:SwiftSyntax/WithCodeBlockSyntax>
- <doc:SwiftSyntax/WithGenericParametersSyntax>
- <doc:SwiftSyntax/WithModifiersSyntax>
- <doc:SwiftSyntax/WithOptionalCodeBlockSyntax>
- <doc:SwiftSyntax/WithStatementsSyntax>
- <doc:SwiftSyntax/WithTrailingCommaSyntax>

Expand Down
45 changes: 41 additions & 4 deletions Sources/SwiftSyntax/generated/SyntaxTraits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,43 @@ public extension SyntaxProtocol {
}
}

// MARK: - WithOptionalCodeBlockSyntax


public protocol WithOptionalCodeBlockSyntax: SyntaxProtocol {
var body: CodeBlockSyntax? {
get
set
}
}

public extension WithOptionalCodeBlockSyntax {
/// Without this function, the `with` function defined on `SyntaxProtocol`
/// does not work on existentials of this protocol type.
@_disfavoredOverload
func with<T>(_ keyPath: WritableKeyPath<WithOptionalCodeBlockSyntax, T>, _ newChild: T) -> WithOptionalCodeBlockSyntax {
var copy: WithOptionalCodeBlockSyntax = self
copy[keyPath: keyPath] = newChild
return copy
}
}

public extension SyntaxProtocol {
/// Check whether the non-type erased version of this syntax node conforms to
/// `WithOptionalCodeBlockSyntax`.
/// Note that this will incur an existential conversion.
func isProtocol(_: WithOptionalCodeBlockSyntax.Protocol) -> Bool {
return self.asProtocol(WithOptionalCodeBlockSyntax.self) != nil
}

/// Return the non-type erased version of this syntax node if it conforms to
/// `WithOptionalCodeBlockSyntax`. Otherwise return `nil`.
/// Note that this will incur an existential conversion.
func asProtocol(_: WithOptionalCodeBlockSyntax.Protocol) -> WithOptionalCodeBlockSyntax? {
return Syntax(self).asProtocol(SyntaxProtocol.self) as? WithOptionalCodeBlockSyntax
}
}

// MARK: - WithStatementsSyntax


Expand Down Expand Up @@ -649,7 +686,7 @@ public extension SyntaxProtocol {

extension AccessorBlockSyntax: BracedSyntax {}

extension AccessorDeclSyntax: WithAttributesSyntax {}
extension AccessorDeclSyntax: WithOptionalCodeBlockSyntax, WithAttributesSyntax {}

extension AccessorEffectSpecifiersSyntax: EffectSpecifiersSyntax {}

Expand Down Expand Up @@ -693,7 +730,7 @@ extension DeclNameArgumentsSyntax: ParenthesizedSyntax {}

extension DeferStmtSyntax: WithCodeBlockSyntax {}

extension DeinitializerDeclSyntax: WithAttributesSyntax, WithModifiersSyntax {}
extension DeinitializerDeclSyntax: WithAttributesSyntax, WithModifiersSyntax, WithOptionalCodeBlockSyntax {}

extension DictionaryElementSyntax: WithTrailingCommaSyntax {}

Expand Down Expand Up @@ -723,7 +760,7 @@ extension ExtensionDeclSyntax: DeclGroupSyntax, WithAttributesSyntax, WithModifi

extension ForStmtSyntax: WithCodeBlockSyntax {}

extension FunctionDeclSyntax: NamedDeclSyntax, WithAttributesSyntax, WithGenericParametersSyntax, WithModifiersSyntax {}
extension FunctionDeclSyntax: NamedDeclSyntax, WithAttributesSyntax, WithGenericParametersSyntax, WithModifiersSyntax, WithOptionalCodeBlockSyntax {}

extension FunctionEffectSpecifiersSyntax: EffectSpecifiersSyntax {}

Expand All @@ -747,7 +784,7 @@ extension ImportDeclSyntax: WithAttributesSyntax, WithModifiersSyntax {}

extension InheritedTypeSyntax: WithTrailingCommaSyntax {}

extension InitializerDeclSyntax: WithAttributesSyntax, WithGenericParametersSyntax, WithModifiersSyntax {}
extension InitializerDeclSyntax: WithAttributesSyntax, WithGenericParametersSyntax, WithModifiersSyntax, WithOptionalCodeBlockSyntax {}

extension LabeledExprSyntax: WithTrailingCommaSyntax {}

Expand Down
21 changes: 4 additions & 17 deletions Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,9 @@ extension SyntaxStringInterpolation {
}
}

// MARK: - HasCodeBlock

public protocol HasTrailingCodeBlock {
var body: CodeBlockSyntax { get set }
// MARK: - HasTrailingCodeBlock

public protocol HasTrailingCodeBlock: WithCodeBlockSyntax {
/// Constructs a syntax node where `header` builds the text of the node before the body in braces and `bodyBuilder` is used to build the node’s body.
///
/// For example, you can construct
Expand Down Expand Up @@ -90,11 +88,9 @@ extension ForStmtSyntax: HasTrailingCodeBlock {}
extension GuardStmtSyntax: HasTrailingCodeBlock {}
extension WhileStmtSyntax: HasTrailingCodeBlock {}

// MARK: - HasOptionalCodeBlock

public protocol HasTrailingOptionalCodeBlock {
var body: CodeBlockSyntax? { get set }
// MARK: - WithOptionalCodeBlockSyntax

public extension WithOptionalCodeBlockSyntax where Self: DeclSyntaxProtocol {
/// Constructs a syntax node where `header` builds the text of the node before the body in braces and `bodyBuilder` is used to build the node’s body.
///
/// For example, you can construct
Expand All @@ -114,10 +110,6 @@ public protocol HasTrailingOptionalCodeBlock {
/// ```
///
/// Throws an error if `header` defines a different node type than the type the initializer is called on. E.g. if calling `try FunctionDeclSyntax("init") {}`
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws
}

public extension HasTrailingOptionalCodeBlock where Self: DeclSyntaxProtocol {
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws {
let decl = DeclSyntax("\(header) {}")
guard let castedDecl = decl.as(Self.self) else {
Expand All @@ -128,11 +120,6 @@ public extension HasTrailingOptionalCodeBlock where Self: DeclSyntaxProtocol {
}
}

extension AccessorDeclSyntax: HasTrailingOptionalCodeBlock {}
extension DeinitializerDeclSyntax: HasTrailingOptionalCodeBlock {}
extension FunctionDeclSyntax: HasTrailingOptionalCodeBlock {}
extension InitializerDeclSyntax: HasTrailingOptionalCodeBlock {}

// MARK: HasTrailingMemberDeclBlock

public protocol HasTrailingMemberDeclBlock {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ extension ClosureParameterSyntax: SyntaxExpressibleByStringInterpolation {}

extension CodeBlockItemSyntax: SyntaxExpressibleByStringInterpolation {}

extension CodeBlockSyntax: SyntaxExpressibleByStringInterpolation {}

extension DeclSyntax: SyntaxExpressibleByStringInterpolation {}

extension EnumCaseParameterSyntax: SyntaxExpressibleByStringInterpolation {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ extension FunctionParameterSyntax {
///
/// The parameter names for these three parameters are `a`, `b`, and `see`,
/// respectively.
var parameterName: TokenSyntax? {
@_spi(Testing)
public var parameterName: TokenSyntax? {
// If there were two names, the second is the parameter name.
if let secondName {
if secondName.text == "_" {
Expand Down
53 changes: 50 additions & 3 deletions Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import SwiftBasicFormat
import SwiftSyntax
@_spi(MacroExpansion) import SwiftSyntaxMacros
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros

public enum MacroRole {
case expression
Expand All @@ -24,6 +24,8 @@ public enum MacroRole {
case conformance
case codeItem
case `extension`
@_spi(ExperimentalLanguageFeature) case preamble
@_spi(ExperimentalLanguageFeature) case body
}

extension MacroRole {
Expand All @@ -38,18 +40,23 @@ extension MacroRole {
case .conformance: return "ConformanceMacro"
case .codeItem: return "CodeItemMacro"
case .extension: return "ExtensionMacro"
case .preamble: return "PreambleMacro"
case .body: return "BodyMacro"
}
}
}

/// Simple diagnostic message
private enum MacroExpansionError: Error, CustomStringConvertible {
enum MacroExpansionError: Error, CustomStringConvertible {
case unmatchedMacroRole(Macro.Type, MacroRole)
case parentDeclGroupNil
case declarationNotDeclGroup
case declarationNotIdentified
case declarationHasNoBody
case noExtendedTypeSyntax
case noFreestandingMacroRoles(Macro.Type)
case moreThanOneBodyMacro
case preambleWithoutBody

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

case .declarationHasNoBody:
return "declaration is not a type with an optional code block"

case .noExtendedTypeSyntax:
return "no extended type for extension macro"

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

case .moreThanOneBodyMacro:
return "function can not have more than one body macro applied to it"

case .preambleWithoutBody:
return "preamble macro cannot be applied to a function with no body"
}
}
}
Expand Down Expand Up @@ -125,7 +140,7 @@ public func expandFreestandingMacro(
expandedSyntax = Syntax(CodeBlockItemListSyntax(rewritten))

case (.accessor, _), (.memberAttribute, _), (.member, _), (.peer, _), (.conformance, _), (.extension, _), (.expression, _), (.declaration, _),
(.codeItem, _):
(.codeItem, _), (.preamble, _), (.body, _):
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
}
return expandedSyntax.formattedExpansion(definition.formatMode, indentationWidth: indentationWidth)
Expand Down Expand Up @@ -288,6 +303,38 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
$0.formattedExpansion(definition.formatMode, indentationWidth: indentationWidth)
}

case (let attachedMacro as PreambleMacro.Type, .preamble):
guard let declToPass = Syntax(declarationNode).asProtocol(SyntaxProtocol.self) as? (DeclSyntaxProtocol & WithOptionalCodeBlockSyntax)
else {
// Compiler error: declaration must have a body.
throw MacroExpansionError.declarationHasNoBody
}

let preamble = try attachedMacro.expansion(
of: attributeNode,
providingPreambleFor: declToPass,
in: context
)
return preamble.map {
$0.formattedExpansion(definition.formatMode, indentationWidth: indentationWidth)
}

case (let attachedMacro as BodyMacro.Type, .body):
Copy link
Contributor

@ktoso ktoso Jul 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, this doesn't trigger if the function we're attaching the macro to has a body, but that's the case we'd want -- am I confused about something here or are we talking about different kinds of macros perhaps?

E.g.

      @Traced
      func hello(one: String, two: Int) async throws -> String {
        return "\(one) + \(two)"
      }

is expected usage of traced, but such snippet does not trigger this macro. The test case also specifically is about a body-less function, like

      @Traced
      func hello(one: String, two: Int) async throws -> String

(which does trigger the macro). Is this just "provide a body macro" vs the "replace the body macro" which could write a macro as follows:

struct TracedBodyMacro: BodyMacro {
  static func expansion(
    of node: AttributeSyntax,
    providingBodyFor declaration: some DeclSyntaxProtocol & HasTrailingOptionalCodeBlock,
    in context: some MacroExpansionContext
  ) throws -> [CodeBlockItemSyntax] {

    // FIXME: Should be able to support (de-)initializers and accessors as
    // well, but this is a lazy implementation.
    guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
      return []
    }

    guard let body = declaration.body else {
      return []
    }

    let funcBaseName = funcDecl.identifier.text
    let paramNames = funcDecl.signature.input.parameterList.map { param in
      if let name = param.parameterName {
        return "\(name.text):"
      } else {
        return "_:"
      }
    }
    let fullName = "\(funcBaseName)(\(paramNames))"

    return [
      """
      return try await withSpan(\(literal: fullName)) {
        \(body)
      }
      """
    ]
  }
}

test:

  func testTracedBodyExpansion() {
    assertMacroExpansion(
      """
      @Traced
      func hello(one: String, two: Int) async throws -> String {
        return "\\(one) + \\(two)"
      }
      """,
      expandedSource: """

        func hello(one: String, two: Int) async throws -> String {
          return try await withSpan("hello(one:two:)") {
            return "\\(one) + \\(two)"
          }
        }
        """,
      macros: testMacros,
      indentationWidth: indentationWidth
    )
  }

since we need to wrap the code since that's the only official way to do task-locals; (We could abuse the internal "push tasklocal" and "pop tasklocal" but I'd rather not 🤔)

I could be just mis-expecting this "body macro" as the "body replacement macro" if there's to be different things for those?

guard let declToPass = Syntax(declarationNode).asProtocol(SyntaxProtocol.self) as? (DeclSyntaxProtocol & WithOptionalCodeBlockSyntax)
else {
// Compiler error: declaration must have a body.
throw MacroExpansionError.declarationHasNoBody
}

let body = try attachedMacro.expansion(
of: attributeNode,
providingBodyFor: declToPass,
in: context
)
return body.map {
$0.formattedExpansion(definition.formatMode, indentationWidth: indentationWidth)
}

default:
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
}
Expand Down
Loading