over and over, you create a reusable Macro which can generate new code by analyzing your own code Swift Compiler % Your code, with attached or standalone macros Super-powered code with Expanded macro &
an API call - you might have many of these in your app class API { func getMemes( count: Int, completion: @escaping (Result<[Meme], Error>) -" Void ) { /$ ..& } }
an API call - you might have many of these in your app class API { func getMemes( count: Int, completion: @escaping (Result<[Meme], Error>) -" Void ) { /$ ..& } } What about async/await? $
an API call - you might have many of these in your app class API { func getMemes( count: Int, completion: @escaping (Result<[Meme], Error>) -" Void ) { /$ ..& } } What about async/await? $ @AddAsync
of our existing code, and can derive new code based on it, from a single reusable implementation class API { func getMemes( count: Int, completion: @escaping (Result<[Meme], Error>) -" Void ) { /$ ..& } func getMemes(count: Int) async throws -> [Meme] { // Generated code which calls out to the closure-based // implementation and bridges to async/await } } @AddAsync
of our existing code, and can derive new code based on it, from a single reusable implementation class API { func getMemes( count: Int, completion: @escaping (Result<[Meme], Error>) -" Void ) { /$ ..& } func getMemes(count: Int) async throws -> [Meme] { // Generated code which calls out to the closure-based // implementation and bridges to async/await } } @AddAsync
of our existing code, and can derive new code based on it, from a single reusable implementation class API { func getMemes( count: Int, completion: @escaping (Result<[Meme], Error>) -" Void ) { /$ ..& } func getMemes(count: Int) async throws -> [Meme] { // Generated code which calls out to the closure-based // implementation and bridges to async/await } } @AddAsync
of our existing code, and can derive new code based on it, from a single reusable implementation class API { func getMemes( count: Int, completion: @escaping (Result<[Meme], Error>) -" Void ) { … } func addMeme( _ meme: [Meme], completion: @escaping (Result<Meme, Error>) -" Void ) { … } /$ etc etc… @AddAsync @AddAsync
pieces: A macro declaration and a compiler plugin Your code using macros I want to use the @AddAsync macro @AddAsync func getMemes(…) { ..# } Macro declaration (Definition) Yes, this macro exists and it can do these things, but I don’t have its implementation @attached(peer, names: arbitrary) public macro AddAsync() = #externalMacro( module: "ShaisMacros", type: "AddAsyncMacro" ) Macro Compiler Plugin (Actual implementation) I hold the actual macro implementation and generate new code based on existing code in a separate process public struct AddAsyncMacro: PeerMacro { /% Macro implementation }
in your main app or in a separate module, and it has several pieces @attached(peer, names: arbitrary) public macro AddAsync() = #externalMacro( module: "ShaisMacros", type: "AddAsyncMacro" )
in your main app or in a separate module, and it has several pieces @attached(peer, names: arbitrary) public macro AddAsync() = #externalMacro( module: "ShaisMacros", type: "AddAsyncMacro" ) Name specifiers { Role { Type {
in your main app or in a separate module, and it has several pieces @attached(peer, names: arbitrary) public macro AddAsync() = #externalMacro( module: "ShaisMacros", type: "AddAsyncMacro" ) Name and access level Module containing the implementation Name of the struct implementing the macro
Abstract Syntax Tree (AST) representing how Swift “sees” the syntax tree, and lets you peek in and generate code based on your existing code Apple provides a package called Swift Syntax which is a source-accurate tree representation of Swift source code, meaning the di!erent pieces of Swift codes are represented as actual Swift types on their own https://github.com/apple/swift-syntax
(Result<Memes, Error>) -> Void ) -> Void { // ... } The implementation accepts what’s called an Abstract Syntax Tree (AST) representing how Swift “sees” the syntax tree, and lets you peek in and generate code based on your existing code
Freestanding Attached Modify the declaration they’re attached to Start with @ i.e. @Observable, @AddAsync Standalone, similar to a free function Start with a # i.e. #Preview, #warning, #line
what the macro can do Freestanding Attached @freestanding(expression) Creates a piece of code that returns a value @freestanding(declaration) Creates one or more declarations @attached(peer) Adds new declarations on the same scope as the symbol @attached(accessor) Adds get/set accessors to a property @attached(memberAttribute) Adds attributes to a type / extension @attached(member) Adds new declaration inside a type / extension @attached(extension) Adds a conformance to a type / extension
a di!erent dedicated protocol Freestanding Attached member - Adds new declaration inside a type / extension expression - ExpressionMacro Creates a piece of code that returns a value declaration - DeclarationMacro Creates one or more declarations peer - PeerMacro Adds new declarations on the same scope as the symbol accessor - AccessorMacro Adds get/set accessors to a property memberAttribute - MemberAttributeMacro Adds attributes to a type / extension extension - ExtensionMacro Adds a conformance to a type / extension MemberMacro
the newly generated code, which is strictly-checked by Swift Syntax: public struct AgendaMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [ """ enum Topics: String { case what = "What are Macros?" case how = "How do they work?" case typesAndRoles = "Types & Roles" case testingDebugging = "Testing & Debugging" case liveCoding = "Your first Macro" case examples = "Useful examples" case limitations = "Limitations & Downsides" case tools = "Tools & Resources" } """ ] } }
notify the user when they’re doing something wrong, the simplest way is throwing an error enum AgendaMacroError: Error, CustomStringConvertible { case tooAwesome var description: String { switch self { case .tooAwesome: "This lecture is awesome, it doesn't need an agenda!" } } } public struct AgendaMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { // ... throw AgendaMacroError.tooAwesome
what’s called a Diagnostic, where you can customize the severity, add fix-its, and even notes and highlights enum AgendaMacroDiagnostic: String, DiagnosticMessage { var message: String { switch self { case .tooAwesome: "This lecture is awesome, it doesn't need an agenda!" } } var diagnosticID: MessageID { MessageID(domain: "com.freak4pc.app", id: rawValue) } var severity: DiagnosticSeverity { switch self { case .tooAwesome: .warning } } case tooAwesome } public struct AgendaMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { // .. context.diagnose( Diagnostic( node: node, message: AgendaMacroDiagnostic.tooAwesome, highlights: […], notes: […], fixIts: […] ) ) return []
writing tests around them is a great way to confirm that given a syntax tree, the expected piece of code is generated assertMacro part of pointfreeco/swift-macro-testing assertMacroExpansion part of apple/swift-syntax
for the variable and adds getters and setters. /// The initial value of the variable becomes the default value of the EnvironmentKey. @attached(peer, names: prefixed(EnvironmentKey_)) @attached(accessor, names: named(get), named(set)) public macro EnvironmentValue() = #externalMacro( module: “SwiftUIMacrosImpl", type: “AttachedMacroEnvironmentKey" )
accessing outside information such as Date, identifiers, etc - such use is highly discourage since it can lead to unexpected behaviors for your macro If you need a random, safe-to-use name for a generated type, you can simply use makeUniqueName context.makeUniqueName(“AmazingNewType") -> __macro_local_14AmazingNewTypefMu_ /
2016), uses Swift Syntax as well • Can do most of what Swift Macros can, but uses Stencil templates instead of Swift code • Does not incur the build time penalty since it runs outside of the Xcode Build System as a CLI • Tooling is not as tight-knit with Xcode & Swift as Apple’s solution, can’t bundle as dependency /$ Found {{ types.all.count }} Types /$ {% for type in types.all %}{{ type.name }} {% endfor %} Template.stencil /$ Found 2 Types /$ AppDelegate ViewController Template.generate.swift Tools & Resources