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

2023 Developer ToolsSwift

WWDC23 · 40 min · Developer Tools / Swift

Expand on Swift macros

Discover how Swift macros can help you reduce boilerplate in your codebase and adopt complex features more easily. Learn how macros can analyze code, emit rich compiler errors to guide developers towards correct usage, and generate new code that is automatically incorporated back into your project. We’ll also take you through important concepts like macro roles, compiler plugins, and syntax trees.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

Code shown on screen · 47 snippets

The #unwrap expression macro, with a more complicated argument swift · at 0:44 ↗
let image = #unwrap(request.downloadedImage, message: "was already checked")

            // Begin expansion for "#unwrap"
            { [wrappedValue = request.downloadedImage] in
                guard let wrappedValue else {
                    preconditionFailure(
                        "Unexpectedly found nil: ‘request.downloadedImage’ " + 
                            "was already checked",
                        file: "main/ImageLoader.swift",
                        line: 42
                    )
                }
                return wrappedValue
            }()
            // End expansion for "#unwrap"
Existing features using expansions (1) swift · at 0:50 ↗
struct Smoothie: Codable {
    var id, title, description: String
    var measuredIngredients: [MeasuredIngredient]

    static let berryBlue =
        Smoothie(id: "berry-blue", title: "Berry Blue") {
            """
            Filling and refreshing, this smoothie \
            will fill you with joy!
            """

            Ingredient.orange
                .measured(with: .cups).scaled(by: 1.5)
            Ingredient.blueberry
                .measured(with: .cups)
            Ingredient.avocado
                .measured(with: .cups).scaled(by: 0.2)
        }
}
Existing features using expansions (2) swift · at 1:11 ↗
struct Smoothie: Codable {
    var id, title, description: String
    var measuredIngredients: [MeasuredIngredient]

        // Begin expansion for Codable
        private enum CodingKeys: String, CodingKey {
            case id, title, description,
                 measuredIngredients
        }

        init(from decoder: Decoder) throws {  }

        func encode(to encoder Encoder) throws {  }
        // End expansion for Codable

    static let berryBlue =
        Smoothie(id: "berry-blue", title: "Berry Blue") {
            """
            Filling and refreshing, this smoothie \
            will fill you with joy!
            """

            Ingredient.orange
                .measured(with: .cups).scaled(by: 1.5)
            Ingredient.blueberry
                .measured(with: .cups)
            Ingredient.avocado
                .measured(with: .cups).scaled(by: 0.2)
        }
}
Macros inputs are complete, type-checked, and validated swift · at 3:16 ↗
#unwrap(1 + )    // error: expected expression after operator




@AddCompletionHandler(parameterName: 42)    // error: cannot convert argument of type 'Int' to expected type 'String'
func sendRequest() async throws -> Response




@DictionaryStorage class Options {  }    // error: '@DictionaryStorage' can only be applied to a 'struct'
Macro expansions are inserted in predictable ways swift · at 3:45 ↗
func doThingy() {
    startDoingThingy()

    #someUnknownMacro()

    finishDoingThingy()
}
How macros work, featuring #stringify swift · at 4:51 ↗
func printAdd(_ a: Int, _ b: Int) {
    let (result, str) = #stringify(a + b)
  
        // Begin expansion for "#stringify"
        (a + b, "a + b")
        // End expansion for "#stringify"
  
    print("\(str) = \(result)")
}

printAdd(1, 2)    // prints "a + b = 3"
Macro declaration for #stringify swift · at 5:43 ↗
/// Creates a tuple containing both the result of `expr` and its source code represented as a
/// `String`.
@freestanding(expression)
macro stringify<T>(_ expr: T) -> (T, String)
What’s an expression? swift · at 7:11 ↗
let numPixels = (x + width) * (y + height)
//              ^~~~~~~~~~~~~~~~~~~~~~~~~~ This is an expression
//               ^~~~~~~~~                 But so is this
//                   ^~~~~                 And this
The #unwrap expression macro: motivation swift · at 7:34 ↗
// Some teams are nervous about this:
let image = downloadedImage!

