Swift Macros - 宏之语法树

445 阅读6分钟

在正式深入宏的世界之前,我们必须理解一个核心概念:Syntax(语法节点) 。它不仅是 Swift 宏生成和操作代码的“原材料”,更是编译器理解代码结构的基础。

语法树(Syntax Tree) 是代码生成与转换的基础数据结构。理解语法树的结构和操作方式是掌握宏开发的关键第一步。

本篇文章旨在帮助你掌握 SwiftSyntax 提供的语法节点体系、如何从语法树中提取信息、如何构建语法树,以及这些能力在宏中的实战应用,为你后续理解宏协议与宏实现打下扎实的基础。

1. 为什么需要了解语法树

在 Swift 宏中:

  • 你处理的不是“字符串代码”,而是结构化的 语法树
  • 宏的输入是语法节点,输出也是语法节点
  • 宏的参数、上下文、返回值都来自语法结构

简言之:不了解语法树,就无法理解宏的工作方式。

2. Syntax 与 SwiftSyntax

2.1 什么是 Syntax?

Syntax 是对 Swift 源代码的结构化表示。Swift 编译器在编译时,将源代码依次转换为:

 🟡 源代码 → 🟢 词法分析 → 🔵 语法分析 → 🟣 语法树 → 🔴 宏处理 → 🟤 编译 

每一行 Swift 代码,都会被解析为一棵树状结构,树上的每个节点都是一个语法片段,称为 Syntax 节点(Syntax Node)

2.2 常见语法节点类型举例

节点类别示例类型对应代码示例
表达式节点InfixOperatorExprSyntaxa + b
声明节点VariableDeclSyntaxlet x = 1
语句节点ReturnStmtSyntaxreturn result
类型节点TypeAnnotationSyntax: Int

每种节点类型都有明确的结构定义,可通过 SwiftSyntax 操作。

2.3 为什么宏操作的是 Syntax?

在 Swift 宏中,你不是直接操作字符串文本,也不是直接修改源代码,而是:

🟡 读取 Syntax → 🟢 生成新的 Syntax → 🔵 交给编译器继续处理

直接操作结构化的语法树,能带来:

优势说明
安全性高生成的语法结构不会导致非法代码
可读性强结构清晰,易于调试和理解
自动格式化编译器可自动对齐风格,无需手动调整
易于优化编译器直接理解语法结构,可执行更智能的优化

所以,你可以把 Swift 宏想象成是在编辑一棵代码树(Syntax Tree) ,而你的任务,就是在这棵树上插入、修改、替换节点。

2.3 语法树结构示意

可以用一张简单图理解:

 源代码
  └──> 词法分析(Tokenize)
        └──> 语法分析(Parse)
              └──> 生成 Syntax Tree(语法树)

每个 Syntax 节点都有:

  • 节点类型(比如表达式、声明、类型等)
  • 子节点(例如函数调用有函数名、参数列表子节点)
  • 源代码位置信息(可以定位到具体代码行列)
  • 描述信息(可以输出源代码片段)

以代码 print(a + b) 为例,它的语法树大致如下:

对应的 Syntax 树结构大致是:

 FunctionCallExprSyntax
 ├── calledExpression: DeclReferenceExprSyntax                 // 不是 IdentifierExprSyntax
 │   └── baseName: .identifier("print")                       // 标识符节点
 ├── leftParen: .leftParen                                     // 左括号
 ├── arguments: LabeledExprListSyntax                         // 参数列表
 │   └── [0]: LabeledExprSyntax                               // 参数元素
 │       ├── expression: InfixOperatorExprSyntax               // 中缀表达式
 │       │   ├── leftOperand: DeclReferenceExprSyntax("a")
 │       │   ├── operator: BinaryOperatorExprSyntax("+")
 │       │   └── rightOperand: DeclReferenceExprSyntax("b")
 └── rightParen: .rightParen                                   // 右括号

这种树形结构确实体现了宏系统的核心优势:

