Swift macros are expanded during the semantic analysis phase of compilation, specifically after parsing but before type checking the final Abstract Syntax Tree (AST). This timing is crucial because it allows the macro expansion to generate code that must still undergo full type checking and semantic validation. By operating at this stage, Swift ensures that expanded code cannot violate the language's type safety guarantees or bypass access control modifiers.
The problem arises because macros transform source code by generating new syntax nodes, which could potentially introduce identifiers that conflict with existing variables in the surrounding lexical scope. If a macro simply injected hardcoded variable names, it could accidentally capture or shadow variables from the calling context. This would lead to subtle bugs or security vulnerabilities where generated code interferes with the caller's logic.
To solve this, Swift employs a hygienic macro system that uses unique internal identifiers for all synthesized bindings. The compiler attaches metadata to syntax nodes that tracks their original lexical context, ensuring that generated identifiers are treated as distinct from user-written code unless explicitly unwrapped. This mechanism allows macros to safely introduce temporary variables without risk of name collisions, while still permitting intentional name capture through explicit parameter passing when desired.
Our team was building a Swift package for dependency injection that used an attached macro called @Injectable to automatically generate initializer code for complex service classes. The macro needed to create temporary variables to hold intermediate dependencies during construction, but we faced the risk that common variable names like container or service might already exist in the target class scope. This created a dilemma: how could we generate safe initialization code without risking name collisions that would break client code or introduce subtle reassignment bugs?
We initially considered implementing a naive text-based code generation approach using simple string templates to produce the initializer implementation. The primary advantage was implementation simplicity, as we could immediately inspect the generated Swift code and debug it directly. However, the critical disadvantage was the lack of hygiene guarantees; there was no mechanism to ensure that temporary variable names wouldn't conflict with existing properties in the target class, potentially causing compilation failures or silent logic errors where the macro accidentally reassigned existing instance variables.
We then evaluated using Sourcery, a mature third-party code generation tool that operates as a pre-compile step external to the Swift compiler. The pros included extensive documentation, flexible stencil templates, and the ability to generate entire files rather than just inline code. Unfortunately, the cons involved complex build tool integration requiring additional Run Script phases in Xcode, significantly slower build times due to the external process overhead, and the lack of real-time semantic analysis which meant type errors in generated code would only surface at compile time without clear source mapping to the original macro invocation.
Ultimately, we chose Swift's native macro system introduced in Swift 5.9, utilizing a peer macro attached to the service class declaration. This solution was selected because it integrates directly into the compiler pipeline, providing compile-time type checking of expanded code and built-in hygiene for generated identifiers through the SwiftSyntax library. The result was a robust dependency injection framework where the @Injectable macro could safely generate complex initialization logic without fear of name shadowing, reducing boilerplate code by approximately 70% while maintaining full compile-time safety guarantees and clear error messages that pointed directly to the macro usage site.
The final implementation eliminated an entire category of naming-related bugs that had plagued our previous manual dependency injection setup. Build times improved by 40% compared to the Sourcery approach, and developers could refactor service classes with confidence knowing that the macro-generated initializers would automatically adapt to new dependencies without manual synchronization.
Why can't macros in Swift modify existing code in place, and what alternative patterns achieve similar semantics?
Unlike Lisp or Rust procedural macros that can transform existing syntax nodes in place, Swift macros are purely additive—they can only generate new code, never mutate the original source. This restriction exists because Swift's compilation model requires the original source to remain intact for debugging, source mapping, and incremental compilation purposes. To achieve "modification" semantics, developers must use peer macros that generate additional overloads or wrapper types, combined with deprecation annotations on the original declarations to guide migration toward the generated alternatives.
How does the macro expansion handle type inference for generated expressions, and what happens when inference fails?
When a macro expands into code containing expressions without explicit type annotations, Swift performs type inference on the generated AST during the standard type-checking phase that occurs after macro expansion. If inference fails, the compiler emits diagnostic messages that map error locations back to the macro invocation site using source location metadata attached during expansion. Candidates often miss that macros can explicitly generate #file and #line literals or use the #sourceLocation directive to control how diagnostics appear to the user, ensuring errors point to meaningful locations rather than internal macro implementation details.
What is the difference between freestanding and attached macros in terms of their expansion context and available semantic information?
Freestanding macros (prefixed with #) expand at the expression or statement level and have limited access to the surrounding type information, receiving only the syntax of their arguments. In contrast, attached macros (prefixed with @) operate on declarations and receive rich semantic information including the attached declaration's syntax, access modifiers, and inheritance relationships through the macro declaration's context parameter. Beginners frequently confuse these boundaries, attempting to use freestanding macros where attached peer or member macros are required to access type members or generate nested declarations within specific type scopes.