// Alternatives are super wordy:
guard let image = downloadedImage else {
    preconditionFailure("Unexpectedly found nil: downloadedImage was already checked")
}
The #unwrap expression macro: macro declaration swift · at 8:03 ↗
/// Force-unwraps the optional value passed to `expr`.
/// - Parameter message: Failure message, followed by `expr` in single quotes
@freestanding(expression)
macro unwrap<Wrapped>(_ expr: Wrapped?, message: String) -> Wrapped
The #unwrap expression macro: usage swift · at 8:21 ↗
let image = #unwrap(downloadedImage, message: "was already checked")

            // Begin expansion for "#unwrap"
            { [downloadedImage] in
                guard let downloadedImage else {
                    preconditionFailure(
                        "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked",
                        file: "main/ImageLoader.swift",
                        line: 42
                    )
                }
                return downloadedImage
            }()
            // End expansion for "#unwrap"
The #makeArrayND declaration macro: motivation swift · at 9:09 ↗
public struct Array2D<Element>: Collection {
    public struct Index: Hashable, Comparable { var storageIndex: Int }
  
    var storage: [Element]
    var width1: Int
  
    public func makeIndex(_ i0: Int, _ i1: Int) -> Index {
        Index(storageIndex: i0 * width1 + i1)
    }
  
    public subscript (_ i0: Int, _ i1: Int) -> Element {
        get { self[makeIndex(i0, i1)] }
        set { self[makeIndex(i0, i1)] = newValue }
    }

    public subscript (_ i: Index) -> Element {
        get { storage[i.storageIndex] }
        set { storage[i.storageIndex] = newValue }
    }

    // Note: Omitted additional members needed for 'Collection' conformance
}

public struct Array3D<Element>: Collection {
    public struct Index: Hashable, Comparable { var storageIndex: Int }
  
    var storage: [Element]
    var width1, width2: Int
  
    public func makeIndex(_ i0: Int, _ i1: Int, _ i2: Int) -> Index {
        Index(storageIndex: (i0 * width1 + i1) * width2 + i2)
    }
  
    public subscript (_ i0: Int, _ i1: Int, _ i2: Int) -> Element {
        get { self[makeIndex(i0, i1, i2)] }
        set { self[makeIndex(i0, i1, i2)] = newValue }
    }
  
    public subscript (_ i: Index) -> Element {
        get { storage[i.storageIndex] }
        set { storage[i.storageIndex] = newValue }
    }
  
    // Note: Omitted additional members needed for 'Collection' conformance
}
The #makeArrayND declaration macro: macro declaration swift · at 10:03 ↗
/// Declares an `n`-dimensional array type named `Array<n>D`.
/// - Parameter n: The number of dimensions in the array.
@freestanding(declaration, names: arbitrary)
macro makeArrayND(n: Int)
The #makeArrayND declaration macro: usage swift · at 10:15 ↗
#makeArrayND(n: 2)

// Begin expansion for "#makeArrayND"
public struct Array2D<Element>: Collection {
    public struct Index: Hashable, Comparable { var storageIndex: Int }
    var storage: [Element]
    var width1: Int
    public func makeIndex(_ i0: Int, _ i1: Int) -> Index {
        Index(storageIndex: i0 * width1 + i1)
    }
    public subscript (_ i0: Int, _ i1: Int) -> Element {
        get { self[makeIndex(i0, i1)] }
        set { self[makeIndex(i0, i1)] = newValue }
    }
    public subscript (_ i: Index) -> Element {
        get { storage[i.storageIndex] }
        set { storage[i.storageIndex] = newValue }
    }
}
// End expansion for "#makeArrayND"

#makeArrayND(n: 3)
#makeArrayND(n: 4)
#makeArrayND(n: 5)
The @AddCompletionHandler peer macro: motivation swift · at 11:23 ↗
/// Fetch the avatar for the user with `username`.
func fetchAvatar(_ username: String) async -> Image? {
    ...
}

