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 ↗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
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
(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "WWDCMacros", type: "StringifyMacro") Implementation of the stringify macro
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
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
/// 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
/// 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.
(member, names: named(init))
public macro SlopeSubset() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro") Write empty implementation for SlopeSubset
/// 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
@main
struct WWDCPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
SlopeSubsetMacro.self
]
} Test SlopeSubset
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
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
// TODO: Emit an error here
return []
} Extract enum members
let members = enumDecl.memberBlock.members Load enum cases
let caseDecls = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) } Retrieve enum elements
let elements = caseDecls.flatMap { $0.elements } Generate initializer
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
return [DeclSyntax(initializer)] Apply SlopeSubset to EasySlope
/// Slopes suitable for beginners. Subset of `Slopes`.
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
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
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
throw SlopeSubsetError.onlyApplicableToEnum Generalize SlopeSubset declaration to EnumSubset
(member, names: named(init))
public macro EnumSubset<Superset>() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro") Retrieve the generic parameter of EnumSubset
guard let supersetType = attribute
.attributeName.as(SimpleTypeIdentifierSyntax.self)?
.genericArgumentClause?
.arguments.first?
.argumentType else {
// TODO: Handle error
return []
} Related sessions
-
13 min -
43 min -
23 min -
40 min