Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2023 Developer ToolsSwift

WWDC23 · 34 min · Developer Tools / Swift

Write Swift macros

Discover how you can use Swift macros to make your codebase more expressive and easier to read. Code along as we explore how macros can help you avoid writing repetitive code and find out how to use them in your app. We’ll share the building blocks of a macro, show you how to test it, and take you through how you can emit compilation errors from macros.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

  • 1:15 — Overview
  • 5:10 — Create a macro using Xcode's macro template
  • 10:50 — Macro roles
  • 11:40 — Write a SlopeSubset macro to define an enum subset
  • 20:17 — Inspect the syntax tree structure in the debugger
  • 24:35 — Add a macro to an Xcode project
  • 27:05 — Emit error messages from a macro
  • 30:12 — Generalize SlopeSubset to a generic EnumSubset macro

Code shown on screen · 21 snippets

Invocation of the stringify macro swift · at 5:55 ↗
import WWDC

let a = 17
let b = 25

let (result, code) = #stringify(a + b)

print("The value \(result) was produced by the code \"\(code)\"")
Declaration of the stringify macro swift · at 6:31 ↗
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "WWDCMacros", type: "StringifyMacro")
Implementation of the stringify macro swift · at 7:10 ↗
public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.argumentList.first?.expression else {
            fatalError("compiler bug: the macro does not have any arguments")
        }

        return "(\(argument), \(literal: argument.description))"
    }
}
Tests for the stringify Macro swift · at 9:12 ↗
final class WWDCTests: XCTestCase {
    func testMacro() {
        assertMacroExpansion(
            """
            #stringify(a + b)
            """,
            expandedSource: """
            (a + b, "a + b")
            """,
            macros: testMacros
        )
    }
}

let testMacros: [String: Macro.Type] = [
    "stringify": StringifyMacro.self
]
Slope and EasySlope swift · at 12:05 ↗
/// Slopes in my favorite ski resort.
enum Slope {
    case beginnersParadise
    case practiceRun
    case livingRoom
    case olympicRun
    case blackBeauty
}

/// Slopes suitable for beginners. Subset of `Slopes`.
enum EasySlope {
    case beginnersParadise
    case practiceRun

    init?(_ slope: Slope) {
        switch slope {
        case .beginnersParadise: self = .beginnersParadise
        case .practiceRun: self = .practiceRun
        default: return nil
        }
    }

    var slope: Slope {
        switch self {
        case .beginnersParadise: return .beginnersParadise
        case .practiceRun: return .practiceRun
        }
    }
}
Declare SlopeSubset swift · at 14:16 ↗
/// Defines a subset of the `Slope` enum
///
/// Generates two members:
///  - An initializer that converts a `Slope` to this type if the slope is
///    declared in this subset, otherwise returns `nil`
///  - A computed property `slope` to convert this type to a `Slope`
///
/// - Important: All enum cases declared in this macro must also exist in the
///              `Slope` enum.
@attached(member, names: named(init))
public macro SlopeSubset() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro")
Write empty implementation for SlopeSubset swift · at 15:24 ↗
/// Implementation of the `SlopeSubset` macro.
public struct SlopeSubsetMacro: MemberMacro {
    public static func expansion(
        of attribute: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        return []
    }
}
Register SlopeSubsetMacro in the compiler plugin swift · at 16:23 ↗
@main
struct WWDCPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        SlopeSubsetMacro.self
    ]
}
Test SlopeSubset swift · at 18:41 ↗
let testMacros: [String: Macro.Type] = [
    "SlopeSubset" : SlopeSubsetMacro.self,
]

final class WWDCTests: XCTestCase {
    func testSlopeSubset() {
        assertMacroExpansion(
            """
            @SlopeSubset
            enum EasySlope {
                case beginnersParadise
                case practiceRun
            }
            """, 
            expandedSource: """

            enum EasySlope {
                case beginnersParadise
                case practiceRun
                init?(_ slope: Slope) {
                    switch slope {
                    case .beginnersParadise:
                        self = .beginnersParadise
                    case .practiceRun:
                        self = .practiceRun
                    default:
                        return nil
                    }
                }
            }
            """, 
            macros: testMacros
        )
    }
}
Cast declaration to an enum declaration swift · at 19:25 ↗
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
    // TODO: Emit an error here
    return []
}
Extract enum members swift · at 21:14 ↗
let members = enumDecl.memberBlock.members
Load enum cases swift · at 21:32 ↗
let caseDecls = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
Retrieve enum elements swift · at 21:58 ↗
let elements = caseDecls.flatMap { $0.elements }
Generate initializer swift · at 24:11 ↗
let initializer = try InitializerDeclSyntax("init?(_ slope: Slope)") {
    try SwitchExprSyntax("switch slope") {
        for element in elements {
            SwitchCaseSyntax(
                """
                case .\(element.identifier):
                    self = .\(element.identifier)
                """
            )
        }
        SwitchCaseSyntax("default: return nil")
    }
}
Return generated initializer swift · at 24:19 ↗
return [DeclSyntax(initializer)]
Apply SlopeSubset to EasySlope swift · at 25:51 ↗
/// Slopes suitable for beginners. Subset of `Slopes`.
@SlopeSubset
enum EasySlope {
    case beginnersParadise
    case practiceRun

    var slope: Slope {
        switch self {
        case .beginnersParadise: return .beginnersParadise
        case .practiceRun: return .practiceRun
        }
    }
}
Test that we generate an error when applying SlopeSubset to a struct swift · at 28:00 ↗
func testSlopeSubsetOnStruct() throws {
    assertMacroExpansion(
        """
        @SlopeSubset
        struct Skier {
        }
        """,
        expandedSource: """

        struct Skier {
        }
        """,
        diagnostics: [
            DiagnosticSpec(message: "@SlopeSubset can only be applied to an enum", line: 1, column: 1)
        ],
        macros: testMacros
    )
}
Define error to emit when SlopeSubset is applied to a non-enum type swift · at 28:48 ↗
enum SlopeSubsetError: CustomStringConvertible, Error {
    case onlyApplicableToEnum
    
    var description: String {
        switch self {
        case .onlyApplicableToEnum: return "@SlopeSubset can only be applied to an enum"
        }
    }
}
Throw error if SlopeSubset is applied to a non-enum type swift · at 29:09 ↗
throw SlopeSubsetError.onlyApplicableToEnum
Generalize SlopeSubset declaration to EnumSubset swift · at 31:03 ↗
@attached(member, names: named(init))
public macro EnumSubset<Superset>() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro")
Retrieve the generic parameter of EnumSubset swift · at 31:33 ↗
guard let supersetType = attribute
    .attributeName.as(SimpleTypeIdentifierSyntax.self)?
    .genericArgumentClause?
    .arguments.first?
    .argumentType else {
    // TODO: Handle error
    return []
}