func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) {
    Task.detached { onCompletion(await fetchAvatar(username)) }
}
The @AddCompletionHandler peer macro: macro declaration swift · at 11:51 ↗
/// Overload an `async` function to add a variant that takes a completion handler closure as
/// a parameter.
@attached(peer, names: overloaded)
macro AddCompletionHandler(parameterName: String = "completionHandler")
The @AddCompletionHandler peer macro: usage swift · at 11:59 ↗
/// Fetch the avatar for the user with `username`.
@AddCompletionHandler(parameterName: "onCompletion")
func fetchAvatar(_ username: String) async -> Image? {
    ...
}

    // Begin expansion for "@AddCompletionHandler"

    /// Fetch the avatar for the user with `username`.
    /// Equivalent to ``fetchAvatar(username:)`` with
    /// a completion handler.
    func fetchAvatar(
        _ username: String,
        onCompletion: @escaping (Image?) -> Void
    ) {
        Task.detached {
            onCompletion(await fetchAvatar(username))
        }
    }

    // End expansion for "@AddCompletionHandler"
The @DictionaryStorage accessor macro: motivation swift · at 12:36 ↗
struct Person: DictionaryRepresentable {
    init(dictionary: [String: Any]) { self.dictionary = dictionary }
    var dictionary: [String: Any]
  
    var name: String {
        get { dictionary["name"]! as! String }
        set { dictionary["name"] = newValue }
    }
    var height: Measurement<UnitLength> {
        get { dictionary["height"]! as! Measurement<UnitLength> }
        set { dictionary["height"] = newValue }
    }
    var birthDate: Date? {
        get { dictionary["birth_date"] as! Date? }
        set { dictionary["birth_date"] = newValue as Any? }
    }
}
The @DictionaryStorage accessor macro: declaration swift · at 13:04 ↗
/// Adds accessors to get and set the value of the specified property in a dictionary
/// property called `storage`.
@attached(accessor)
macro DictionaryStorage(key: String? = nil)
The @DictionaryStorage accessor macro: usage swift · at 13:20 ↗
struct Person: DictionaryRepresentable {
    init(dictionary: [String: Any]) { self.dictionary = dictionary }
    var dictionary: [String: Any]
    
    @DictionaryStorage var name: String
        // Begin expansion for "@DictionaryStorage"
        {
            get { dictionary["name"]! as! String }
            set { dictionary["name"] = newValue }
        }
        // End expansion for "@DictionaryStorage"

    @DictionaryStorage var height: Measurement<UnitLength>
        // Begin expansion for "@DictionaryStorage"
        {
            get { dictionary["height"]! as! Measurement<UnitLength> }
            set { dictionary["height"] = newValue }
        }
        // End expansion for "@DictionaryStorage"

    @DictionaryStorage(key: "birth_date") var birthDate: Date?
        // Begin expansion for "@DictionaryStorage"
        {
            get { dictionary["birth_date"] as! Date? }
            set { dictionary["birth_date"] = newValue as Any? }
        }
        // End expansion for "@DictionaryStorage"
}
The @DictionaryStorage member attribute macro: macro declaration swift · at 13:56 ↗
/// Adds accessors to get and set the value of the specified property in a dictionary
/// property called `storage`.
@attached(memberAttribute)
@attached(accessor)
macro DictionaryStorage(key: String? = nil)
The @DictionaryStorage member attribute macro: usage swift · at 14:46 ↗
@DictionaryStorage
struct Person: DictionaryRepresentable {
    init(dictionary: [String: Any]) { self.dictionary = dictionary }
    var dictionary: [String: Any]
    
        // Begin expansion for "@DictionaryStorage"
        @DictionaryStorage
        // End expansion for "@DictionaryStorage"
    var name: String
    
        // Begin expansion for "@DictionaryStorage"
        @DictionaryStorage
        // End expansion for "@DictionaryStorage"
    var height: Measurement<UnitLength>

