Slide 1

Slide 1 text

Shai Mishali @ Waze/Google 2024 Macro Polo: A new generation of Code Generation !

Slide 2

Slide 2 text

Senior iOS Tech Lead @ monday.com Open Source ❤ Hackathon fan and winner of some International Speaker About Me @freak4pc

Slide 3

Slide 3 text

About Me Author & Editor of Several Books and Publications about Swift & iOS @freak4pc

Slide 4

Slide 4 text

Today’s Agenda #

Slide 5

Slide 5 text

Agenda

Slide 6

Slide 6 text

Agenda

Slide 7

Slide 7 text

What are Macros $

Slide 8

Slide 8 text

What are Macros ? “Macros transform your source code when you compile it, letting you avoid writing repetitive code by hand.” The Swift Programming Language Book https://docs.swift.org/swift-book

Slide 9

Slide 9 text

What are Macros? In essence, instead of writing “boilerplate code” 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 &

Slide 10

Slide 10 text

What are Macros? Consider this simple closure-based function which performs an API call - you might have many of these in your app class API { func getMemes( count: Int, completion: @escaping (Result<[Meme], Error>) -" Void ) { /$ ..& } }

Slide 11

Slide 11 text

What are Macros? Consider this simple closure-based function which performs 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? $

Slide 12

Slide 12 text

What are Macros? Consider this simple closure-based function which performs 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

Slide 13

Slide 13 text

What are Macros? We gain compile-time access to the context 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

Slide 14

Slide 14 text

What are Macros? We gain compile-time access to the context 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

Slide 15

Slide 15 text

What are Macros? We gain compile-time access to the context 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

Slide 16

Slide 16 text

What are Macros? We gain compile-time access to the context 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) -" Void ) { … } /$ etc etc… @AddAsync @AddAsync

Slide 17

Slide 17 text

How does it Work? ⚙

Slide 18

Slide 18 text

How does it work? Macros are made from two primary 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 }

Slide 19

Slide 19 text

How does it work? A macro definition can either live 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" )

Slide 20

Slide 20 text

How does it work? A macro definition can either live 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 {

Slide 21

Slide 21 text

How does it work? A macro definition can either live 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

Slide 22

Slide 22 text

How does it work? 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 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

Slide 23

Slide 23 text

How does it work? func getMemes( count: Int, completion: @escaping (Result) -> 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

Slide 24

Slide 24 text

How does it work? ├─name: identifier("getMemes") ├─signature: FunctionSignatureSyntax │ ├─parameterClause: FunctionParameterClauseSyntax │ │ ├─leftParen: leftParen │ │ ├─parameters: FunctionParameterListSyntax │ │ │ ├─[0]: FunctionParameterSyntax │ │ │ │ ├─attributes: AttributeListSyntax │ │ │ │ ├─modifiers: DeclModifierListSyntax │ │ │ │ ├─firstName: identifier("count") │ │ │ │ ├─colon: colon │ │ │ │ ├─type: IdentifierTypeSyntax │ │ │ │ │ ╰─name: identifier("Int") │ │ │ │ ╰─trailingComma: comma │ │ │ ╰─[1]: FunctionParameterSyntax │ │ │ ├─attributes: AttributeListSyntax │ │ │ ├─modifiers: DeclModifierListSyntax │ │ │ ├─firstName: identifier("completion") │ │ │ ├─parameters: TupleTypeElementListSyntax │ │ │ │ ╰─[0]: TupleTypeElementSyntax │ │ │ │ ╰─type: IdentifierTypeSyntax │ │ │ │ ├─name: identifier("Result") │ │ │ │ ╰─genericArgumentClause: GenericArgumentClauseSyntax │ │ │ │ ├─leftAngle: leftAngle │ │ │ │ ├─arguments: GenericArgumentListSyntax │ │ │ │ │ ├─[0]: GenericArgumentSyntax │ │ │ │ │ │ ├─argument: IdentifierTypeSyntax │ │ │ │ │ │ │ ╰─name: identifier(“Memes”) │ │ │ │ │ │ ╰─trailingComma: comma │ │ │ │ │ ╰─[1]: GenericArgumentSyntax │ │ │ │ │ ╰─argument: IdentifierTypeSyntax │ │ │ │ │ ╰─name: identifier("Error") │ │ │ │ ╰─rightAngle: rightAngle func getMemes( count: Int, completion: @escaping (Result) -> 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

Slide 25

Slide 25 text

How does it work? 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 ├─name: identifier("getMemes") ├─signature: FunctionSignatureSyntax │ ├─parameterClause: FunctionParameterClauseSyntax │ │ ├─leftParen: leftParen │ │ ├─parameters: FunctionParameterListSyntax │ │ │ ├─[0]: FunctionParameterSyntax │ │ │ │ ├─attributes: AttributeListSyntax │ │ │ │ ├─modifiers: DeclModifierListSyntax │ │ │ │ ├─firstName: identifier("count") │ │ │ │ ├─colon: colon │ │ │ │ ├─type: IdentifierTypeSyntax │ │ │ │ │ ╰─name: identifier("Int") │ │ │ │ ╰─trailingComma: comma │ │ │ ╰─[1]: FunctionParameterSyntax │ │ │ ├─attributes: AttributeListSyntax │ │ │ ├─modifiers: DeclModifierListSyntax │ │ │ ├─firstName: identifier("completion") │ │ │ ├─parameters: TupleTypeElementListSyntax │ │ │ │ ╰─[0]: TupleTypeElementSyntax │ │ │ │ ╰─type: IdentifierTypeSyntax │ │ │ │ ├─name: identifier("Result") │ │ │ │ ╰─genericArgumentClause: GenericArgumentClauseSyntax │ │ │ │ ├─leftAngle: leftAngle │ │ │ │ ├─arguments: GenericArgumentListSyntax │ │ │ │ │ ├─[0]: GenericArgumentSyntax │ │ │ │ │ │ ├─argument: IdentifierTypeSyntax │ │ │ │ │ │ │ ╰─name: identifier(“Memes”) │ │ │ │ │ │ ╰─trailingComma: comma │ │ │ │ │ ╰─[1]: GenericArgumentSyntax │ │ │ │ │ ╰─argument: IdentifierTypeSyntax │ │ │ │ │ ╰─name: identifier("Error") │ │ │ │ ╰─rightAngle: rightAngle func getMemes(count: Int) async throws -> Memes

Slide 26

Slide 26 text

Types and Roles !

Slide 27

Slide 27 text

Types and Roles There are two types of Macros ✌ 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

Slide 28

Slide 28 text

Types and Roles Each type has di!erent roles which determine 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

Slide 29

Slide 29 text

Types and Roles Each role requires an implementation conforming to 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

Slide 30

Slide 30 text

Types and Roles Each macro implementation is responsible for returning 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" } """ ] } }

