news 2026/9/23 9:03:04

SE-0382 深度解析:Swift 表达式宏(Expression Macros)——从 `stringify` 到宏系统基石的完整实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SE-0382 深度解析:Swift 表达式宏(Expression Macros)——从 `stringify` 到宏系统基石的完整实战指南
  • 文档

【免费下载链接】swift-evolution

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.

项目地址:https://gitcode.com/gh_mirrors/sw/swift-evolution
点击查看免费下载

表达式宏(Expression Macros)是 Swift 5.9 引入的宏系统基石之一,它允许开发者用#前缀的表达式语法对源码做任意的语法树级转换,从而以"库能力"替代过去只能通过新增语言特性才能实现的功能。本文将基于 swift-evolution 仓库中的 SE-0382 提案原文,完整讲解表达式宏的设计动机、类型检查模型、语法转换机制、宏声明与展开的详细规则、宏实现库 API、标准库内置宏,以及沙箱化与工具链支持,并结合 宏愿景文档、SE-0389 附属宏、SE-0394 SwiftPM 宏支持 等配套提案,帮助你从"会用"进阶到"理解并独立编写"表达式宏。

提案背景与动机

在宏系统出现之前,Swift 表达式层面已经提供了不错的运行时行为抽象——你可以编写一个函数,在任意位置以表达式形式调用它。但除了少数硬编码进编译器的特例(如#file#line),表达式无法感知或修改正在编译的程序源码。这类需求过去只能求助于外部源码生成工具,而这些工具往往难以与编译器、IDE、调试器等既有工具链干净地集成。

表达式宏正是 A Vision for Macros in Swift(macros 愿景文档)的落地成果之一。该愿景认为,宏系统的价值在于"将语法糖民主化":许多原本需要新语言特性或外部代码生成器的任务,都可以实现为宏。宏虽然可能带来次优的语法、较弱的诊断或更差的编译期性能,但它能让语言保持精简,同时通过可扩展的库生态维持表达力。

具体到表达式:#前缀语法在 Swift 中已有大量先例(#filePath#line#colorLiteral#warning等),用宏来统一并泛化这一语法是自然的选择。SE-0382 提出的方案引入"表达式宏"——它以#标记出现在源码中,展开后仍是表达式;它拥有类似函数的参数与结果类型签名,从而在不实际展开宏的情况下就能描述宏展开的效果。

核心方案概览:表达式宏如何工作

表达式宏的使用形式非常直观,例如stringify宏:

#stringify(x + y)

编译器将其展开为:

(x + y, "x + y")

即:既保留原始参数的值,又生成一个包含该参数源码文本的字符串字面量。这个宏的声明形如函数,是声明的一部分:

@freestanding(expression) macro stringify<T>(_: T) -> (T, String)

宏展开是源码到源码(source-to-source)的语法树转换:宏实现拿到宏展开表达式本身的语法树(从#开始到最后一个参数结束),将其重写为新的语法树;该语法树随后会对照宏的结果类型进行类型检查。

类型检查的宏参数与结果:两阶段模型

SE-0382 最核心的设计决策之一是:宏参数在宏实例化之前就已完成类型检查。以#stringify(x + y)为例:

  • 参数x + y会先被类型检查;若它非法(例如xIntyString),宏永远不会被展开
  • 若合法,泛型参数T会被推断为x + y的结果类型,并贯穿到宏的结果类型中。

提案原文明确了这一"类型检查优先"模型的三个收益:

  1. 宏实现保证拿到类型正确的参数,无需担心非法代码混入;
  2. 工具可以像对待函数一样对待宏,代码补全、语法高亮等能力直接可用,因为宏参数遵循与其他 Swift 代码相同的规则;
  3. 宏展开表达式可以在不展开宏的情况下部分类型检查,这让工具在未做宏展开时也能给出合理结果,同时提升编译性能——同一宏不会在类型推断过程中被反复展开。

宏展开后,产出的语法树会以宏结果类型作为上下文类型进行类型检查。例如#stringify(x + y)x + yInt时,展开结果(x + y, "x + y")会以(Int, String)为上下文类型接受检查。

宏表达式的类型检查与函数调用类似,类型推断信息可以在宏参数与结果类型之间双向流动。提案给出了一个很能说明问题的例子:

let (a, b): (Double, String) = #stringify(1 + 2)

这里的整型字面量12会被赋值为Double类型——类型信息从结果类型反推到了宏参数上,宏在此处表现得如同一个普通泛型函数调用。

语法转换:为什么选择 source-to-source

宏展开是纯语法操作:输入是完整的宏展开表达式语法树,输出是一棵新的语法树,随后按宏结果类型检查。相对于直接操作编译器的 AST 或内部表示(IR),纯语法转换有一系列优点:

  • 宏展开可以使用完整的 Swift 语言来表达其效果——只要在语法上该位置能写 Swift 源码,宏就能展开成它;
  • Swift 程序员理解 Swift 源码,因此可以推理宏的输出,这对编写宏和使用宏都大有帮助;
  • 使用宏的源码可以被"展开"以消除宏,例如便于推理、调试,或让代码在不支持宏的旧编译器上工作;
  • 编译器的 AST 与内部表示无需暴露给客户端,避免因向后兼容顾虑而限制编译器演进。

但纯语法转换也有明确代价,提案如实列出了三点:

  • 易产生编译期失败:本质上是把源码当字符串处理,宏实现中很容易引入语法错误或类型错误;
  • 重新解析与重新类型检查:比直接操作 AST/IR 有更多编译期开销;
  • 非卫生(hygienic):宏展开的处理方式取决于其展开环境,且可能影响该环境。

提案的结论是:语法宏易用、易理解的优势压过了这些问题。关于"非卫生"带来的名字冲突风险,将在下文makeUniqueName的设计中给出缓解方案。

宏定义作为独立程序

关于"宏的展开操作如何定义",提案对比了两大类方案:

  • 声明式变换规则:为语言扩展专门语法,编译器对每次宏展开套用规则。C 预处理器的宏、Racket 的模式宏、Rust 的声明式宏(macro_rules!)属于此类;Swift 若走这条路,需要发明一套匹配与重写语法树的模式语言。
  • 可执行程序转换源码:运行一个程序直接操作源码。Scala 3 借助 JVM 将目标代码与宿主代码交织;Rust 过程宏则编译为独立 crate 供编译器交互。

Swift 选择了后者:宏定义是独立的程序,通过 swift-syntax 包操作 Swift 语法树,以符合ExpressionMacro协议的类型来表达:

public protocol ExpressionMacro: FreestandingMacro { /// Expand a macro described by the given freestanding macro expansion /// within the given context to produce a replacement expression. static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax }

expansion(of:in:)方法接收宏展开表达式的语法节点(例如#stringify(x + y))以及提供编译上下文的"context",产出包含重写后语法树的宏结果。MacroExpressionMacroMacroExpansionContext的细节将在下文详细设计中展开。

动手实现第一个表达式宏:StringifyMacro

stringify的实现是一个符合ExpressionMacro的新类型StringifyMacro

import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros public struct StringifyMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) -> ExprSyntax { guard let argument = node.argumentList.first?.expression else { fatalError("compiler bug: the macro does not have any arguments") } return "(\(argument), \(literal: argument.description))" } }

这个实现非常精简,因为stringify本身足够简单:

  1. 从语法树中取出宏参数(即#stringify(x + y)中的x + y);
  2. 通过字符串插值构造元组表达式:第一个元素是原始参数表达式本身,第二个元素用literal:插值把argument.description(参数的源码文本)转义为字符串字面量
  3. 该字符串被解析为表达式,产生ExprSyntax节点作为宏展开结果。

这实际上是SwiftSyntaxBuilder模块提供的一种**准引用(quasi-quoting)**形式:主要语法节点(此处为ExprSyntax)实现了ExpressibleByStringInterpolation,允许把已有语法节点插值到包含展开后 Swift 代码的字符串字面量中,再整体解析回语法树。

最后,还需要把声明与实现"绑定"起来。SE-0382 提出用内置宏externalMacro来指代宏实现所在的模块与类型名(写在=之后):

@freestanding(expression) macro stringify<T>(_: T) -> (T, String) = #externalMacro(module: "ExampleMacros", type: "StringifyMacro")

详细设计:宏声明语法

宏声明由以下文法描述(SE-0382 原文):

declaration -> macro-declaration macro-declaration -> macro-head identifier generic-parameter-clause[opt] macro-signature macro-definition[opt] generic-where-clause[opt] macro-head -> attributes[opt] declaration-modifiers[opt] 'macro' macro-signature -> parameter-clause macro-function-signature-result[opt] macro-function-signature-result -> '->' type macro-definition -> '=' expression

要点如下:

  • @freestanding(expression)属性仅适用于宏,标明该宏是表达式宏。"freestanding"一词来自 宏愿景文档,用于描述以#前缀展开的宏。
  • 宏签名为函数式:包含参数子句(可为空)与可选的结果类型。
  • 宏只能在文件作用域声明,且可与函数一样重载——只要参数标签、参数类型或结果类型不同即可。
  • macro-definition提供展开实现,按一般表达式解析,但必须是宏展开表达式macro-expansion-expression)。因此所有非内置宏都以其他宏定义,最终终止于由编译器提供实现的内置宏。定义中macro-expansion-expression的参数必须是对外层宏参数的直接引用,或字面量。
  • 宏参数可以带默认值,但默认值只能由字面量表达式和其他宏展开构成
  • 宏支持不透明结果类型,但唯一性规则与函数不同:每次宏展开产出的不透明类型都被视为不同类型。例如下面代码是非法的:
@freestanding(expression) macro someMacroWithOpaqueResult() -> some Collection<UInt8> var a = #someMacroWithOpaqueResult a = #someMacroWithOpaqueResult // cannot assign value with type of macro expansion here to opaque type from macro expansion above

详细设计:宏展开流程

宏展开表达式的文法(SE-0382 原文):

primary-expression -> macro-expansion-expression macro-expansion-expression -> '#' identifier generic-argument-clause[opt] function-call-argument-clause[opt] trailing-closures[opt]

#语法是刻意选定的:Swift 已包含大量#前缀的"类宏"表达式,其中一些可以直接实现为表达式宏。identifier引用的宏必须是表达式宏(由声明上的@freestanding(expression)标明)。

function-call-argument-clausetrailing-closures都是可选的;两者都省略时,宏按提供了空参数列表()展开。宏不像函数那样是一等实体,不能被当作值传递,也不需要"未应用宏"语法——这让#line等宏不必写成#line()。这也与属性包装器(用于附属宏)的先例一致。

两阶段展开

当源码中出现宏展开时,其展开分两个阶段:

第一阶段(类型检查阶段):宏参数对照命名宏的参数进行类型检查,命名宏的结果类型对照宏展开发生的上下文检查。这与函数调用的类型检查等价,不涉及宏定义本身

第二阶段(宏展开阶段):宏参数的语法提供给宏定义。对内置宏,行为取决于该宏的语义——例如externalMacro会调用外部程序,向其提供宏展开的源码;对其他宏,参数被替换进定义的macro-expansion-expression中。提案用prohibitBinaryOperatorsaddBlocker展示了宏之间的组合:

@freestanding(expression) macro prohibitBinaryOperators<T>(_ value: T, operators: [String]) -> T = #externalMacro(module: "ExampleMacros", type: "ProhibitBinaryOperators") @freestanding(expression) macro addBlocker<T>(_ value: T) -> T = #prohibitBinaryOperators(value, operators: ["+"]) #addBlocker(x + y * z)

#addBlocker(x + y * z)的展开会先变成#prohibitBinaryOperators(x + y * z, operators: ["+"]),再交由ExampleMacros.ProhibitBinaryOperators(一个符合ExpressionMacro的结构体)处理。

宏展开产出新的源码(语法树),随后以原宏结果类型作为上下文类型接受类型检查。例如stringify返回(T, String),参数为Int时,展开结果按如下右侧方式检查:

let _: (Int, String) = <macro expansion result>

嵌套展开、递归限制与性能优化

宏展开表达式可以出现在宏参数中:

#addBlocker(#stringify(1 + 2))

第一阶段不做任何展开:#stringify(1 + 2)推断出其TInt、产生(Int, String)值;addBlocker推断出其T(Int, String)。第二阶段从外向内展开:先展开addBlocker得到#prohibitBinaryOperators(#stringify(1 + 2), operators: ["+"]),再展开prohibitBinaryOperators,其产出的结果重新类型检查时会再次类型检查并最终展开#stringify(1 + 2)

实现层面,编译器保留避免重复类型检查的权利:当同一语法节点被原样复用时,可复用第一阶段算出的类型——这是类型检查器的重要性能优化。

此外还有两条硬性规则:

  • 宏展开不能递归:若某个宏的展开产出的源码再次展开同一宏,程序非法——这防止了无界宏展开;
  • 除内置的源码位置宏(#fileID#line等)外,宏不能用作参数的默认参数。源码位置宏作为默认参数时,会在调用点按调用方的源码位置展开,这是既有且有用的行为,但不一定适合所有宏,故提案先禁止(非内置)宏作默认参数以避免困惑,并保留未来重新审视的空间。

宏实现库:协议与上下文

宏定义使用 swift-syntax 包(提供 Swift 语法树操作与解析能力),其中SwiftSyntaxMacros模块提供定义宏所需的功能。

Macro协议族

public protocol Macro { }

Macro是所有宏定义的根协议,目前没有任何要求。所有 freestanding 宏符合FreestandingMacro

public protocol FreestandingMacro: Macro { }

表达式宏由ExpressionMacro描述,是 freestanding 宏的一种:

public protocol ExpressionMacro: FreestandingMacro { /// Expand a macro described by the given freestanding macro expansion syntax node /// within the given context to produce a replacement expression. static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax }

FreestandingMacroExpansionSyntax协议是描述上文macro-expansion-expression文法项的 swift-syntax 节点,携带宏展开在源码中出现的完整语法树(含所有空白与注释)

宏实现若无法继续展开,可以抛出错误而非尝试产出新语法节点,编译器会将该错误报告给用户;更详细的诊断可通过宏展开上下文提供。

MacroExpansionContext:上下文三件套

宏展开上下文提供宏展开环境的信息,可在展开过程中查询:

/// Protocol whose conforming types provide information about the context in /// which a given macro is being expanded. public protocol MacroExpansionContext: AnyObject { /// Generate a unique name for use in the macro. public func makeUniqueName(_ name: String) -> TokenSyntax /// Emit a diagnostic (i.e., warning or error) that indicates a problem with the macro /// expansion. public func diagnose(_ diagnostic: Diagnostic) /// Retrieve a source location for the given syntax node. /// /// - Parameters: /// - node: The syntax node whose source location to produce. /// - position: The position within the syntax node for the resulting /// location. /// - filePathMode: How the file name contained in the source location is /// formed. /// /// - Returns: the source location within the given node, or `nil` if the /// given syntax node is not rooted in a source file that the macro /// expansion context knows about. func location( of node: some SyntaxProtocol, at position: PositionInSyntaxNode, filePathMode: SourceLocationFilePathMode ) -> AbstractSourceLocation? }

三个操作各有明确用途:

  • makeUniqueName(_:):生成唯一名字,使宏展开能产出不会与同作用域其他声明冲突的新声明。返回的标识符 token 会融合传入的name以便调试。这让宏在一定程度上"更卫生"——不会引入影响宏参数代码类型检查的新名字。
  • diagnose(_:):让宏实现在展开期间产出诊断(警告或错误)。Diagnostic类型属于 swift-syntax 库,可表达编译器会产出的各类诊断:警告、错误、范围高亮、Fix-It 与附加注释。典型场景是"宏参数类型检查通过,但宏实现无法理解其使用的某些 Swift 语法"。产出诊断的宏仍应产出展开结果,除非它也抛错——此时诊断与错误都会被报告。该 API 由展开宏的工具(如编译器)呈现。
  • location(of:at:filePathMode:):确定语法节点的源码位置信息(文件、行、列)。positionfilePathMode可定制输出——例如指向语法节点的哪一部分、文件名字如何呈现。

配套的枚举与结构体如下:

/// Describe the position within a syntax node that can be used to compute /// source locations. public enum PositionInSyntaxNode { /// Refers to the start of the syntax node's leading trivia, which is /// the first source location covered by the syntax node. case beforeLeadingTrivia /// Refers to the start of the syntax node's first token, which /// immediately follows the leading trivia. case afterLeadingTrivia /// Refers to the end of the syntax node's last token, right before the /// trailing trivia. case beforeTrailingTrivia /// Refers just past the end of the source text that is covered by the /// syntax node, after all trailing trivia. case afterTrailingTrivia } /// Describes how a source location file path will be formed. public enum SourceLocationFilePathMode { /// A file ID consisting of the module name and file name (without full path), /// as would be generated by the macro expansion `#fileID`. case fileID /// A full path name as would be generated by the macro expansion `#filePath`, /// e.g., `/home/taylor/alison.swift`. case filePath }

源码位置以抽象形式描述,可插值进期望字符串字面量(文件名)或整数字面量(行、列)的表达式位置。与makeUniqueName返回TokenSyntax而非String同理,这种抽象允许编译器引入特殊语法节点(甚至普通 Swift 无法表达的形式)来表示这些值:

/// Abstractly represents a source location in the macro. public struct AbstractSourceLocation { /// A primary expression that represents the file and is `ExpressibleByStringLiteral`. public let file: ExprSyntax /// A primary expression that represents the line and is `ExpressibleByIntegerLiteral`. public let line: ExprSyntax /// A primary expression that represents the column and is `ExpressibleByIntegerLiteral`. public let column: ExprSyntax }

提案同时指出:MacroExpansionContext有意设计为可随时间扩展,未来会纳入更多构建环境信息,例如目标平台信息(OS、架构、部署版本)以及通过-D传入的编译期定义。

标准库中的宏:收编既有#表达式

#语法与众多既有内建表达式(如#line)相同,因此 SE-0382 提议把这些内建表达式收编为 Swift 标准库中的宏。宏实现仍由编译器提供,甚至可能涉及纯语法宏无法实现的部分;但通过提供宏声明,语言中不再需要为它们保留特例,并可享受为宏提供的全套工具能力。

externalMacro内置宏

macro externalMacro<T>(module: String, type: String) -> T

参数标识提供外部宏定义的类型所在模块与类型名。注意externalMacro很特殊:它只能被展开来定义另一个宏,在别处使用即为错误——这就是它不带@freestanding(expression)属性的原因。

源码位置宏

// File and path-related information @freestanding(expression) macro fileID<T: ExpressibleByStringLiteral>() -> T @freestanding(expression) macro file<T: ExpressibleByStringLiteral>() -> T @freestanding(expression) macro filePath<T: ExpressibleByStringLiteral>() -> T // Current function @freestanding(expression) macro function<T: ExpressibleByStringLiteral>() -> T // Source-location information @freestanding(expression) macro line<T: ExpressibleByIntegerLiteral>() -> T @freestanding(expression) macro column<T: ExpressibleByIntegerLiteral>() -> T // Current shared object handle. @freestanding(expression) macro dsohandle() -> UnsafeRawPointer

绝大多数提供源码位置信息的操作可实现为符合ExpressionMacro的类型,借助MacroExpansionContextlocation操作。例外包括:#file(需扩展MacroExpansionContext以区分#file表现为#fileID还是#filePath的编译模式,相关背景见 SE-0285 关于#file迁移的提案)、dsohandle(需要特定编译器支持)、#function(需要MacroExpansionContext中不存在的上下文信息)。

这些签名捕获了既有#file#line等的大部分类型系统行为——它们像字面量一样处理,可适配任何实现了对应ExpressibleBy*协议的上下文类型。但上面的实现无法通过下面这类代码的类型检查:

let x = #file

会报类似error: generic parameter 'T' could not be inferred的错误。要匹配既有#file#line的行为,需要一个与字面量类型一致的类型默认化规则;目前这需要编译器特殊处理,未来若语言支持"默认泛型参数",或许可直接在类型系统中表达。

Objective-C 辅助宏

#selector#keyPath的语法与类型检查行为可用宏声明表达:

@freestanding(expression) macro selector<T>(_ method: T) -> Selector @freestanding(expression) macro selector<T>(getter property: T) -> Selector @freestanding(expression) macro selector<T>(setter property: T) -> Selector @freestanding(expression) macro keyPath<T>(_ property: T) -> String

这些宏无法基于本提案的设施实现为ExpressionMacro类型,因为需要确定宏展开参数(如#selector(getter: Person.name))中引用了哪些声明。但为它们提供带内置实现的宏声明,可以降低其"特殊性",减少语言中的特例。

对象字面量宏

@freestanding(expression) macro colorLiteral<T: ExpressibleByColorLiteral>(red: Float, green: Float, blue: Float, alpha: Float) -> T @freestanding(expression) macro imageLiteral<T: ExpressibleByImageLiteral>(resourceName: String) -> T @freestanding(expression) macro fileLiteral<T: ExpressibleByFileReferenceLiteral>(resourceName: String) -> T

对象字面量允许在程序中引用各类资源。上述签名并非对象字面量当前类型检查方式的精确写照(它们不必然是泛型的):现在编译器会在当前模块中查找特殊命名类型(如_ColorLiteralType)作为对应字面量的类型。为保持行为不变,提案提出对对象字面量的宏展开执行与今天相同的查找,再把该类型作为对应宏的泛型实参——这样从语言内置的特殊对象字面量表达式迁移到带内置实现的宏声明时,类型检查行为完全不变。

沙箱化宏实现:安全与可预测性

宏实现模块如何构建与提供给编译器,由 SE-0394 "Package Manager Support for Custom Macros" 等后续提案负责。但 SE-0382 明确划出了一条安全底线:宏实现将在沙箱中执行(与 SE-0303 SwiftPM 可扩展构建工具 的安全模型类似),禁止文件系统与网络访问。

这既是安全预防措施,也是务实引导:宏不应依赖除"待展开的宏展开节点及其子节点(而非父节点)"和"宏展开上下文明确提供的信息"之外的任何状态。未来若宏需要访问更多信息,将通过扩展宏展开上下文实现——这也让编译器得以跟踪宏究竟查询了哪些信息。

工具链:使用与开发宏的支撑

宏最核心的关切之一是易用性与可开发性:我们如何知道宏对程序做了什么?如何开发、调试一个新宏?

得益于"宏展开结果永远是 Swift 源码"的语法模型,第一个问题很容易回答。工具至少应能展示任意宏用法的展开结果,最小集合包括传给编译器的展开标志(原型提供了-Xfrontend -dump-macro-expansions),未来可能包括输出"宏展开后源码文件"的模式(类似 C 编译器输出预处理文件)。IDE 也应能就地展示某个宏用法的展开,方便开发者检视宏行为——因为结果总是 Swift 源码,比检视操作 AST/IR 的宏实现更容易推理。

宏实现是独立程序这一事实反而让开发更容易:可以为宏实现编写单元测试——提供宏的输入源码(如#stringify(x + y)),用 swift-syntax 的设施展开宏,验证结果代码无语法错误且与期望一致。swift-syntax 仓库中的宏系统测试文件即以这种方式开发了大多数"内置"宏示例。

在 SE-0394 中,这一开发与分发模型被具体化为 SwiftPM 的.macro目标类型:宏被构建为面向宿主平台的可执行文件,编译器通过构建系统传入可执行文件路径,在编译过程中按需运行。宏实现通过CompilerPlugin入口点暴露:

import SwiftSyntax import SwiftCompilerPlugin import SwiftSyntaxBuilder import SwiftSyntaxMacros @main struct MyPlugin: CompilerPlugin { var providingMacros: [Macro.Type] = [FontLiteralMacro.self] }

一个最小包同时包含宏实现、宏定义与宏客户端,并辅以MacroTests测试目标:

import PackageDescription import CompilerPluginSupport let package = Package( name: "MacroPackage", dependencies: [ .package(url: "https://github.com/apple/swift-syntax", from: "509.0.0"), ], targets: [ .macro(name: "MacroImpl", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax") ]), .target(name: "MacroDef", dependencies: ["MacroImpl"]), .executableTarget(name: "MacroClient", dependencies: ["MacroDef"]), .testTarget(name: "MacroTests", dependencies: ["MacroImpl"]), ] )

宏实现测试可声明对宏目标的依赖(与可执行目标测试类似)。SwiftPM 通过-load-plugin-executable传递宏可执行文件路径,例如-load-plugin-executable /path/to/package/.build/debug/MacroImpl#MacroImpl#后是可用逗号分隔的模块名列表,对应#externalMacro声明中module参数的引用)。SwiftSyntax 的版本方案基于 Swift 主版本(如 509.0.0 对应 Swift 5.9),SwiftPM 的依赖解析会为所有宏及客户端合并到同一版本的 SwiftSyntax。

更多表达式宏示例

表达式宏的用途远超前文展示,提案收集了若干基于既有#表达式与社区灵感的示例(原型实现可在 swift-syntax 仓库的宏系统测试文件中找到)。

#colorLiteral:为给定的红、绿、蓝、透明值提供颜色字面量语法:

// Declaration of #colorLiteral @freestanding(expression) macro colorLiteral(red: Float, green: Float, blue: Float, alpha: Float) -> _ColorLiteralType = SwiftBuiltinMacros.ColorLiteralMacro // Implementation of #colorLiteral struct ColorLiteralMacro: ExpressionMacro { /// Replace the label of the first element in the tuple with the given /// new label. func replaceFirstLabel( of tuple: TupleExprElementListSyntax, with newLabel: String ) -> TupleExprElementListSyntax{ guard let firstElement = tuple.first else { return tuple } return tuple.replacing( childAt: 0, with: firstElement.withLabel(.identifier(newLabel))) } static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) -> ExprSyntax { let argList = replaceFirstLabel( of: node.argumentList, with: "_colorLiteralRed" ) let initSyntax: ExprSyntax = ".init(\(argList))" if let leadingTrivia = node.leadingTrivia { return MacroResult(initSyntax.withLeadingTrivia(leadingTrivia)) } return initSyntax } }

同样的思路可用于文件与图片字面量。注意它把元组第一个参数的标签替换为_colorLiteralRed,生成.init(...)调用并保留前导 trivia(空白与注释),保持源码外观整洁。

Power assertions(强大断言):由 Kishikawa Katsumi 提出,断言宏捕获断言表达式中的中间值,断言失败时展示这些值。原型输出如下:

#powerAssert(mike.isTeenager && john.age < mike.age) | | | | | | | | | true | | 42 | | 13 | | | | Person(name: "Mike", age: 13) | | | false | | Person(name: "John", age: 42) | false Person(name: "Mike", age: 13)

兼容性与稳定性影响

  • 源码兼容性:宏是纯语言扩展,使用全新语法,不影响源码兼容性;
  • ABI 稳定性:宏是源码到源码的转换工具,无 ABI 影响;
  • API 弹性:宏是源码到源码的转换工具,对 API resilience 无影响。

未来方向

宏参数类型信息

宏参数在调用宏实现前已完成完整类型检查,但该类型检查产生的信息并未提供给宏——宏只能拿到原始源码。某些场景下,参数及其子表达式的类型、表达式内引用声明的全名、类型检查中的隐式转换等信息会非常有用。例如 power assertions 的应用:

#assert(Color(parsing: "red") == .red)

实现希望把==的两个操作数拆到局部变量(用唯一名)以捕获值:

{ let _unique1 = Color(parsing: "red") let _unique2 = .red if !(_unique1 == _unique2) { fatalError("assertion failed: \(_unique1) != \(_unique2)") } }()

但这段代码无法通过类型检查——_unique2的初始化需要上下文信息来解析.red。若宏实现能拿到两个子表达式的类型,就能生成可正确类型检查的版本:

{ let _unique1: Color = Color(parsing: "red") let _unique2: Color = .red if !(_unique1 == _unique2) { fatalError("assertion failed: \(_unique1) != \(_unique2)") } }()

宏展开上下文可扩展出产生语法节点类型的操作:

extension MacroExpansionContext { func type(of node: ExprSyntax) -> Type? }

Type需要能表达 Swift 类型系统的广度:元组、函数等结构化类型,以及 struct、enum、actor、protocol 等名义类型。还可提供解析后声明的信息,例如.red解析为Color.red==解析为比较两个Color的具体运算符声明。此方向的主要复杂度在于定义描述 Swift 类型系统的稳定 API,提案认为它价值极高,但范围较大,最好作为后续独立提案引入。

更多种类的宏

表达式只是语言中宏能发挥作用的一处。其他位置包括函数或闭包体(如添加追踪或日志)、类型或扩展定义内部(如添加新成员)、协议一致性上(如综合协议一致性)——宏愿景文档 提出了大量候选想法。这些方向后来分别由 SE-0389 附属宏(member、peer、accessor、conformance 等角色)与 SE-0397 自由声明宏(@freestanding(declaration)角色,可收编 SE-0196 的#warning/#error)逐步落地。基本macro声明形态保持一致,区别仅在于宏可用的上下文、展开的拼写(声明上可能用@更合适)、标明宏类型的属性,以及SwiftSyntaxMacros模块中对应的继承自Macro的协议。

结语

SE-0382 确立了 Swift 宏系统的第一块基石:类型检查优先、源码到源码转换、独立程序实现、上下文受控、沙箱化执行。理解表达式宏,就等于理解了整个 Swift 宏体系的核心运转机制——两阶段展开、#externalMacro绑定、ExpressionMacro协议与MacroExpansionContext的职责划分。在此之上,附属宏、自由声明宏 与 SwiftPM 宏打包分发 共同构成了 Swift 5.9 完整可用的宏生态。无论你接下来要编写#stringify式的工具宏,还是设计领域专用的 DSL,本文梳理的声明、展开、上下文与工具链知识都是你直接可用的起点。

  • 文档

【免费下载链接】swift-evolution

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.

项目地址:https://gitcode.com/gh_mirrors/sw/swift-evolution
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 9:00:53

MTK6575 USB驱动实操:Host+OTG双模从加载失败到稳定枚举

简介&#xff1a;本资源为MTK6575平台USB驱动的完整源码包&#xff0c;面向嵌入式Linux驱动开发工程师、Android底层开发者及芯片级固件研究者&#xff0c;聚焦USB协议栈在MediaTek单核移动处理器上的具体实现与调试。资源包含42个文件&#xff0c;其中20个C文件实现主机/设备模…

作者头像 李华
网站建设 2026/9/23 8:59:34

day03学习校准法:用认知验证替代时间打卡

1. 这不是日程表&#xff0c;而是一套可验证的学习操作系统“day03-学习计划和进度”——看到这个标题&#xff0c;很多人第一反应是&#xff1a;又一个打卡模板&#xff1f;又一份Excel表格&#xff1f;又一段“今天学了2小时Python”的流水账&#xff1f;但在我带过87个自学转…

作者头像 李华
网站建设 2026/9/23 8:58:43

GTA6实体盒不含光盘?标准版与豪华版预购选择全解析

标准版和豪华版都摆在预购页上了&#xff0c;很多人却在“实体盒里没光盘”这句话上卡住了&#xff1a;盒子到底盒子里装什么&#xff1f;我买它图个啥&#xff1f;这个版本和纯数字版有什么区别&#xff1f;如果你正在纠结这两个版本怎么选&#xff0c;这篇文章就是把这笔账给…

作者头像 李华
网站建设 2026/9/23 8:56:10

AI代理上岗:本地模型如何成为数字隐私守门人

AI代理这个词最近出镜率实在太高了&#xff0c;高到快被说烂了。什么"AI代理将取代程序员"、"AI代理改变工作流"&#xff0c;听着确实提气&#xff0c;但很多人没意识到&#xff0c;比"干掉某个岗位"更早发生、影响也更深远的一件事是&#xff1…

作者头像 李华
网站建设 2026/9/23 8:56:09

家长最怕的那几件事,湘楚有才单招是怎么回应的

怕孩子管不住自己这是家长最普遍的焦虑。孩子在家备考,手机不离手,短视频一刷就是两小时。书桌前一坐,发呆的时间比看书还长。说多了嫌烦,说少了没用。家长急得不行,孩子却像没事人一样。更让家长无奈的是,这种状态不是一天两天了,从初中到高中,反复说过、吵过、甚至没收过手机…

作者头像 李华
网站建设 2026/9/23 8:55:41

OpenSpec规格管理实战:从散落文档到活契约的落地指南

1. 从"规格散落一地"说起&#xff1a;OpenSpec 到底想解决什么问题如果你参与过稍微有点规模的软件项目&#xff0c;大概率经历过这样的场景&#xff1a;需求文档在飞书里、接口定义在 Swagger 里、数据库字段在某个 Excel 里、字段校验规则藏在后端代码的 if-else 里…

作者头像 李华