    @DictionaryStorage(key: "birth_date") var birthDate: Date?
}
The @DictionaryStorage member macro: macro definition swift · at 15:52 ↗
/// Adds accessors to get and set the value of the specified property in a dictionary
/// property called `storage`.
@attached(member, names: named(dictionary), named(init(dictionary:)))
@attached(memberAttribute)
@attached(accessor)
macro DictionaryStorage(key: String? = nil)
The @DictionaryStorage member macro: usage swift · at 16:26 ↗
// The @DictionaryStorage member macro

@DictionaryStorage struct Person: DictionaryRepresentable {
        // Begin expansion for "@DictionaryStorage"
        init(dictionary: [String: Any]) {
            self.dictionary = dictionary
        }
        var dictionary: [String: Any]
        // End expansion for "@DictionaryStorage"
    
    var name: String
    var height: Measurement<UnitLength>
    @DictionaryStorage(key: "birth_date") var birthDate: Date?
}
The @DictionaryStorage conformance macro: macro definition swift · at 16:59 ↗
/// Adds accessors to get and set the value of the specified property in a dictionary
/// property called `storage`.
@attached(conformance)
@attached(member, names: named(dictionary), named(init(dictionary:)))
@attached(memberAttribute)
@attached(accessor)
macro DictionaryStorage(key: String? = nil)
The @DictionaryStorage conformance macro: usage swift · at 17:09 ↗
struct Person
        // Begin expansion for "@DictionaryStorage"
        : DictionaryRepresentable
        // End expansion for "@DictionaryStorage"
{
    var name: String
    var height: Measurement<UnitLength>
    @DictionaryStorage(key: "birth_date") var birthDate: Date?
}
@DictionaryStorage starting point swift · at 17:28 ↗
struct Person: DictionaryRepresentable {
    init(dictionary: [String: Any]) { self.dictionary = dictionary }
    var dictionary: [String: Any]

    var name: String {
        get { dictionary["name"]! as! String }
        set { dictionary["name"] = newValue }
    }
    var height: Measurement<UnitLength> {
        get { dictionary["height"]! as! Measurement<UnitLength> }
        set { dictionary["height"] = newValue }
    }
    var birthDate: Date? {
        get { dictionary["birth_date"] as! Date? }
        set { dictionary["birth_date"] = newValue as Any? }
    }
}
@DictionaryStorage ending point swift · at 17:32 ↗
@DictionaryStorage
struct Person
        // Begin expansion for "@DictionaryStorage"
        : DictionaryRepresentable
        // End expansion for "@DictionaryStorage"
{
        // Begin expansion for "@DictionaryStorage"
        init(dictionary: [String: Any]) { self.dictionary = dictionary }
        var dictionary: [String: Any]
        // End expansion for "@DictionaryStorage"

        // Begin expansion for "@DictionaryStorage"
        @DictionaryStorage
        // End expansion for "@DictionaryStorage"
    var name: String
        // Begin expansion for "@DictionaryStorage"
        {
            get { dictionary["name"]! as! String }
            set { dictionary["name"] = newValue }
        }
        // End expansion for "@DictionaryStorage"

        // Begin expansion for "@DictionaryStorage"
        @DictionaryStorage
        // End expansion for "@DictionaryStorage"
    var height: Measurement<UnitLength>
        // Begin expansion for "@DictionaryStorage"
        {
            get { dictionary["height"]! as! Measurement<UnitLength> }
            set { dictionary["height"] = newValue }
        }
        // End expansion for "@DictionaryStorage"

    @DictionaryStorage(key: "birth_date")
    var birthDate: Date?
        // Begin expansion for "@DictionaryStorage"
        {
            get { dictionary["birth_date"] as! Date? }
            set { dictionary["birth_date"] = newValue as Any? }
        }
        // End expansion for "@DictionaryStorage"
}
@DictionaryStorage ending point (without expansions) swift · at 17:35 ↗
@DictionaryStorage
struct Person {
    var name: String
    var height: Measurement<UnitLength>

