How does a Swift macro actually run, what can it not do, and what does it cost?

5.5.1 · L5 · Interview

Predict first

What does a macro plugin receive from the compiler?

// A: the fully type-checked program
// B: the syntax tree of the macro use, plus some lexical context
// C: the whole file system
  • A
  • B
  • C

Predict first

The generated code from a macro has a type error. Where is the error reported?

// A: from the macro plugin at runtime
// B: by the compiler, when it type-checks the expansion
// C: only at app launch
  • A
  • B
  • C

Mental model. A macro is a typist who copies out the boilerplate you would have written, with a strict rule: they see only the page you hand them, not the rest of the project.

A macro is implemented as a compiler plugin: a separate executable built with SwiftSyntax. When the compiler meets a macro use it launches the plugin, sends it the syntax tree of the use site (plus some lexical context, such as the enclosing type’s name), and receives new source text back. The compiler parses and type-checks the expansion like ordinary code, so errors in generated code are ordinary compile errors. Expansion is deterministic and sandboxed: a plugin cannot read arbitrary files or the network, and it must be explicitly trusted before it runs (Xcode asks, and CI uses a flag to skip validation).

The limits follow from that. The plugin sees syntax, not types, so it cannot ask “does this property conform to Codable?”; it can only act on what is written. It cannot rewrite existing code (attached macros add members, accessors, peers or conformances; they do not edit your lines), and expansions cannot see other files. The costs are build time (SwiftSyntax must be built, though newer toolchains reduce it, and every expansion adds work), harder debugging (use Expand Macro in Xcode, or -Xfrontend -dump-macro-expansions), and generated code that is invisible to search. Use a macro when boilerplate cannot be removed with a generic, a protocol extension or a property wrapper.

The plugin gets syntax, not types, and runs in a sandbox with no file or network access. Its output is then type-checked as normal code.

The expansion is just source text, so the compiler type-checks it like any other code and reports errors against the expansion.

Common traps

  • Expecting a macro to inspect a type’s conformances. It sees syntax only. Use protocol requirements in the generated code to enforce them.
  • Reaching for a macro where a property wrapper works. You pay in build time and tooling for something the language already does.

They might follow up with

  • Why must a macro be trusted? The plugin runs as a program during your build, so the toolchain asks before running one from a package.
  • How do you test one? With a library that runs the expansion on a source string and compares the generated text to what you expect.

Related questions

  • 5.5.2 What are the macro roles, and which real macros use which?
  • 5.6.1 A Swift file takes forever to compile. How do you find the cause and fix it?
  • 1.10.3 Macros vs property wrappers vs result builders — one sentence each.

What are the macro roles, and which real macros use which? →