Skip to content

Macros

Macros allow you to generate code automatically during compilation.

They transform your source code at compile time, helping you avoid repetitive manual coding. During the build process, the language expands any macros in your code before proceeding with the regular compilation.

Macro expansion is always additive: It inserts new code without removing or altering existing parts.

Both the macro's input and its expanded output are validated for correct syntax. Arguments passed to macros and generated code are type-checked. If a macro encounters an issue during expansion, it's treated as a build error. These checks make macro-based code easier to understand and debug, highlighting misuse or implementation flaws early.

The language supports two macro types:

  • Standalone macros exist independently, not linked to a specific declaration.

  • Attached macros enhance the declaration they're applied to.

Both types follow similar expansion logic and implementation patterns. The sections below explore them further.

Standalone Macros

Invoke a standalone macro by prefixing its name with a hash symbol (#), followed by arguments in parentheses if needed. Example:

swift
func processData() {
    print("Executing in \(#currentFunction)")
    #alert("Potential issue detected")
}

Here, #currentFunction uses a built-in macro to insert the current function's name. When run, it might output "Executing in processData". The #alert macro generates a custom build-time notification.

Standalone macros can yield a value (like #currentFunction) or trigger compile-time actions (like #alert).

Attached Macros

Call an attached macro by prefixing its name with an at symbol (@), followed by arguments in parentheses if applicable.

These macros augment the attached declaration, such as adding methods or protocol adherences.

Consider this non-macro code for a bitmask of pizza toppings:

swift
struct PizzaToppings: BitMask {
    let rawValue: Int
    static let cheese = PizzaToppings(rawValue: 1 << 0)
    static let pepperoni = PizzaToppings(rawValue: 1 << 1)
    static let olives = PizzaToppings(rawValue: 1 << 2)
}

Each option requires manual initializer calls, which can lead to errors when adding new items.

Using a macro instead:

swift
@BitMask<Int>
struct PizzaToppings {
    private enum Choices: Int {
        case cheese
        case pepperoni
        case olives
    }
}

The @BitMask macro scans the private enum cases, creates corresponding constants, and adds bitmask protocol conformance.

For illustration, the expanded form (not written by you) might resemble:

swift
struct PizzaToppings {
    private enum Choices: Int {
        case cheese
        case pepperoni
        case olives
    }

    typealias RawValue = Int
    var rawValue: RawValue
    init() { self.rawValue = 0 }
    init(rawValue: RawValue) { self.rawValue = rawValue }
    static let cheese: Self = Self(rawValue: 1 << Choices.cheese.rawValue)
    static let pepperoni: Self = Self(rawValue: 1 << Choices.pepperoni.rawValue)
    static let olives: Self = Self(rawValue: 1 << Choices.olives.rawValue)
}
extension PizzaToppings: BitMask { }

The macro-generated code after the enum makes the structure cleaner and more maintainable than manual versions.

Declaring Macros

Macros separate declaration from implementation. The declaration specifies name, parameters, usage contexts, and generated code types. The implementation handles the actual code generation.

Declare with the macro keyword. Example partial declaration for @BitMask:

swift
public macro BitMask<RawType>() =
    #externalMacro(module: "CustomMacros", type: "BitMaskMacro")

This names the macro BitMask (no arguments) and points to its implementation in the CustomMacros module's BitMaskMacro type.

Attached macros use upper camel case names (like types); standalone ones use lower camel case (like functions).

Macros are always public since they're typically used across modules.

Declarations define roles—where the macro applies and what it produces—via attributes. Extended @BitMask declaration:

swift
@attached(member)
@attached(extension, conformances: BitMask)
public macro BitMask<RawType>() =
    #externalMacro(module: "CustomMacros", type: "BitMaskMacro")

@attached(member) adds new members (e.g., initializers). @attached(extension, conformances: BitMask) adds protocol conformance via an extension.

For standalone:

swift
@freestanding(expression)
public macro currentLine<T: IntegerLiteralExpressible>() -> T =
    /* implementation location */

This has an expression role, producing a value or action.

Declarations can list generated symbol names for predictability:

swift
@attached(member, names: named(RawValue), named(rawValue),
    named(`init`), arbitrary)
@attached(extension, conformances: BitMask)
public macro BitMask<RawType>() =
    #externalMacro(module: "CustomMacros", type: "BitMaskMacro")

Fixed names like RawValue are explicit; arbitrary allows dynamic names based on usage (e.g., cheese from enum cases).

For full role details, refer to attribute documentation.

Macro Expansion Process

During builds with macros, the compiler expands them:

  1. Compiler parses code into an abstract syntax tree (AST).

  2. Sends relevant AST portion to macro implementation for expansion.

  3. Replaces macro call with generated AST.

  4. Proceeds with compilation using the updated code.

Example:

swift
let codeValue = #compactCode("WXYZ")

#compactCode converts a 4-char string to a 32-bit integer. AST input to macro: macro name and string arg. Output: integer literal. Final code equivalent: let codeValue = 1464816201 as UInt32.

Macros expand independently, outer-first if nested. Recursion is restricted to prevent cycles.

Creating a Macro

Implement in two parts: Expansion type and declaring library. Build separately from client code.

For new packages: swift package init --type macro.

For existing: Update Package.swift for tools version 5.9+, import CompilerPluginSupport, add macOS 10.15+ platform.

Add targets: Macro implementation (depends on SwiftSyntax) and library exposing macros.

Depend on SwiftSyntax: .package(url: "https://github.com/swiftlang/swift-syntax", from: "509.0.0").

Conform to role-specific protocols (e.g., ExpressionMacro for standalone expressions).

Example #compactCode implementation:

swift
import SwiftSyntax
import SwiftSyntaxMacros

public struct CompactCode: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        guard let arg = node.argumentList.first?.expression,
              let segs = arg.as(StringLiteralExprSyntax.self)?.segments,
              segs.count == 1,
              case .stringSegment(let litSeg)? = segs.first
        else {
            throw ErrorMsg.message("Requires static string")
        }

        let str = litSeg.content.text
        guard let val = compactCode(for: str) else {
            throw ErrorMsg.message("Invalid compact code")
        }

        return "\(raw: val) as UInt32"
    }
}

private func compactCode(for chars: String) -> UInt32? {
    guard chars.count == 4 else { return nil }
    var res: UInt32 = 0
    for char in chars {
        res = res << 8
        guard let asc = char.asciiValue else { return nil }
        res += UInt32(asc)
    }
    return res
}
enum ErrorMsg: Error { case message(String) }

Entry point:

swift
import SwiftCompilerPlugin

@main
struct CustomMacros: CompilerPlugin {
    var providingMacros: [Macro.Type] = [CompactCode.self]
}

Expansion method processes AST, validates, generates new syntax.

Testing and Debugging Macros

Macros suit test-driven development: Pure AST transformations, easy string-based inputs/outputs.

Example test:

swift
let src: SourceFileSyntax =
    """
    let wxyz = #compactCode("WXYZ")
    """

let ctx = BasicMacroExpansionContext(/* setup */)

let transformed = src.expand(macros: ["compactCode": CompactCode.self], in: ctx)

let expected =
    """
    let wxyz = 1464816201 as UInt32
    """

precondition(transformed.description == expected)

Use diagnostics for meaningful errors. View expansions via compiler flags during debugging.

Released under the MIT License.