# Swift Macro
---
## Agenda
* 事前準備
* Install
* SPM Setup
* 實作 macro
* Error Handle
* Test
---
## The First Time We Met Macro
----
## C [Macro](https://gcc.gnu.org/onlinedocs/cpp/Macros.html)
A macro is a fragment of code which has been given a name. Whenever the name is used, it is replaced by the contents of the macro.
----
### Kind
* Object-like Macros
* Function-like Macros
----
### Object-like Macros
```clike=
#define BUFFER_SIZE 1024
foo = (char *) malloc (BUFFER_SIZE);
foo = (char *) malloc (1024);
```
----
### Function-like Macros
```clike=
#define lang_init() c_init()
lang_init()
c_init()
```
----
### Stringizing
```clike=
#define xstr(s) str(s)
#define str(s) #s
#define foo 4
str (foo)
→ "foo"
xstr (foo)
→ xstr (4)
→ str (4)
→ "4"
```
----
### Concatenation
```clike=
#define COMMAND(NAME) { #NAME, NAME ## _command }
COMMAND (quit),
COMMAND (help),
{ "quit", quit_command },
{ "help", help_command },
```
---
## 用途?
----
### Code Gen?
----
## [Type-safe metaprogramming in Swift? Let's talk about sourcery](https://speakerdeck.com/cocoaheadsukraine/type-safe-metaprogramming-in-swift-lets-talk-about-sourcery-by-krzysztof-zablocki)
----
### What is metaprogramming?
* Metaprogramming means that a program can be designed to read, generate, analyse or transform other programs, and even modify itself while running.
* But in reality the most common use case is to generate boilerplate code automatically
----
### Why would we want metaprogramming:
* If we analyze our day to day job, we repeat same code patterns:
* Equality
* Hashing
* Data Persistance
* JSON parsing
* Writing and updating testing Mocks / Stubs
----
### Why would we want metaprogramming:
* Metaprogramming allows us to:
* Adhere to DRY principle
* Write once,
* test once
* Ease maintenance costs
* Avoid human mistakes
* Experiment with interesting architecture choices
----
### Avoiding human mistakes
* don't have a problem writing boilerplate code like Equatable implementation, its simple after all
* The problem isn't writing it.
* The problem is that when you change it e.g. add new variables
* You might not realize you forgot to update your boilerplate
* This can cause hard to track bugs
---
## 事前準備
----
### [AST Explorer](https://swift-ast-explorer.com/)
----
### 略懂 Swift Syntax
----
### 常用 Syntax
* Decl
* DeclSyntax
* DeclSyntaxProtocol
* DeclGroupSyntax
* Expr
* ExprSyntax
----
### Decl(Declaration)
> 宣告
----
### ClassDecl(class 宣告)
```swift=
class Temp {
let a = 1
}
```
----
### VariableDecl(變數宣告)
```swift=
let a = 1
```
----
### DeclSyntax & DeclSyntaxProtocol
```swift=
public struct DeclSyntax:
DeclSyntaxProtocol,
SyntaxHashable {}
```
```swift=
public struct ClassDeclSyntax:
DeclSyntaxProtocol,
SyntaxHashable {}
```
----
### 哪些屬於 DeclSyntax(1/2)
* ClassDecl
* VariableDecl
* ...
----
### 哪些屬於 DeclSyntax(2/2)
```swift=
public struct DeclSyntax: DeclSyntaxProtocol, SyntaxHashable {
public init?<S: SyntaxProtocol>(_ node: S) {
switch node.raw.kind {
case .accessorDecl, .actorDecl, .associatedtypeDecl,
.classDecl, .deinitializerDecl, .editorPlaceholderDecl,
.enumCaseDecl, .enumDecl, .extensionDecl,
.functionDecl, .ifConfigDecl, .importDecl, .initializerDecl, .macroDecl, .macroExpansionDecl, .missingDecl, .operatorDecl, .poundSourceLocation, .precedenceGroupDecl, .protocolDecl, .structDecl, .subscriptDecl, .typealiasDecl, .variableDecl:
self._syntaxNode = node._syntaxNode
default:
return nil
}
}
}
```
----
### DeclGroupSyntax
```swift=
extension ActorDeclSyntax: DeclGroupSyntax {}
extension ClassDeclSyntax: DeclGroupSyntax {}
extension EnumDeclSyntax: DeclGroupSyntax {}
extension ExtensionDeclSyntax: DeclGroupSyntax {}
extension ProtocolDeclSyntax: DeclGroupSyntax {}
extension StructDeclSyntax: DeclGroupSyntax {}
```
----
### Expr(Expression)
> 表達式 表述式
----
### IntegerLiteralExpr
```swift=
1 // let a = 1
```
----
### SequenceExpr
```swift=
1 + 2
```
----
### FunctionCallExpr
```swift=
print("Hello World")
```
----
## WHY?
----
```swift=
public func expansionDecl() throws -> DeclSyntax {}
public func expansionExpr() throws -> ExprSyntax {}
```
----
```swift=
import SwiftSyntaxBuilder
public func expansionDecl() throws -> DeclSyntax {
// x
// return "1"
return "class Temp {}"
}
public func expansionExpr() throws -> ExprSyntax {
// x
// return "class Temp {}"
return "1"
}
```
---
## Install
----
### swift 5.9
* toolchain [swift5.9_2023_06_07](https://download.swift.org/development/xcode/swift-DEVELOPMENT-SNAPSHOT-2023-06-07-a/swift-DEVELOPMENT-SNAPSHOT-2023-06-07-a-osx.pkg)
* XCode 15 Beta
----
## Setup
```sh
export TOOLCHAINS=$(plutil -extract CFBundleIdentifier raw /Library/Developer/Toolchains/swift-5.9-DEVELOPMENT-SNAPSHOT-2023-06-05-a.xctoolchain/Info.plist)
```
----
## Check
```sh
swift --version
Apple Swift version 5.9-dev (LLVM 464b04eb9b157e3, Swift 7203d52cb1e074d)
Target: x86_64-apple-macosx12.0
```
---
### SPM Setup
> Package@swift-5.9.swift
```swift=
// swift-tools-version: 5.9
import PackageDescription
import CompilerPluginSupport
let package = Package(
name: "MacroPrac",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
.macCatalyst(.v13),
],
dependencies: [
.package (url: "https://github.com/apple/swift-syntax.git", from:
"509.0.0-Swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
],
targets: [
.executableTarget(
name: "MacroMain",
dependencies: [
"WWDCMacros",
]),
.macro(
name: "WWDCMacrosPlugin",
dependencies:[
.product(name: "SwiftSyntaxMacros",package:"swift-syntax"),
.product(name: "SwiftCompilerPlugin",package: "swift-syntax"),
]),
.target(
name: "WWDCMacros",
dependencies: [
"WWDCMacrosPlugin",
]),
.testTarget(
name: "Macro123Tests",
dependencies: [
"WWDCMacrosPlugin",
.product(name: "SwiftSyntaxMacros",package:"swift-syntax"),
.product(name: "SwiftSyntaxMacrosTestSupport",package:"swift-syntax"),
]),
])
```
----
### Swift Tool Version
```swift=
// swift-tools-version: 5.9
```
----
### import CompilerPluginSupport
```swift=
import CompilerPluginSupport
```
----
### OS 最低版本需求
```swift=
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
.macCatalyst(.v13),
],
```
----
### Import SwiftSyntax
```swift=
dependencies: [
.package (url: "https://github.com/apple/swift-syntax.git", from:
"509.0.0-Swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
],
```
----
### 實作 macro 的地方
```swift=
.macro(name: "WWDCMacrosPlugin")
```
----
### 定義 Macro Interface 的地方
```swift=
.target(name: "WWDCMacros")
```
---
## 實作 Macro
----
### Macro Type
* freestanding
* `#someFreestanding()`
* attached
* `@SomeAttached var a: Int`
----
### Macro Role(freestanding)
* expression
* declaration
* codeItem
* new
----
### Macro Role(attached)
* peer
* accessor
* memberAttribute
* member
* conformance
* extension
* system
* new
----
### Macro Role
|macro|protocol|
|:-|:-|
|@freestanding(expression)|ExpressionMacro|
|@freestanding(declaration)|DeclarationMacro|
|@attached(peer)|PeerMacro|
|@attached(accessor)|AccessorMacro|
|@attached(memberAttribute)|MemberAttributeMacro|
|@attached(member)|MemberMacro|
|@attached(conformance)|ConformanceMacro|
----
### 1. 實作 Macro
```swift=
// .macro(name: "WWDCMacrosPlugin")
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
public struct OneMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext)
-> ExprSyntax {
return "1"
}
}
```
----
### 2. 註冊 Macro
```swift=
// .macro(name: "WWDCMacrosPlugin")
#if canImport(SwiftCompilerPlugin)
import SwiftCompilerPlugin
import SwiftSyntaxMacros
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
OneMacro.self,
]
}
#endif
```
----
### 3. 定義 marco interface
```swift=
// .target(name: "WWDCMacros")
@freestanding(expression)
public macro one() = #externalMacro(
module: "WWDCMacrosPlugin",
type: "OneMacro"
)
```
----
### 4. 使用(展開)
```swift=
import WWDCMacros
let a = #one()
```
```swift=
import WWDCMacros
let a = 1
```
---
## Error Handle
* throw `Swift.Error`
* provide `DiagnosticMessage`
----
### throw `Swift.Error`
```swift=
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
throw MyError.caseA
}
```
----
### provide `DiagnosticMessage`(1/2)
```swift=
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
let error = Diagnostic(
node: attribute,
message: MyLibDiagnostic.someError
)
context.diagnose(structError)
return []
}
```
----
### provide `DiagnosticMessage`(2/2)
```swift=
enum MyLibDiagnostic: String, DiagnosticMessage {
case someError
var severity: DiagnosticSeverity { .error }
var message: String {
switch self {
case .someError:
return "someError"
}
}
var diagnosticID: MessageID {
MessageID(domain: "MyLibMacros", id: rawValue)
}
}
```
---
## Test
```swift=
import SwiftSyntaxMacrosTestSupport
func testOne() {
assertMacroExpansion(
"#one()",
expandedSource: """
1
""",
macros: ["one": OneMacro.self]
)
}
```
---
## 參數解析
----
### Format
* Macro
* (attached)目標
* context
* Code Gen
----
### DeclarationMacro

* node: Macro
* `-> [DeclSyntax]`: Code Gen
----
### MemberAttributeMacro

* node: Macro
* delaration: 目標
* member: 子目標
* `-> [AttributeSyntax]`: Code Gen
----

----
### node
```swift=
@memberAttribute
```
----
### declaration
```swift=
//@memberAttribute
struct A {
var a: Int
var b: Int
@abc
var c: Int
}
```
----
### member
```swift=
var a: Int
```
```swift=
var b: Int
```
```swift=
@abc
var c: Int
```
----
### Code Gen
```swift=
return ["@abc"]
```
---
## Freestanding Macro
```swift=
public struct SomeMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
// ...
)
}
```
----
### FreestandingMacroExpansionSyntax
> #free<T1, T2>(v1, v2)
* poundToken: `#`
* macro: `free`
* genericArguments: `<T1, T2>`
* leftParen: `(`
* argumentList: `v1, v2`
* rightParen: `)`
----
### FreestandingMacroExpansionSyntax
> #free {...}
* trailingClosure: `{...}`
----
### FreestandingMacroExpansionSyntax
> #free {...} v2: {....}
* additionalTrailingClosures
---
## Attached Macro
```swift=
public struct SomwMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
// ...
)
}
```
----
### AttributeSyntax
> @MyMacro(123, 456)
* atSign: `@`
* attributeName: `MyMacro`
* leftParen: `(`
* argument: `123, 456`
* rightParen: `)`
----
```swift=
@attached(xxx)
macro test<T>(_ v1: Int = 0, _ v2: T)
@test("a")
syntax.attributeName = "test"
// T ???
syntax.argument[0] = "a"
```
----
```swift=
// macro test<T>(_ v1: Int = 0, _ v2: T)
// @test("a")
let v1, v2: Syntax
if syntax.count == 1 {
v1 = 0
v2 = syntax.argument[0]
} else syntax.count == 2 {
v1 = syntax.argument[0]
v2 = syntax.argument[1]
}
```
---
## Macro 簡易展開說明
----
### ExpressionMacro
```swift=
public struct MyExpressionMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext)
-> ExprSyntax {
```
```swift=
let a = #one
let a = 1
```
----
### DeclarationMacro
```swift=
public struct MyDeclarationMacro: DeclarationMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
```
```swift=
#tuple(2)
struct Tuple2<V0, V1> {
let v0: V0
let v1: V1
}
```
----
### CodeItemMacro
```swift=
public struct MyCodeItemMacro: CodeItemMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext)
-> [CodeBlockItemSyntax] {
```
----
### CodeItemMacro(Package.swift)
```swift=
let settings: [SwiftSetting] = [
.enableExperimentalFeature("CodeItemMacros"),
]
.target(
name: "WWDCMacros",
dependencies: [
"WWDCMacrosPlugin",
],
swiftSettings: settings
),
```
----
### CodeBlockItemSyntax(1/2)
```swift=
import Foundation
let a = 1
print(a)
```
```swift=
CodeBlockItem(Decl(import Foundation))
CodeBlockItem(Decl(let a = 1))
CodeBlockItem(Expr(print(a)))
```
----
### CodeBlockItemSyntax(2/2)
```swift=
func test() {
line1
line2
line3
}
```
```swift=
func test() {
CodeBlockItem(line1)
CodeBlockItem(line2)
CodeBlockItem(line3)
}
```
----
### Peer
```swift=
struct MyPeerMacro: PeerMacro {
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
```
```swift=
@Peer
func abc() {}
func abc() {}
/* func _abc() {} */
```
----
### Accessor
```swift=
struct MyAccessorMacro: AccessorMacro {
static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax]
```
```swift=
@Accessor
var a: Int
var a: Int /* {...} */
```
----
### MemberAttribute
```swift=
struct MyMemberAttributeMacro: MemberAttributeMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
```
```swift=
@MemberAttribute
struct A {
var a: Int
}
struct A {
/* @xxx */
var a: Int
}
```
----
### Member
```swift=
struct MyMemberMacro: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
```
```swift=
@Member
struct A {
}
struct A {
/* func dump() {} */
}
```
----
### Conformance(1/2)
```swift=
struct MyConformanceMacro: ConformanceMacro {
static func expansion(
of node: AttributeSyntax,
providingConformancesOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
```
----
### Conformance(2/2)
```swift=
@Conformance
struct A {}
struct A: Codable {}
```
```swift=
@Conformance
struct A<T> {}
struct A : Codable where T: Codable {}
```
----
### Conformance(extension)
```swift=
@Conformance
struct A {}
// struct A: Codable {}
extension A: Codable {}
```
```swift=
@Conformance
struct A<T> {}
// struct A : Codable where T: Codable {}
extension A : Codable where T: Codable {}
```
---
## Role composition
Macro 可以有多個 role
----
## Role composition
在使用 macro 的地方,Swift 將展開所有適用的 role。
至少必須有一個適用的 role。
----
```swift=
@attached(member)
@attached(memberAttribute)
@attached(accessor)
public macro composition() = #externalMacro(
module: "WWDCMacrosPlugin",
type: "CompositionMacro"
)
```
---
## Making names Visible
> 必須在 macro interface 定義會產生的名稱
----
### Name Specifiers
|||
|:-|:-|
|overloaded|attached only|
|prefixed(<some prefix>)|attached only, can start with `$`|
|suffixed(<some suffix>)|attached only|
|named(<some name>)||
|arbitrary||
----
### overloaded
```swift=
@attached(peer, named: overloaded)
public macro ToAsync()
@ToAsync
func call(callback: (T) -> ()) {}
func call(callback: (T) -> ()) {}
func call() async -> T {}
```
----
### named
```swift=
@attached(member, named: named(init))
public macro Init()
@Init
struct A {
}
struct A {
init() {}
}
```
----
### arbitrary
```swift=
@attached(peer, named: arbitrary)
public macro ToPublic(_ name: String)
@ToPublic("A")
private var a = 1
private var a = 1
public var A = a
```
---
## Macros
----
### [深度解读 Observation —— SwiftUI 性能提升的新途径](https://www.fatbobman.com/posts/mastering-Observation/)
----
### OptionSet
* OptionSetMacro
* ConformanceMacro
* MemberMacro
----
### [PowerAssert](https://github.com/kishikawakatsumi/swift-power-assert)

----
### [swift-request](https://github.com/ailtonvivaz/swift-request)
```swift=
@Service(resource: "quotes")
protocol QuoteService {
@GET("random")
func getRandomQuotes(@QueryParam limit: Int?)
async throws -> [Quote]
@GET("{id}")
func getQuote(@Path by id: String)
async throws -> Quote
}
```
----
### Retrofit(kotlin)
```kotlin=
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
```
----
### [#Preview](https://developer.apple.com/documentation/swiftui/previews-in-xcode)
----
### [MacroExample](https://github.com/DougGregor/swift-macro-examples)
---
### 禁止使用外部資訊
* 只能使用編譯器提供的資訊
* 否則,工具將無法得知何時需要重新展開巨集
* 巨集無法存取檔案系統或網路
* 即使沙箱無法阻止你,但你仍不應該:
* 插入像現在的時間、進程 ID 或隨機數之類的 API 結果
* 在展開之間在全域變數中儲存資訊
---
## Ideas
---
### Point(2/3/4)D
----
### DeclarationMacro
```swift=
#Point(2)
class Point2D {
let v0: Double
let v1: Double
}
```
---
## RawJSON [String: Any]
```swift=
@attached(accessor)
@attached(memberAttribute)
@attached(member)
macro RawJSON(name: String? = nil)
```
----
### member
```swift=
@RawJSON
struct A {
}
struct A {
let dict: [String: Any]
init(_ dict: [String: Any]) {
self.dict = dict
}
}
```
----
### memberAttribute
```swift=
@RawJSON
struct A {
var a: Int
@RawJSON("B")
var b: String
}
struct A {
@RawJSON
var a: Int
@RawJSON("B")
var b: String
}
```
----
### accessor
```swift=
@RawJSON("B")
var b: String
var b: String {
get {
dict["B"] as! String
}
}
```
---
## PublicInit
----
### member
```swift=
@PublicInit
public struct A {
let a: Int
}
public struct A {
let a: Int
public init(a: Int) {
self.a = a
}
}
```
----
### peer(@Default)
```swift=
macro<T> Default(_ value: T)
// return []
@PublicInit
public struct A {
@Default(1)
let a: Int
}
public struct A {
let a: Int
public init(a: Int = 1) {
self.a = a
}
}
```
---
## [Swift-Macros](https://github.com/krzysztofzablocki/Swift-Macros)
---
## End
---
## Other
```swift=
context.makeUniqueName()
return \(literal: xxx)"
let location = contenxt.location(of: syntax)!
location.file
location.line
```
{"breaks":true,"title":"Swift Macro","lang":"zh-TW","dir":"ltr","description":"Swift Macro 入門","contributors":"[{\"id\":\"6883ab5f-8423-424e-bfcb-d0002f96698f\",\"add\":34041,\"del\":14254}]"}