    @DictionaryStorage(key: "birth_date")
    var birthDate: Date?
}
Macro implementations swift · at 18:01 ↗
/// Creates a tuple containing both the result of `expr` and its source code represented as a
/// `String`.
@freestanding(expression)
macro stringify<T>(_ expr: T) -> (T, String) = #externalMacro(
                                                   module: "MyLibMacros",
                                                   type: "StringifyMacro"
                                               )
Implementing @DictionaryStorage’s @attached(member) role (1) swift · at 19:18 ↗
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftSyntaxBuilder

struct DictionaryStorageMacro: MemberMacro {
    static func expansion(
        of attribute: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        return [
           "init(dictionary: [String: Any]) { self.dictionary = dictionary }",
           "var dictionary: [String: Any]"
        ]
    }
}
Code used to demonstrate SwiftSyntax trees swift · at 19:52 ↗
@DictionaryStorage
struct Person {
    var name: String
    var height: Measurement<UnitLength>
    @DictionaryStorage(key: "birth_date")
    var birthDate: Date?
}
Implementing @DictionaryStorage’s @attached(member) role (2) swift · at 22:00 ↗
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftSyntaxBuilder

struct DictionaryStorageMacro: MemberMacro {
    static func expansion(
        of attribute: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        return [
           "init(dictionary: [String: Any]) { self.dictionary = dictionary }",
           "var dictionary: [String: Any]"
        ]
    }
}
A type that @DictionaryStorage isn’t compatible with swift · at 24:29 ↗
@DictionaryStorage
enum Gender {
    case other(String)
    case female
    case male

        // Begin expansion for "@DictionaryStorage"
        init(dictionary: [String: Any]) { self.dictionary = dictionary }
        var dictionary: [String: Any]
        // End expansion for "@DictionaryStorage"
}
Expansion method with error checking swift · at 25:17 ↗
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftSyntaxBuilder

struct DictionaryStorageMacro: MemberMacro {
    static func expansion(
        of attribute: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard declaration.is(StructDeclSyntax.self) else {
            let structError = Diagnostic(
                node: attribute,
                message: MyLibDiagnostic.notAStruct
            )
            context.diagnose(structError)
            return []
        }
        return [
            "init(dictionary: [String: Any]) { self.dictionary = dictionary }",
            "var dictionary: [String: Any]"
        ]
    }
}

enum MyLibDiagnostic: String, DiagnosticMessage {
    case notAStruct

    var severity: DiagnosticSeverity { return .error }

    var message: String {
        switch self {
        case .notAStruct:
            return "'@DictionaryStorage' can only be applied to a 'struct'"
        }
    }

    var diagnosticID: MessageID {
        MessageID(domain: "MyLibMacros", id: rawValue)
    }
}
Parameter list for `ArrayND.makeIndex` swift · at 29:32 ↗
FunctionParameterListSyntax {
    for dimension in 0 ..< numDimensions {
        FunctionParameterSyntax(
            firstName: .wildcardToken(),
            secondName: .identifier("i\(dimension)"),
            type: TypeSyntax("Int")
        )
    }
}
The #unwrap expression macro: revisited swift · at 30:17 ↗
let image = #unwrap(downloadedImage, message: "was already checked")

            // Begin expansion for "#unwrap"
            { [downloadedImage] in
                guard let downloadedImage else {
                    preconditionFailure(
                        "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked",
                        file: "main/ImageLoader.swift",
                        line: 42
                    )
                }
                return downloadedImage
            }()
            // End expansion for "#unwrap"
