Macros
|
This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A Swift macro is a piece of code that runs at compile time and expands into new Swift code — unlike C’s textual preprocessor macros, a Swift macro operates on the parsed syntax tree, is type-checked like any other code, and can be inspected (via "Expand Macro" in Xcode) as ordinary Swift source.
Freestanding and Attached Macros
#warning("remove before release") // freestanding macro: stands in for a piece of code/expression
let value = #Predicate<Person> { $0.age > 18 } // freestanding macro producing a value
@Observable
class Model { // attached macro: attaches to a declaration, adding to it
var count = 0
}
@Test func additionWorks() { // attached macro from Swift Testing
#expect(2 + 2 == 4) // freestanding macro asserting a condition
}
Freestanding macros (#name) stand in for a value or a statement at the point they’re written — #warning
emits a compiler warning, #expect (from Swift Testing) evaluates a condition and records a test failure with a
source-accurate message if it’s false. Attached macros (@Name) are written on a declaration — a type,
property, or function — and add to that declaration rather than replacing it: @Observable (see
Properties) adds change-tracking machinery to a class’s stored
properties, and @Test (from Swift Testing) marks a function as a test case, both without the developer writing
that boilerplate by hand.
Macro Declarations and Roles
@freestanding(expression)
macro stringify<T>(_ value: T) -> (T, String) =
#externalMacro(module: "MyMacros", type: "StringifyMacro")
@attached(member, names: named(_storage))
macro AddStorage() = #externalMacro(module: "MyMacros", type: "AddStorageMacro")
@attached(accessor)
macro Clamped(_ range: ClosedRange<Int>) = #externalMacro(module: "MyMacros", type: "ClampedMacro")
A macro’s own declaration is a macro keyword paired with a role attribute stating what kind of code it may
produce and where: @freestanding(expression) (produces a value, like #stringify) or
@freestanding(declaration) (produces one or more declarations); @attached(member) (adds new members to a
type), @attached(accessor) (adds a getter/setter to a property), @attached(peer) (adds a sibling
declaration alongside the one it’s attached to), @attached(extension) (adds a protocol conformance via an
extension), and @attached(memberAttribute) (adds attributes to a type’s existing members). A macro’s names:
argument declares up front which new identifiers it may introduce, so callers and tooling can see a
declaration’s full shape without expanding the macro. The body after = is always #externalMacro(module:type:) — the macro’s actual implementation lives in a separate compiler-plugin module, never inline.
Macro Expansion and Hygiene
#stringify(1 + 2) // expands to: (1 + 2, "1 + 2") -- the macro sees the unevaluated expression AND its source text
The compiler expands a macro by handing its implementation the argument as a syntax tree (not a runtime value) and receiving back another syntax tree, which the compiler then splices into the program and type-checks as if it had been written by hand. Macro hygiene guarantees this splicing can’t accidentally capture or collide with identifiers at the call site: any new local name a macro’s expansion introduces is invisible to, and cannot clash with, names already in scope where the macro is used, and vice versa — a guarantee textual preprocessor macros never provide.
Implementing a Macro
import SwiftSyntax
import SwiftSyntaxMacros
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
guard let argument = node.arguments.first?.expression else {
throw MacroError.missingArgument
}
return "(\(argument), \(literal: argument.description))"
}
}
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [StringifyMacro.self]
}
A macro’s implementation lives in a separate Swift package target that depends on SwiftSyntax/
SwiftSyntaxMacros (from the open-source swift-syntax project) and conforms to a role-specific protocol
(ExpressionMacro, MemberMacro, AccessorMacro, …) whose expansion method receives the call site’s syntax
tree and returns the expanded syntax; a CompilerPlugin conformance registers which macro types the plugin
module exposes to the compiler. Package layout, expanding/debugging a macro (Xcode’s "Expand Macro" context menu,
or swift package dump-macro-expansion) and testing a macro’s expansion against expected output are covered in
Swift Package Manager (macro targets) and
Testing (asserting on expanded output with Swift Testing).
A macro-bearing package therefore has (at least) three targets: the client code that writes @AddStorage or
#stringify; a thin macro-declaration target the client depends on, holding only the macro declarations
themselves; and a macro-plugin target (built as a separate executable the compiler invokes out-of-process) that
does the actual syntax-tree expansion using swift-syntax.
See Also
-
Attributes and Compiler Control — attributes in general, including
@attached/@freestandingthemselves. -
Result Builders — a different compile-time transformation mechanism, rewriting a block’s statements rather than a declaration or expression.
-
Swift Package Manager — structuring a package with a macro-plugin target.
-
Testing —
@Testand#expect, the Swift Testing macros used throughout this reference’s own examples.