特性语法树体现宏系统收益
层次化表达式嵌套(InfixOperatorExpr 作为 FunctionCall 的子节点)允许递归处理复杂表达式
类型安全每个节点类型明确(如区分 DeclReferenceBinaryOperator编译时验证生成代码合法性
可组合性独立节点通过父子关系组合(如操作符左右操作数)支持模块化代码生成
精准定位每个节点包含位置信息(leading/trailing trivia)实现精确的错误诊断

2.5 SwiftSyntax 的协议体系

SwiftSyntax 中的节点都遵循一套协议:

协议名描述
SyntaxProtocol所有节点的基类协议
DeclSyntaxProtocol声明类节点
ExprSyntaxProtocol表达式类节点
TypeSyntaxProtocol类型相关节点
StmtSyntaxProtocol语句节点

这些协议能帮助你在代码中进行统一操作与类型匹配。

3. 如何从语法树中提取信息?

3.1 .as(...) 类型转换

 if let call = expr.as(FunctionCallExprSyntax.self) {
    let functionName = call.calledExpression.description
 }

3.2 访问节点字段

 let structDecl = decl.as(StructDeclSyntax.self)
 let name = structDecl?.identifier.text
 let members = structDecl?.memberBlock.members

3.3 遍历子节点

 for child in node.children(viewMode: .all) {
    print(child.syntaxNodeType)
 }

4. 如何构建语法节点?

4.1 使用字符串构造

 let expr: ExprSyntax = "1 + 2"

这是最常用且便捷的构造方式,适合简单的宏输出场景。

4.2 使用 SwiftSyntaxBuilder 构造复杂结构

 let one = ExprSyntax("1")
 let two = ExprSyntax("2")
 let plus = TokenSyntax.binaryOperator("+")
 let expr = InfixOperatorExprSyntax(
    leftOperand: one,
    operatorOperand: plus,
    rightOperand: two
 )

适用于需要控制每个组成部分、生成复杂结构的宏实现。

5. (raw:):安全插入语法节点

Swift 宏返回 ExprSyntax 时常见写法是:

 return "(raw: value)"

这和普通的字符串插值有什么区别?

  • 错误写法:生成的是字符串
 let sum = 10
 return "(sum)" // 实际生成的是字符串字面量 "10"
  • 正确写法:使用 raw: 插入表达式
 let sum = 10
 return "(raw: sum)" // 生成真正的数字表达式 10

为什么推荐使用 (raw:)

场景不使用 raw使用 raw
插入 Int"10"(字符串)10(数字)
插入表达式"(a + b)"(字符串)a + b(语法结构)

这能确保生成的是 合法的语法节点,而非拼接的字符串,避免类型错误。

  • 保持类型正确性(比如数字就是数字,表达式就是表达式)
  • 避免字符串包裹(防止出现 "10" 这种非预期结果)
  • 直接生成合法的 Syntax 节点

6. 示例:实现一个表达式宏 #sum(...)

下面是一个简单的宏,它可以将多个整数参数相加:

宏声明

 @freestanding(expression)
 public macro sum(_ values: Int...) -> Int = #externalMacro(module: "McccMacros", type: "SumMacro")

宏实现

 public struct SumMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
         
        // 解析所有参数,确保是整数
        let values: [Int] = try node.arguments.map { element in
            guard let literalExpr = element.expression.as(IntegerLiteralExprSyntax.self),
                  let intValue = Int(literalExpr.literal.text) else {
                throw ASTError("All arguments to #sum must be integer literals.")
            }
            return intValue
        }
         
        // 计算总和
        let sum = values.reduce(0, +)
 ​
        // 直接返回表达式
        return "(raw: sum)"
    }
 }

使用示例

 let total = #sum(1, 2, 3, 4)

宏展开后:

 let total = 10

6. 小结

在 Swift 宏系统中,你要掌握的不是字符串拼接技巧,而是:

  • 如何识别语法节点类型(如函数、变量、表达式)
  • 如何提取节点信息(名称、参数、属性等)
  • 如何构建语法结构(表达式、语句、声明等)
  • 如何插入语法节点(使用 (raw:) 保证结构合法)

语法树是宏系统的“语言”,也是宏生成代码的唯一通道。