Implementing the #unwrap expression macro: start swift · at 30:38 ↗
static func makeGuardStmt() -> StmtSyntax {
    return """
        guard let downloadedImage else {
            preconditionFailure(
                "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked",
                file: "main/ImageLoader.swift",
                line: 42
            )
        }
    """
}
Implementing the #unwrap expression macro: the message string swift · at 30:57 ↗
static func makeGuardStmt(message: ExprSyntax) -> StmtSyntax {
    return """
        guard let downloadedImage else {
            preconditionFailure(
                "Unexpectedly found nil: ‘downloadedImage’ " + \(message),
                file: "main/ImageLoader.swift",
                line: 42
            )
        }
    """
}
Implementing the #unwrap expression macro: the variable name swift · at 31:21 ↗
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax {
    return """
        guard let \(wrapped) else {
            preconditionFailure(
                "Unexpectedly found nil: ‘downloadedImage’ " + \(message),
                file: "main/ImageLoader.swift",
                line: 42
            )
        }
    """
}
Implementing the #unwrap expression macro: interpolating a string as a literal swift · at 31:44 ↗
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax {
    let messagePrefix = "Unexpectedly found nil: ‘downloadedImage’ "

    return """
        guard let \(wrapped) else {
            preconditionFailure(
                \(literal: messagePrefix) + \(message),
                file: "main/ImageLoader.swift",
                line: 42
            )
        }
    """
}
Implementing the #unwrap expression macro: adding an expression as a string swift · at 32:11 ↗
static func makeGuardStmt(wrapped: TokenSyntax,
                           originalWrapped: ExprSyntax,
                           message: ExprSyntax) -> StmtSyntax {
    let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ "

    return """
        guard let \(wrapped) else {
            preconditionFailure(
                \(literal: messagePrefix) + \(message),
                file: "main/ImageLoader.swift",
                line: 42
            )
        }
    """
}
Implementing the #unwrap expression macro: inserting the file and line numbers swift · at 33:00 ↗
static func makeGuardStmt(wrapped: TokenSyntax,
                           originalWrapped: ExprSyntax,
                           message: ExprSyntax,
                           in context: some MacroExpansionContext) -> StmtSyntax {
    let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ "
    let originalLoc = context.location(of: originalWrapped)!

    return """
        guard let \(wrapped) else {
            preconditionFailure(
                \(literal: messagePrefix) + \(message),
                file: \(originalLoc.file),
                line: \(originalLoc.line)
            )
        }
    """
}
The #unwrap expression macro, with a name conflict swift · at 34:05 ↗
let wrappedValue = "🎁"
let image = #unwrap(request.downloadedImage, message: "was \(wrappedValue)")

            // Begin expansion for "#unwrap"
            { [wrappedValue = request.downloadedImage] in
                guard let wrappedValue else {
                    preconditionFailure(
                        "Unexpectedly found nil: ‘request.downloadedImage’ " +
                            "was \(wrappedValue)",
                        file: "main/ImageLoader.swift",
                        line: 42
                    )
                }
                return wrappedValue
            }()
            // End expansion for "#unwrap"
The MacroExpansion.makeUniqueName() method swift · at 34:30 ↗
let captureVar = context.makeUniqueName()

return  """
        { [\(captureVar) = \(originalWrapped)] in
            \(makeGuardStmt(wrapped: captureVar, ))
            \(makeReturnStmt(wrapped: captureVar))
        }
        """
Declaring a macro’s names swift · at 35:44 ↗
@attached(conformance)
@attached(member, names: named(dictionary), named(init(dictionary:)))
@attached(memberAttribute)
@attached(accessor)
macro DictionaryStorage(key: String? = nil)



@attached(peer, names: overloaded)
macro AddCompletionHandler(parameterName: String = "completionHandler")



@freestanding(declaration, names: arbitrary)
macro makeArrayND(n: Int)
Macros are testable swift · at 38:28 ↗
import MyLibMacros
import XCTest
import SwiftSyntaxMacrosTestSupport

final class MyLibTests: XCTestCase {
    func testMacro() {
        assertMacroExpansion(
            """
            @DictionaryStorage var name: String
            """,
            expandedSource: """
            var name: String {
                get { dictionary["name"]! as! String }
                set { dictionary["name"] = newValue }
            }
            """,
            macros: ["DictionaryStorage": DictionaryStorageMacro.self])
    }
}