Slide 31

Slide 31 text

Testing, Debugging & Diagnostics )*

Slide 32

Slide 32 text

Testing, Debugging & Diagnostics Remember - Macros are NOT a black box. You can always expand macros and see exactly what code they’re generating, for example - #Preview:

Slide 33

Slide 33 text

Testing, Debugging & Diagnostics Remember - Macros are NOT a black box. You can always expand macros and see exactly what code they’re generating, for example - #Preview:

Slide 34

Slide 34 text

Testing, Debugging & Diagnostics Sometimes your macro will need to 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

Slide 35

Slide 35 text

Testing, Debugging & Diagnostics You can go even deeper with 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 []

Slide 36

Slide 36 text

Testing, Debugging & Diagnostics You can go even deeper with what’s called a Diagnostic, where you can customize the severity, add fix-its, and even notes and highlights

Slide 37

Slide 37 text

Testing, Debugging & Diagnostics Macros are inherently pure functions, so 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

Slide 38

Slide 38 text

Now that you know so much about macros Let’s build one together! +

Slide 39

Slide 39 text

Now that you know so much about macros Let’s build one together! +

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

Useful Real-Life Examples ,

Slide 42

Slide 42 text

Useful Real-Life Examples @EnvironmentValue https://github.com/Wouter01/SwiftUI-Macros

Slide 43

Slide 43 text

Useful Real-Life Examples @EnvironmentValue https://github.com/Wouter01/SwiftUI-Macros

Slide 44

Slide 44 text

Useful Real-Life Examples @EnvironmentValue https://github.com/Wouter01/SwiftUI-Macros /// Creates an unique EnvironmentKey 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" )

Slide 45

Slide 45 text

Useful Real-Life Examples @Entry New W W DC ‘24

Slide 46

Slide 46 text

Useful Real-Life Examples @Codable https://github.com/SwiftyLab/MetaCodable

Slide 47

Slide 47 text

Useful Real-Life Examples @Codable https://github.com/SwiftyLab/MetaCodable

Slide 48

Slide 48 text

Useful Real-Life Examples @Codable https://github.com/SwiftyLab/MetaCodable @attached( extension, conformances: Decodable, Encodable, names: named(CodingKeys), named(DecodingKeys), named(init(from:)), named(encode(to:)) ) @attached( member, conformances: Decodable, Encodable, names: named(CodingKeys), named(init(from:)), named(encode(to:)) ) @available(swift 5.9) public macro Codable() = #externalMacro(module: "MacroPlugin", type: "Codable")

Slide 49

Slide 49 text

Useful Real-Life Examples swift-testing https://github.com/apple/swift-testing

Slide 50

Slide 50 text

Limitations & Downsides ⛔

Slide 51

Slide 51 text

Limitations & Downsides Macros are purely additive, they CANNOT remove or modify your code. They are also sandboxed to prevent disk & network access, which overall makes them safe citizens in the Swift Ecosystem. .

Slide 52

Slide 52 text

Limitations & Downsides The sandbox does not prevent you from 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_ /

Slide 53

Slide 53 text

Limitations & Downsides Macros have a lot of heavy dependencies, such as Swift Syntax - when you import a macro, all these dependencies can heavily impact your build time

Slide 54

Slide 54 text

Limitations & Downsides

Slide 55

Slide 55 text

Tools & Resources 0

Slide 56

Slide 56 text

Swift AST Explorer https://swift-ast-explorer.com/ Tools & Resources

Slide 57

Slide 57 text

Sourcery https://github.com/krzysztofzablocki/Sourcery • The OG of AST-Based Code Generation (Released 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

Slide 58

Slide 58 text

Keep learning Expand on Swift Macros - WWDC 23 #10167 https://developer.apple.com/videos/play/wwdc2023/10167 Write Swift Macros - WWDC 23 #10166 https://developer.apple.com/videos/play/wwdc2023/10166 Tools and Resources

Slide 59

Slide 59 text

Keep learning https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/ Tools and Resources

Slide 60

Slide 60 text

Shai Mishali freak4pc Thanks! Please share your feedback about this talk 1: