Swift Macros ทำงานที่ compile time ผ่าน compiler plugin โดยรับ SyntaxNode และ return โค้ด Swift ใหม่ ไม่ใช่ runtime reflection แบบเก่า
มี 2 ตระกูลใหญ่: @freestanding (เรียกด้วย #macroName) และ @attached (เรียกด้วย @MacroName) โดย attached แยกย่อยเป็น peer, member, accessor, memberAttribute, extension และ conformance
โปรเจกต์ macro ต้องแบ่งเป็น 2 target: MacroPluginProvider (executable ที่ compiler โหลด) และ library target ที่ประกาศ macro declaration ให้ผู้ใช้ import
SwiftSyntax เป็น dependency บังคับ ใช้ระบุ syntax tree อย่าง FunctionDeclSyntax หรือ VariableDeclSyntax เพื่อวิเคราะห์และสร้างโค้ด
Xcode 26 ต้องอนุญาต macro plugin ในไดอะล็อก "Trust & Enable" ก่อนใช้งานครั้งแรก และ error ทั้งหมดควรใช้ DiagnosticsError เพื่อให้ผู้ใช้เห็น error ที่ตำแหน่ง call site จริง
ทดสอบ macro ได้โดย assertMacroExpansion จาก SwiftSyntaxMacrosTestSupport ซึ่งเทียบ input source กับ expanded output แบบ string
สารบัญ
Swift Macros คืออะไร และทำไมต้องใช้
Freestanding vs Attached macros ต่างกันอย่างไร
โครงสร้าง Swift Package สำหรับ Macro
สร้าง Freestanding Macro ตัวแรก: #stringify
Attached Member Macro: @AutoInit สำหรับ initializer อัตโนมัติ
Attached Peer Macro: @AddAsync สำหรับ async wrapper
การจัดการ Error ด้วย DiagnosticsError
ทดสอบ Macros ด้วย SwiftSyntaxMacrosTestSupport
@Observable และตัวอย่าง Macro ในโลกจริง
คำถามที่พบบ่อย
Swift Macros คืออะไร และทำไมต้องใช้
เอาล่ะ มาเริ่มจากพื้นฐานกันก่อน Swift Macros คือฟีเจอร์ metaprogramming ที่ Swift 5.9 เพิ่มเข้ามาเพื่อให้เรา generate โค้ด Swift จากภายในโค้ด Swift ด้วยกันในช่วง compile time โดยผ่าน compiler plugin ที่รันแยกจาก main process ของ compiler เอง กลไกทำงานคร่าว ๆ คือคอมไพเลอร์ parse ซอร์สจนได้ syntax tree แล้วส่ง node ที่ถูก annotation ด้วย macro ไปยัง plugin ผ่าน stdio จากนั้น plugin ตอบกลับเป็น syntax tree ใหม่ที่จะถูก splice กลับเข้าไปในโปรแกรมและคอมไพล์ต่อ ผลที่ได้คือ zero-cost abstraction: โค้ดที่ generate ถูกคอมไพล์เหมือนเราเขียนเองด้วยมือ ไม่มี runtime overhead และไม่พึ่งพา Objective-C runtime อย่างที่ @propertyWrapper หรือ Codable รุ่นเก่าเคยทำ
ในสมัย Objective-C เราคุ้นกับ preprocessor macros แบบ #define ที่ทำ textual substitution แบบตาบอด Swift Macros ต่างออกไปสิ้นเชิง เพราะรับ syntactic input เป็น type ที่ define ไว้ใน SwiftSyntax เช่น FunctionDeclSyntax, VariableDeclSyntax, StructDeclSyntax (นี่ทำให้ตรวจสอบและวิเคราะห์โค้ดได้อย่างละเอียด ไม่ใช่แค่ replace ข้อความ) นอกจากนี้ macro ต้อง hygienic โดยธรรมชาติ: identifier ที่ macro สร้างจะไม่ชนกับ scope ภายนอกโดยไม่ตั้งใจ
เหตุผลที่ควรใช้ macros ในโปรเจกต์จริงมีหลายข้อ ตัวอย่างที่ผมใช้บ่อยที่สุดคือลด boilerplate ของ init, Codable, DTO mapping, async wrapper รอบฟังก์ชัน callback-based, และ dependency-injection registration ในโปรเจกต์ SwiftUI ขนาดใหญ่ที่ผมเคยดูแล การย้ายจาก manual init ไปเป็น @AutoInit ตัดโค้ดออกไปหลักพันบรรทัดโดยไม่กระทบ ABI เลย และประเด็นสำคัญคือ macro ไม่ได้ทำให้ compile time พองมากอย่างที่บางคนกลัว เพราะ plugin cache ผลลัพธ์ตาม signature ของ input node
Freestanding vs Attached macros ต่างกันอย่างไร
Swift Macros แบ่งเป็น 2 ตระกูลใหญ่ตามวิธีเรียกใช้ ตระกูลแรกคือ freestanding macros ที่ผู้ใช้เรียกด้วย prefix # เช่น #stringify(x + y) หรือ #URL("https://example.com") โดย macro ตัวเดียวสามารถทำหน้าที่ได้ทั้งเป็น expression (return ค่า) หรือเป็น declaration (สร้าง type/function ใหม่) หรือเป็น codeItem (สร้างชุดคำสั่งใน function body) กลุ่มนี้เหมาะกับงานที่ไม่ผูกกับ symbol อื่น เช่น compile-time validation ของ regex, JSON literal ที่ปลอดภัย, หรือ code generation ระดับ statement
ตระกูลที่สองคือ attached macros ที่ผู้ใช้ติดกับ declaration ผ่าน @ เช่น @AutoInit หรือ @Observable ตระกูลนี้แยกย่อยตาม role ที่ประกาศได้ดังนี้
peer: สร้าง declaration ใหม่ที่อยู่ข้างเคียงกับตัวที่ติด attach เช่นสร้าง async version ของ callback function
member: เพิ่ม properties หรือ methods เข้าไปใน type ที่ติด attach เช่น auto-generated init
accessor: เพิ่ม getter/setter/willSet/didSet ให้ property
memberAttribute: attach attribute เพิ่มเติมให้ทุก member ของ type (เช่น attach @Published ให้ทุก property)
extension: เพิ่ม extension พร้อม conformance เข้าไปให้ type
คุณสมบัติ Freestanding Attached
วิธีเรียก #macroName(...)@MacroName
เห็น context declaration ไม่เห็น เห็น declaration ที่ติด attach
Role ที่ใช้ได้ expression, declaration, codeItem peer, member, accessor, memberAttribute, extension
เหมาะกับ expression-level codegen, compile-time validation ลด boilerplate ของ type, auto-conform protocol
ตัวอย่างในภาษา #warning, #Predicate@Observable, @Model
เลือก freestanding เมื่อ macro ของคุณไม่ต้องรู้อะไรเกี่ยวกับ symbol รอบ ๆ และเลือก attached เมื่อ macro ต้องอ่าน property/method ของ type ที่ติดไป เกณฑ์ง่าย ๆ คือ ถ้าอยากใส่ macro หน้า struct แล้วให้มัน scan properties เพื่อ generate init ก็ต้องเป็น attached member ส่วนถ้าอยากให้ #json("...") ตรวจ JSON literal ที่ compile time ก็คือ freestanding
โครงสร้าง Swift Package สำหรับ Macro
โปรเจกต์ macro ทุกตัวต้องอยู่ใน Swift Package เพราะ compiler ต้อง build plugin เป็น executable แยกจาก client target โครงสร้างมาตรฐานที่ผมใช้ทุกโปรเจกต์คือแบ่งเป็น 3 targets ได้แก่ macro implementation (compiler plugin), macro interface (library), และ test target สำหรับ macro expansion ให้เริ่มด้วยการรัน swift package init --type macro บน Xcode 26 หรือใช้ template "Swift Macro" ใน Xcode ก็ได้ทั้งคู่
// Package.swift
// swift-tools-version: 6.0
import PackageDescription
import CompilerPluginSupport
let package = Package(
name: "MyMacros",
platforms: [.iOS(.v17), .macOS(.v14)],
products: [
.library(name: "MyMacros", targets: ["MyMacros"]),
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "601.0.0"),
],
targets: [
// 1) Compiler plugin (run macro logic)
.macro(
name: "MyMacrosImpl",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]
),
// 2) Library ที่ผู้ใช้ import
.target(name: "MyMacros", dependencies: ["MyMacrosImpl"]),
// 3) Test target
.testTarget(
name: "MyMacrosTests",
dependencies: [
"MyMacrosImpl",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)
จุดสำคัญคือ .macro(...) target ประเภทนี้ Swift Package Manager จะ compile เป็น executable และ compiler โหลดผ่าน -load-plugin-executable ให้อัตโนมัติ ส่วน MyMacros library target มีหน้าที่แค่ประกาศ @freestanding/@attached declaration เพื่อให้ผู้ใช้ import ได้ตามปกติ อย่าใส่ implementation ในนี้ เพราะจะทำให้ macro โผล่เข้าไปในโปรแกรมของ client โดยไม่จำเป็น
คำเตือน: version ของ swift-syntax ต้อง match กับ toolchain ของ Xcode ที่ใช้ compile plugin ถ้าคุณอัปเกรด Xcode 26 ให้ใช้ swift-syntax 601.0.0 ขึ้นไป มิฉะนั้น plugin จะโหลดไม่ขึ้นและ Xcode จะแสดง error "external macro implementation type could not be found" (ผมโดน trap นี้ตอน migrate โปรเจกต์เก่าอยู่หลายครั้ง)
สร้าง Freestanding Macro ตัวแรก: #stringify
ตัวอย่างคลาสสิกที่ Apple ใช้สอนคือ #stringify(x + y) ที่ return tuple ของค่าที่คำนวณและ string ของ expression นั้น ตัว macro นี้อธิบายทั้งกลไกการรับ syntax tree และการ return expression syntax ใหม่ครบทุกขั้นตอน
ในไฟล์ Sources/MyMacros/MyMacros.swift (library target) เราประกาศ declaration ก่อน:
// MyMacros.swift - library interface
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) =
#externalMacro(module: "MyMacrosImpl", type: "StringifyMacro")
จากนั้นเขียน implementation ใน Sources/MyMacrosImpl/StringifyMacro.swift ซึ่งเป็น compiler plugin target:
// StringifyMacro.swift - implementation
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxMacros
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.arguments.first?.expression else {
fatalError("compiler bug: missing argument")
}
return "(\(argument), \(literal: argument.description))"
}
}
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StringifyMacro.self,
]
}
สังเกต 3 จุดหลัก จุดแรกคือต้อง conform ExpressionMacro เพราะประกาศเป็น @freestanding(expression) จุดสองคือใช้ string interpolation ของ ExprSyntax ที่ SwiftSyntax เตรียมให้ ทำให้เขียนโค้ดที่ generate เหมือนเขียน template ปกติ จุดสามคือ @main struct ที่ conform CompilerPlugin จะรีจิสเตอร์ทุก macro ให้ compiler รู้จัก การเรียกใช้จากฝั่ง client ก็ตรงไปตรงมามาก:
import MyMacros
let (value, code) = #stringify(2 + 3)
print(value) // 5
print(code) // "2 + 3"
Attached Member Macro: @AutoInit สำหรับ initializer อัตโนมัติ
ถัดมาเป็นตัวอย่างที่ผมใช้ในโปรเจกต์จริงเกือบทุกวัน คือ attached member macro ที่ scan properties ของ struct แล้ว generate memberwise initializer ให้แม้ struct นั้นเป็น public ก็ตาม (โดยปกติ Swift generate memberwise init เฉพาะ internal เท่านั้น) ประกาศใน library:
@attached(member, names: named(init))
public macro AutoInit() =
#externalMacro(module: "MyMacrosImpl", type: "AutoInitMacro")
Implementation ต้อง conform MemberMacro รับ SomeDeclSyntaxProtocol ของ declaration ที่ติด attach แล้ว return array ของ DeclSyntax:
import SwiftSyntax
import SwiftSyntaxMacros
public struct AutoInitMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// 1) รวบรวม stored properties พร้อม type
let vars = declaration.memberBlock.members.compactMap {
$0.decl.as(VariableDeclSyntax.self)
}
var params: [String] = []
var assigns: [String] = []
for varDecl in vars {
for binding in varDecl.bindings {
guard
let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
let type = binding.typeAnnotation?.type.description
.trimmingCharacters(in: .whitespaces)
else { continue }
params.append("\(name): \(type)")
assigns.append("self.\(name) = \(name)")
}
}
// 2) ประกอบเป็น init declaration ด้วย SwiftSyntax string interpolation
let paramList = params.joined(separator: ", ")
let body = assigns.joined(separator: "\n ")
let initDecl: DeclSyntax = "public init(\(raw: paramList)) {\n \(raw: body)\n}"
return [initDecl]
}
}
เพิ่ม AutoInitMacro.self เข้า providingMacros ของ MyMacrosPlugin แล้วใช้ได้ทันที ในฝั่ง client เขียนแค่:
@AutoInit
public struct User {
public let id: UUID
public let name: String
public var email: String
}
// Expand เป็น:
// public init(id: UUID, name: String, email: String) {
// self.id = id
// self.name = name
// self.email = email
// }
เคล็ดลับ: คลิกขวาที่ @AutoInit ใน Xcode 26 แล้วเลือก Expand Macro จะเห็นโค้ดที่ macro generate เป็นข้อความ ทำให้ debug ได้เหมือน expand โค้ด inline โดยส่วนตัวผมใช้ feature นี้ก่อน commit ทุกครั้ง
Attached Peer Macro: @AddAsync สำหรับ async wrapper
Peer macro สร้าง declaration ที่อยู่ข้างเคียงกับตัวที่ถูก attach ตัวอย่างที่ผมใช้เพื่อ migrate โปรเจกต์เก่าคือ @AddAsync ที่รับ function callback-based รูปแบบ f(args, completion: (T) -> Void) แล้ว generate async version ให้อัตโนมัติ ตัวนี้ช่วย migrate ไปยัง Swift 6.2 Approachable Concurrency ได้เร็วขึ้นมาก (ผมเจอปัญหานี้ตอนย้าย legacy SDK ตัวหนึ่งที่มี callback function หลายร้อยตัว)
@attached(peer, names: overloaded)
public macro AddAsync() =
#externalMacro(module: "MyMacrosImpl", type: "AddAsyncMacro")
public struct AddAsyncMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let fn = declaration.as(FunctionDeclSyntax.self) else {
throw MacroError.notAFunction
}
// ดึงพารามิเตอร์ ยกเว้น completion ตัวสุดท้าย
let params = fn.signature.parameterClause.parameters
guard let completion = params.last,
let completionType = completion.type.as(FunctionTypeSyntax.self),
let resultType = completionType.parameters.first?.type
else { throw MacroError.needsCompletion }
let asyncParams = params.dropLast().map { "\($0.firstName.text): \($0.type.description)" }
let asyncArgs = params.dropLast().map { "\($0.firstName.text): \($0.firstName.text)" }
let name = fn.name.text
let paramList = asyncParams.joined(separator: ", ")
let argList = asyncArgs.joined(separator: ", ")
let asyncDecl: DeclSyntax = "public func \(raw: name)(\(raw: paramList)) async -> \(raw: resultType.description) { await withCheckedContinuation { cont in \(raw: name)(\(raw: argList), completion: { result in cont.resume(returning: result) }) } }"
return [asyncDecl]
}
}
ผลลัพธ์เมื่อผู้ใช้ติด attach:
@AddAsync
func fetchUser(id: UUID, completion: (User) -> Void) { /* legacy */ }
// Expand เป็นฟังก์ชัน peer:
// func fetchUser(id: UUID) async -> User { ... }
การจัดการ Error ด้วย DiagnosticsError
เมื่อ macro รับ input ที่ไม่ถูกต้อง วิธีที่ผิดคือ fatalError หรือ throw error ทั่วไป เพราะ Xcode จะรายงาน error ไปที่ตำแหน่ง macro invocation อย่างเลือน ๆ วิธีที่ถูกกว่าคือส่ง Diagnostic กลับผ่าน context เพื่อชี้ไปที่ node ต้นเหตุพร้อมข้อความที่อ่านง่าย
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftDiagnostics
enum AutoInitDiag: String, DiagnosticMessage {
case notAStruct
var severity: DiagnosticSeverity { .error }
var message: String {
switch self {
case .notAStruct: return "@AutoInit ใช้ได้เฉพาะกับ struct เท่านั้น"
}
}
var diagnosticID: MessageID {
MessageID(domain: "MyMacros", id: rawValue)
}
}
// ในฟังก์ชัน expansion:
guard declaration.is(StructDeclSyntax.self) else {
let diag = Diagnostic(node: Syntax(node), message: AutoInitDiag.notAStruct)
context.diagnose(diag)
return []
}
ผลลัพธ์คือ Xcode 26 จะ underline @AutoInit เป็นสีแดงพร้อมข้อความไทยที่เราตั้งไว้ ทำให้ผู้ใช้ macro รู้ทันทีว่าใช้ผิดที่ นอกจากนี้เรายัง attach FixIt เพื่อเสนอการแก้ไขได้ ซึ่ง Xcode จะแสดงเป็นปุ่ม Fix ให้ผู้ใช้กดยอมรับ (ประสบการณ์การใช้งาน macro ที่ดีเลยขึ้นอยู่กับ diagnostic ที่เขียนอย่างจริงจัง)
ทดสอบ Macros ด้วย SwiftSyntaxMacrosTestSupport
Test ของ macro ไม่ได้เขียนเป็น XCTestCase ปกติ เพราะเราต้องเทียบ input source กับ expanded output ทาง SwiftSyntax แถม SwiftSyntaxMacrosTestSupport ที่มีฟังก์ชัน assertMacroExpansion เพียงตัวเดียวก็เพียงพอ คู่กับ Swift Testing framework ก็อ่านง่ายและทันสมัย
import SwiftSyntaxMacrosTestSupport
import Testing
@testable import MyMacrosImpl
let testMacros: [String: Macro.Type] = [
"AutoInit": AutoInitMacro.self,
"stringify": StringifyMacro.self,
]
@Test func autoInitExpandsToMemberwiseInit() {
let input = #"""
@AutoInit
public struct User {
public let id: UUID
public var name: String
}
"""#
let expanded = #"""
public struct User {
public let id: UUID
public var name: String
public init(id: UUID, name: String) {
self.id = id
self.name = name
}
}
"""#
assertMacroExpansion(input, expandedSource: expanded, macros: testMacros)
}
@Test func stringifyReturnsExpressionAndSource() {
assertMacroExpansion(
"let pair = #stringify(1 + 2)",
expandedSource: "let pair = (1 + 2, \"1 + 2\")",
macros: testMacros
)
}
รันด้วย swift test หรือ ⌘U ใน Xcode ถ้า expanded output ไม่ match SwiftSyntax จะ print diff แบบ character-by-character ทำให้ debug ได้ตรงจุด ผมแนะนำให้เขียน test อย่างน้อยหนึ่ง case ต่อ macro role และหนึ่ง case ต่อ error path เพื่อรัน CI ไม่ให้ regression หลุดออก production (เคยเจอ regression ที่ macro output เปลี่ยน indentation ไปเงียบ ๆ ตอน bump swift-syntax แล้ว snapshot test นี่แหละที่จับได้)
หมายเหตุ: ตั้งแต่ Xcode 26 SwiftSyntax ปรับ default indentation เป็น 4 spaces หากคุณ generate โค้ดด้วย indentation อื่น assertMacroExpansion จะ fail ให้ใช้ \(raw: ...) เมื่อสอด raw string เพื่อควบคุมช่องว่างเอง
@Observable และตัวอย่าง Macro ในโลกจริง
Macro ที่ใช้กันมากที่สุดในปี 2026 คงหนีไม่พ้น @Observable ซึ่งเป็น attached macro ที่รวม member, memberAttribute และ extension role เข้าด้วยกัน เมื่อคุณ annotate class ด้วย @Observable compiler plugin จะ generate storage แยกต่อ property, wrap accessor ให้เรียก _$observationRegistrar.access() และ conform protocol Observable ให้อัตโนมัติ นี่คือเหตุผลว่าทำไม SwiftUI 5+ ไม่ต้องใช้ @Published อีกต่อไป ทุกอย่างถูก generate ตอน compile ด้วย macro นี้เอง
ตัวอย่างที่ผมเห็นบ่อยในโปรเจกต์เพื่อนร่วมทีมยังมีอีกหลายชนิด เช่น @Model ของ SwiftData ที่ generate storage, indexing, และ CloudKit sync attribute; #Predicate ของ Foundation ที่ compile Swift closure เป็น NSPredicate; และ third-party อย่าง swift-macro-toolkit, MetaCodable ที่ generate Codable จาก property annotation ที่ยืดหยุ่นกว่า standard
อ้างอิงและอ่านเพิ่มเติมสำหรับใครที่อยากศึกษาลึกลงไปกว่านี้ ผมแนะนำ The Swift Programming Language: Macros ที่เป็น official reference, swiftlang/swift-syntax repository ที่มีตัวอย่าง macro implementation ใช้ในตัว compiler เอง, และ SE-0389: Attached Macros proposal ที่อธิบายเหตุผลเบื้องหลังการออกแบบระบบ role ทั้งหมด
ท้ายที่สุด macros ไม่ใช่กระสุนเงินสำหรับ boilerplate ทุกชนิด กฎที่ผมยึดคือใช้ macro เมื่อ pattern ซ้ำเกิน 3 ครั้งในโปรเจกต์ และ pattern นั้นไม่สามารถแสดงออกได้ด้วย generics หรือ protocol extension เท่านั้น การใช้เกินอาจทำให้ code review ยากขึ้นและทำให้ผู้ที่ไม่คุ้น SwiftSyntax เสียเวลา onboarding นาน ให้เริ่มจาก macro เล็ก ๆ ที่แก้ปัญหาเฉพาะจุดก่อน แล้วค่อยขยายเมื่อทีมทั้งหมดคุ้น
คำถามที่พบบ่อย
Swift Macros ทำให้ compile time ช้าลงหรือไม่?
โดยรวมช้าลงเล็กน้อยเฉพาะครั้งแรกที่ build เพราะต้อง compile plugin executable ครั้งเดียว หลังจากนั้น SwiftPM cache ผลลัพธ์และ incremental build เกือบเท่าโค้ดปกติ ในโปรเจกต์ผมเองที่มี ~40 macro invocation ต่อไฟล์ compile time เพิ่มขึ้นไม่ถึง 3% เทียบกับเขียนโค้ด boilerplate มือ
ใช้ Swift Macros ใน iOS 15 หรือรุ่นเก่ากว่าได้ไหม?
ได้ครับ เพราะ macro expansion เกิดที่ compile time ล้วน ๆ ไม่ต้องพึ่งพา runtime symbol ใหม่ ตราบใดที่โค้ดที่ generate ยังใช้ API ที่ support minimum deployment target ของคุณอยู่ ก็รันบน iOS 13 ขึ้นไปได้สบาย ต้องใช้ Xcode 15+ ในการ build เท่านั้น
ทำไม Xcode ขึ้น "external macro implementation type could not be found"?
สาเหตุพบบ่อยที่สุดคือ version ของ swift-syntax ไม่ match กับ toolchain ของ Xcode หรือชื่อ module ใน #externalMacro ไม่ตรงกับชื่อ macro target ใน Package.swift ให้เช็คว่าใช้ swift-syntax 601.0.0+ กับ Xcode 26 และชื่อ module ต้องตรง case-sensitive ทั้ง 2 ที่
เขียน Macro โดยไม่ใช้ SwiftSyntax ได้ไหม?
ไม่ได้ครับ เพราะ compiler ส่ง input มาเป็น SwiftSyntax node เท่านั้น ทางเลือกเดียวคือใช้ helper library อย่าง swift-macro-toolkit ที่ห่อ SwiftSyntax ด้วย API ที่อ่านง่ายกว่า แต่ภายในก็ยังพึ่งพา SwiftSyntax เต็ม ๆ อยู่ดี
Debug macro ใน Xcode ได้อย่างไร?
คลิกขวาที่ macro invocation แล้วเลือก "Expand Macro" เพื่อดูโค้ดที่ generate หากต้องการ step debugger ให้ attach process ของ compiler plugin (executable ชื่อ MyMacrosImpl) จาก Debug ▸ Attach to Process แล้วตั้ง breakpoint ในฟังก์ชัน expansion(...) ก่อน trigger build