Lean 语言参考手册

23.5. 宏🔗

是从 SyntaxSyntax 的变换,发生在精译期间以及策略执行期间。 用宏变换所得的结果替换语法,称为宏展开。 一个语法种类可以关联多个宏,Lean 会按定义顺序尝试它们。 宏在一种单子中运行;该单子可访问一些编译期元数据,并能发出错误消息或委托给后续宏,但宏单子的能力远弱于精译单子。

宏与语法种类相关联。 内部表将语法种类映射到类型为 Syntax MacroM Syntax 的宏。 宏通过抛出 unsupportedSyntax 异常委托给表中的下一项。 当某个 Syntax 值的语法种类关联了一个不会抛出 unsupportedSyntax 的宏时,该值就是宏。 如果宏抛出任何其他异常,就会向用户报告错误。 语法类别与宏展开无关;不过,由于每种语法种类通常只关联一个语法类别,实践中它们不会相互干扰。

宏错误报告

以下宏会在参数是字面数值五时报告错误。 在其他所有情况下,它展开为自己的参数。

syntax &"notFive" term:arg : term open Lean in macro_rules | `(term|notFive 5) => Macro.throwError "'5' is not allowed here" | `(term|notFive $e) => pure e

应用于语法形式不是数值五的项时,精译成功:

5#eval notFive (2 + 3)
5

触发错误分支时,用户会收到错误消息:

#eval '5' is not allowed herenotFive 5
'5' is not allowed here

精译一段语法之前,精译器会检查其语法种类是否关联了宏。 这些宏会依次尝试。 如果某个宏成功,并可能返回了不同种类的语法,则会重复检查并继续展开宏,直到语法最外层不再是宏。 随后便可继续精译或执行策略。 只有语法的最外层(通常是一个 node)会被展开,而宏展开的输出可能包含本身也是宏的嵌套语法。 精译器到达这些嵌套宏时,会依次将其展开。

具体而言,Lean 中会在三种情形下进行宏展开:

  1. 在项精译期间,精译器会先展开待精译语法最外层的宏,再调用该语法的项精译器

  2. 在命令精译期间,精译器会先展开待精译语法最外层的宏,再调用该语法的命令精译器

  3. 在策略执行期间,最外层语法中的宏会在将该语法作为策略执行之前展开。

23.5.1. 卫生性🔗

如果一个宏的展开不会导致标识符捕获,那么该宏就是卫生的标识符捕获是指标识符最终指向的绑定位置并非源代码中该标识符出现处作用域内的绑定位置。 标识符捕获有两类:

  • 如果宏的展开引入了绑定器,那么宏参数中的标识符可能会因名称恰好相同而最终指向这些新引入的绑定器。

  • 如果宏的展开意图引用某个名称,但宏所用的上下文在局部绑定了该名称,或其中新引入了同名全局名称,那么它最终可能引用错误的名称。

第一类变量捕获可通过确保宏引入的每个绑定都使用新生成且全局唯一的名称来避免;第二类则可通过始终使用完全限定名称引用常量来避免。 每次调用宏时都必须重新生成新名称,以免递归宏中发生变量捕获。 这些技巧容易出错。 变量捕获问题很难测试,因为它依赖名称选择上的巧合;而始终贯彻这些技巧又会产生冗杂代码。

Lean 具有自动卫生机制:在几乎所有情况下,宏都会自动保持卫生。 通过给宏引入的标识符标注宏作用域来避免新引入绑定造成的捕获;宏作用域能唯一标识每次宏展开调用。 如果标识符的绑定处和使用处具有相同的宏作用域,那么它们由同一步宏展开引入,应当相互指代。 同理,宏生成代码中的全局名称使用处不会被展开上下文中的局部绑定捕获,因为这些使用处带有绑定出现处所没有的宏作用域。 为防止新引入的全局名称造成捕获,宏体生成代码中的潜在全局名称引用会被标注上引用时所有匹配全局名称的集合。 带有潜在指称对象标注的标识符称为预解析标识符Syntax.ident 构造器上的 Syntax.Preresolved 字段用于存储这些潜在指称对象。 精译期间,如果一个标识符关联了预解析的全局名称,那么其他全局名称不会被视为有效的引用目标。

在生成语法中引入宏作用域和预解析标识符发生于引用期间。 不通过引用来构造语法的宏也应以其他方式确保卫生性。 有关 Lean 卫生算法的更多细节,请参阅 Ullrich and de Moura (2020)Sebastian Ullrich and Leonardo de Moura, 2020. “Beyond notations: Hygienic macro expansion for theorem proving languages”. In Proceedings of the International Joint Conference on Automated Reasoning. and Ullrich (2023)Sebastian Ullrich, 2023. An Extensible Theorem Proving Frontend. Dr. Ing. dissertation, Karlsruhe Institute of Technology.

23.5.2. 宏单子🔗

宏单子 MacroM 的能力足以实现卫生性并报告错误。 宏展开不能直接修改环境、执行合一、检查当前局部上下文,也不能进行任何只在某个特定上下文中才有意义的操作。 因此,同一套宏机制可以贯穿 Lean 使用,也使宏比精译器更容易编写。

🔗定义
Lean.MacroM (α : Type) : Type
Lean.MacroM (α : Type) : Type

MacroM 单子是宏展开使用的主要单子。它含有生成卫生名称所需的信息, 也是 macro 定义所在的单子。

这是一个(相对)纯的单子:它不提供 IO,也不能直接访问 Environment。因此无法在其中进行任意环境内省;只能使用宏方法设施提供的受限查询, 也无法使用 IO.Ref 或其他有副作用的操作。若需要更多能力,可以改用 elab,并通过 adaptExpander 编写宏。

🔗定义

stx 是宏,expandMacro? stx 返回 some stxNew,其中 stxNew 是它的展开结果; 否则不返回展开结果。

🔗定义
Lean.Macro.trace (clsName : Lean.Name) (msg : String) : Lean.MacroM Unit
Lean.Macro.trace (clsName : Lean.Name) (msg : String) : Lean.MacroM Unit

向给定的跟踪类添加一条具有给定消息的新跟踪消息。

23.5.2.1. 异常与错误🔗

unsupportedSyntax 异常用于宏展开期间的控制流。 它表示当前宏无法展开所收到的语法,但并未发生错误。 由 throwErrorthrowErrorAt 抛出的异常会终止宏展开,并向用户报告错误。

🔗定义

抛出 unsupportedSyntax 异常。

🔗Lean.Macro.Exception 的构造子

不支持该语法的异常。它被单独保留,是因为宏展开器以它进行控制流: 如果一个宏不支持某段语法,系统就会尝试下一个宏。

🔗定义
Lean.Macro.throwError {α : Type} (msg : String) : Lean.MacroM α
Lean.Macro.throwError {α : Type} (msg : String) : Lean.MacroM α

抛出带有给定消息的错误,并使用当前 ref 提供位置信息。

🔗定义
Lean.Macro.throwErrorAt {α : Type} (ref : Lean.Syntax) (msg : String) : Lean.MacroM α
Lean.Macro.throwErrorAt {α : Type} (ref : Lean.Syntax) (msg : String) : Lean.MacroM α

抛出带有给定消息和位置信息的错误。

23.5.2.2. 与卫生性相关的操作🔗

卫生性通过向语法中出现的标识符添加宏作用域来实现。 通常,引用过程会添加所有必要的作用域,但直接构造语法的宏必须为其引入的标识符添加宏作用域。

🔗定义

递增宏作用域计数器,使动作 x 的主体内使用新的宏作用域。

🔗定义
Lean.Macro.addMacroScope (n : Lean.Name) : Lean.MacroM Lean.Name
Lean.Macro.addMacroScope (n : Lean.Name) : Lean.MacroM Lean.Name

为名称 n 添加一个新的宏作用域。

23.5.2.3. 查询环境🔗

宏只能有限地查询环境。 它们可以检查常量是否存在并解析名称,但无法进行更深入的内省。

🔗定义
Lean.Macro.hasDecl (declName : Lean.Name) : Lean.MacroM Bool
Lean.Macro.hasDecl (declName : Lean.Name) : Lean.MacroM Bool

若环境含有名为 declName 的声明,则返回 true

🔗定义

根据文件中的当前位置获取当前命名空间。

🔗定义
Lean.Macro.resolveNamespace (n : Lean.Name) : Lean.MacroM (List Lean.Name)
Lean.Macro.resolveNamespace (n : Lean.Name) : Lean.MacroM (List Lean.Name)

将给定名称解析为一组重载的命名空间。

🔗定义

将给定名称解析为一组重载的全局定义。每个候选项中的 List String 是推导出的投影列表; 这些投影与名称的组成部分存在歧义。

注意,此函数不会触发与保留名称关联的动作。Lean 存在保留名称;例如,定义 foo 会为陈述 foo 等于其定义的定理保留名称 foo.def,而与 foo.def 关联的动作会自动证明 该定理。在宏层面,名称会被解析,但动作不会执行;这些动作由精译器在把 Syntax 转换为 Expr 时执行。

23.5.3. 引用🔗

引用把代码标记为以 Syntax 类型的数据表示。 被引用的代码会被解析,但不会被精译——它必须在语法上正确,却不必有意义。 引用让以编程方式生成代码容易得多:无需逆向推导 Lean 解析器会产生的 node 值的具体嵌套结构,而可以直接调用解析器来创建它们。 这种方式面对语法重构也更稳健;重构可能改变解析树的内部结构,却不影响用户可见的具体语法。 Lean 中的引用由 `() 包围。

可以在开头的反引号和左括号之后写出被引用的语法类别或解析器名称,再跟一条竖线(|)。 作为特例,名称 tactic 可用于解析策略或策略序列。 若未提供语法类别或解析器,Lean 会同时尝试把引用解析为项和非空命令序列。 项引用的优先级高于命令引用,因此有歧义时会选择项解释;显式指明引用的是命令序列可覆盖这一选择。

项引用与命令引用的语法

在以下示例中,引用的内容既可以是函数应用,也可以是命令序列。 二者匹配文件中的同一区域,因此局部最长匹配规则与此无关。 项引用的优先级高于命令引用,所以该引用被解释为项。 项要求其反引用具有 TSyntax `term 类型,而不是 TSyntax `command

example (cmd1 cmd2 : TSyntax `command) : MacroM (TSyntax `command) := `($Application type mismatch: The argument cmd1 has type TSyntax `command but is expected to have type TSyntax `term in the application cmd1.rawcmd1 $Application type mismatch: The argument cmd2 has type TSyntax `command but is expected to have type TSyntax `term in the application cmd2.rawcmd2)

结果是两个如下所示的类型错误:

Application type mismatch: The argument
  cmd1
has type
  TSyntax `command
but is expected to have type
  TSyntax `term
in the application
  cmd1.raw

引用的类型(MacroM (TSyntax `command))不会用于选择结果,因为语法优先级先于精译应用。 此处,指定反引用为命令即可消除歧义,因为函数应用要求这些位置是项:

example (cmd1 cmd2 : TSyntax `command) : MacroM (TSyntax `command) := `($cmd1:command $cmd2:command)

同样,在引用中插入一个命令,也会排除它是项的可能性:

example (cmd1 cmd2 : TSyntax `command) : MacroM (TSyntax `command) := `($cmd1 $cmd2 #eval "hello!")
语法引用

Lean 的语法包含项、命令、策略和策略序列的引用,也包含一种通用引用语法,可引用 Lean 能够解析的任何输入。 项引用优先级最高,其后依次是策略引用、通用引用,最后是命令引用。

term ::=
      `(term)
    | `(command+)
    | `(tactic|tactic)
    | `(tactic|tactic;*)
    | `(p : identp:ident|在此解析一个 p : identp )

引用的类型不是 Syntax,而是类型为 m Syntax 的单子动作。 引用之所以是单子的,是因为它会如卫生性一节所述,通过添加宏作用域和预解析标识符来实现卫生性。 要使用的具体单子是引用的隐式参数;只要某单子具有 MonadQuotation 类型类的实例,就可以使用。 MonadQuotation 扩展了 MonadRef,后者使引用能够访问宏展开器或精译器当前正在处理的语法的源位置。MonadQuotation 还提供了向标识符添加宏作用域以及为子任务使用新宏作用域的能力。 支持引用的单子包括 MacroMTermElabMCommandElabMTacticM

23.5.3.1. 准引用🔗

准引用是一种可包含反引用的引用形式;反引用是引用中不被引用的区域,其中的表达式会被求值以产生语法。 准引用本质上是一个模板;外层被引用区域提供固定框架,总是产生相同的外层语法,而反引用则产生最终语法中会变化的部分。 Lean 中所有引用都是准引用,因此无需特殊语法来区分准引用和其他引用。 引用过程不会给通过反引用插入的标识符添加宏作用域,因为这些标识符要么来自另一处引用(此时已具有宏作用域),要么来自宏的输入(此时不应有宏作用域,因为它们并非由宏引入)。

基本反引用由美元符号($)及其后紧邻的标识符组成。 这表示要在被引用语法的这个位置代入相应变量的值;该值应当是一棵语法树。 把表达式包在括号中,即可将整个表达式用作反引用。

Lean 的解析器会根据给定位置所期待的内容,为每个反引用指派一个语法类别。 如果解析器期待语法类别 c,那么反引用的类型就是 TSyntax c

某些语法类别可以由其他类别的元素匹配。 例如,数值和字符串字面量除了属于各自的语法类别外,也是有效的项。 可在反引用后附加冒号和类别名称来标注预期类别;这会让解析器验证所标注的类别在给定位置是否可接受,并在解析树中构造所需的中间层。

语法反引用
antiquot ::=
      $ident(:ident)?
    | $(term)(:ident)?

反引用起始的美元符号('$')与其后的标识符或带括号项之间不允许有空白。 同样,标注反引用语法类别的冒号两侧也不允许有空白。

准引用

本例使用了两种形式的反引用。 由于自然数不是语法,因此使用 quote 将数转换为表示该数的语法。

open Lean in example [Monad m] [MonadQuotation m] (x : Term) (n : Nat) : m Syntax := `($x + $(quote (n + 2)))
反引用标注

本例要求 m 是能够执行引用的单子。

variable {m : Type Type} [Monad m] [MonadQuotation m]

默认情况下,反引用 $e 应当是项,因为加法的第二个参数位置紧接着期待的就是这个语法类别。

def ex1 (e) := show m _ from `(2 + $e) ex1 {m : Type Type} [Monad m] [MonadQuotation m] (e : TSyntax `term) : m (TSyntax `term)#check ex1
ex1 {m : Type  Type} [Monad m] [MonadQuotation m] (e : TSyntax `term) : m (TSyntax `term)

$e 标注为数值字面量是可行的,因为数值字面量也是有效的项。 参数 e 的预期类型变为 TSyntax `num

def ex2 (e) := show m _ from `(2 + $e:num) ex2 {m : Type Type} [Monad m] [MonadQuotation m] (e : TSyntax `num) : m (TSyntax `term)#check ex2
ex2 {m : Type  Type} [Monad m] [MonadQuotation m] (e : TSyntax `num) : m (TSyntax `term)

美元符号与标识符之间不允许有空格。

def ex2 (e) := show m _ from `(2 +unexpected token '$'; expected '`(tactic|', 'do' or no space before spliced term $ e:num)
<example>:1:34-1:36: unexpected token '$'; expected '`(tactic|', 'do' or no space before spliced term

冒号之前同样不允许有空格:

def ex2 (e) := show m _ from `(2 + $eunexpected token ':'; expected ')' :num)
<example>:1:37-1:39: unexpected token ':'; expected ')'
展开准引用

打印 f 的定义可以展示准引用的展开结果。

open Lean in def f [Monad m] [MonadQuotation m] (x : Term) (n : Nat) : m Syntax := `(fun k => $x + $(quote (n + 2)) + k) def f : {m : Type Type} [Monad m] [Lean.MonadQuotation m] Lean.Term Nat m Syntax := fun {m} [Monad m] [Lean.MonadQuotation m] x n => do let info Lean.MonadRef.mkInfoFromRefPos let scp Lean.getCurrMacroScope let quotCtx Lean.MonadQuotation.getContext pure { raw := Syntax.node2 info `Lean.Parser.Term.fun (Syntax.atom info "fun") (Syntax.node4 info `Lean.Parser.Term.basicFun (Syntax.node1 info `null (Syntax.ident info "k".toRawSubstring' (Lean.addMacroScope quotCtx `k scp) [])) (Syntax.node info `null #[]) (Syntax.atom info "=>") (Syntax.node3 info `«term_+_» (Syntax.node3 info `«term_+_» x.raw (Syntax.atom info "+") (Lean.quote `term (n + 2)).raw) (Syntax.atom info "+") (Syntax.ident info "k".toRawSubstring' (Lean.addMacroScope quotCtx `k scp) []))) }.raw#print f
def f : {m : Type  Type}  [Monad m]  [Lean.MonadQuotation m]  Lean.Term  Nat  m Syntax :=
fun {m} [Monad m] [Lean.MonadQuotation m] x n => do
  let info  Lean.MonadRef.mkInfoFromRefPos
  let scp  Lean.getCurrMacroScope
  let quotCtx  Lean.MonadQuotation.getContext
  pure
      {
          raw :=
            Syntax.node2 info `Lean.Parser.Term.fun (Syntax.atom info "fun")
              (Syntax.node4 info `Lean.Parser.Term.basicFun
                (Syntax.node1 info `null (Syntax.ident info "k".toRawSubstring' (Lean.addMacroScope quotCtx `k scp) []))
                (Syntax.node info `null #[]) (Syntax.atom info "=>")
                (Syntax.node3 info `«term_+_»
                  (Syntax.node3 info `«term_+_» x.raw (Syntax.atom info "+") (Lean.quote `term (n + 2)).raw)
                  (Syntax.atom info "+")
                  (Syntax.ident info "k".toRawSubstring' (Lean.addMacroScope quotCtx `k scp) []))) }.raw

在此输出中,引用是一个 Lean.Parser.Term.do : termdo 块。 它首先为所得语法构造源信息;该信息通过向编译器查询当前正在处理的用户语法而获得。 然后,它取得当前宏作用域和正在处理的模块名称,因为宏作用域会相对于模块添加,以便独立编译并避免使用全局计数器。 接着,它使用 Syntax.node1Syntax.node2 等辅助函数构造节点;这些函数会创建具有指定子节点数量的 Syntax.node。 每个标识符都会添加宏作用域,并使用 TSyntax.raw 提取有类型语法包装器中的内容。 xquote (n + 2) 的反引用直接出现在展开结果中,作为 Syntax.node3 的参数。

23.5.3.2. 拼接🔗

除了通过反引用纳入其他语法外,准引用还可以包含拼接。 拼接表示要按顺序插入数组中的元素。 重复元素可以包含分隔符,例如列表或数组元素之间的逗号。 拼接可以是带有拼接后缀的普通反引用,也可以是提供额外重复结构的扩展拼接

拼接后缀由星号或一个有效原子后跟星号(*)组成。 后缀可以跟在任何标识符反引用或项反引用之后。 带有拼接后缀 * 的反引用对应 manymany1 的用法;语法规则中的 *+ 后缀都对应 * 拼接后缀。 星号前包含原子的拼接后缀对应 sepBysepBy1 的用法。 拼接后缀 ? 对应 optional 或语法规则中的 ? 后缀。 由于 ? 是有效的标识符字符,要把它用作后缀时必须给标识符加括号。

尽管语法的重复说明符与反引用后缀有所重叠,它们的语法并不相同。 定义语法时,Lean 内置了后缀 *+,*,+,*,?,+,?。 除了 , 以外,没有更简短的方式指定分隔符。 反引用后缀要么只是 *,要么是提供给 sepBysepBy1 的原子后跟 *。 语法重复 +* 对应拼接后缀 *;重复 ,*,+,*,?,+,? 对应 ,*。 语法和拼接中的可选后缀 ? 相互对应。

语法重复

拼接后缀

+*

*

,*,+,*,?,+,?

,*

sepBy(_, "S")sepBy1(_, "S")

S*

?

?

带后缀的拼接

本例要求 m 是能够执行引用的单子。

variable {m : Type Type} [Monad m] [MonadQuotation m]

默认情况下,反引用 $e 应当是一个以逗号分隔的项数组,正如列表体中所期待的那样:

def ex1 (xs) := show m _ from `(#[$xs,*]) ex1 {m : Type Type} [Monad m] [MonadQuotation m] (xs : Syntax.TSepArray `term ",") : m (TSyntax `term)#check ex1
ex1 {m : Type  Type} [Monad m] [MonadQuotation m] (xs : Syntax.TSepArray `term ",") : m (TSyntax `term)

不过,Lean 提供了一组不同数组表示之间的强制转换,可自动插入或移除分隔符,因此普通的项数组也可接受:

def ex2 (xs : Array (TSyntax `term)) := show m _ from `(#[$xs,*]) ex2 {m : Type Type} [Monad m] [MonadQuotation m] (xs : Array (TSyntax `term)) : m (TSyntax `term)#check ex2
ex2 {m : Type  Type} [Monad m] [MonadQuotation m] (xs : Array (TSyntax `term)) : m (TSyntax `term)

重复标注也可用于项反引用和语法类别标注。 本例位于 CommandElabM 中,以便方便地记录结果。

def ex3 (size : Nat) := show CommandElabM _ from do let mut nums : Array Nat := #[] for i in [0:size] do nums := nums.push i let stx `(#[$(nums.map (Syntax.mkNumLit toString)):num,*]) -- 在此使用 logInfo 会让语法经由 -- 美化打印器渲染。 logInfo stx #[0, 1, 2, 3]#eval ex3 4
#[0, 1, 2, 3]
非逗号分隔符

以下非常规列表语法使用破折号或双星号分隔数值元素,而不是逗号。

syntax "⟦" sepBy1(num, " — ") "⟧": term syntax "⟦" sepBy1(num, " ** ") "⟧": term

这意味着在 原子之间,—**** 都是有效的拼接后缀。 对于 ***,前两个星号是语法规则中的原子,第三个才是重复后缀。

macro_rules | `($n:num—*) => `($n***) | `($n:num***) => `([$n,*]) [1, 2, 3]#eval 1 2 3
[1, 2, 3]
可选拼接

以下语法声明可选地匹配两个词法单元之间的一个项。 嵌套 term 外的括号是必需的,因为 term? 是有效标识符。

syntax "⟨| " (term)? " |⟩": term

项的 ? 拼接后缀期待一个 Option Term

def mkStx [Monad m] [MonadQuotation m] (e : Option Term) : m Term := `(⟨| $(e)? |⟩) mkStx {m : Type Type} [Monad m] [MonadQuotation m] (e : Option Term) : m Term#check mkStx
mkStx {m : Type  Type} [Monad m] [MonadQuotation m] (e : Option Term) : m Term

提供 some 时,可选项会出现。

⟨| 5 |⟩#eval do logInfo ( mkStx (some (quote 5)))
⟨| 5 |⟩

提供 none 时,可选项不会出现。

⟨| |⟩#eval do logInfo ( mkStx none)
⟨| |⟩

23.5.3.3. 词法单元反引用🔗

除了完整语法的反引用外,Lean 还提供词法单元反引用,它允许用其他语法的源信息替换某个原子的源信息。 所得合成源信息会被标记为规范的,从而用于错误消息、证明状态及其他反馈。 其主要用途是控制 Lean 向用户报告错误消息或其他信息的位置。 词法单元反引用不允许通过求值插入任意原子。 词法单元反引用由一个原子(即关键字)组成

语法词法单元反引用

词法单元反引用会把词法单元上的源信息(类型为 SourceInfo)替换为其他语法的源信息。

antiquot ::= ...
    | atom%$ident

23.5.4. 匹配语法🔗

See Also

新语法使用语法扩展定义。

准引用可以用于模式匹配,以识别符合某个模板的语法。 正如用作项的引用中的反引用区域会被当作普通未引用表达式处理,模式中的反引用区域也会被当作普通 Lean 模式处理。 引用模式的编译方式不同于其他模式,因此不能在同一个 Lean.Parser.Term.match : termmatch 表达式中与非引用模式混用。 与普通引用一样,引用模式首先由 Lean 的解析器处理。 随后,解析器输出会被编译成判断是否匹配的代码。 语法匹配假定被匹配语法由 Lean 解析器产生——无论是通过引用还是直接来自用户代码——并利用这一点省略某些检查。 例如,如果某个位置只能出现特定关键字,就可以省略检查。

在以下情况下,语法与引用模式匹配:

原子

关键字原子(如 termIfThenElse : term`if c then t else e` 是 `ite c t e`(即“如果—那么—否则”)的记法;它根据 `c` 是否为真返回 `t` 或 `e`。 显式参数 `c : Prop` 本身没有计算内容;另有一个由实例合成得到的 `[Decidable c]` 参数,真正决定如何把 `c` 求值为真或假。 写成 `if h : c then t else e` 时表示依赖式条件 `dite`,此时 `t` 和 `e` 可以使用 `c` 为真或假的事实。 标识符中的记法约定:建议将 `if c then t else e` 写作 `ite`,并分别用 `left`、`right` 指代 `t`、`e`。ifLean.Parser.Term.match : termmatch)会产生单例节点,其种类是以 token. 开头、后接该原子。 在许多情况下,无需检查具体原子值,因为语法只允许一个关键字,此时不会执行检查。 如果被匹配项的语法要求该检查,就会比较节点种类。

字面量(如字符串或数值字面量)按其底层字符串表示比较。 模式 `(0x15) 与引用 `(21) 不匹配。

节点

如果模式和被匹配值都表示 Syntax.node,则当二者语法种类相同、子节点数相同,且每个子模式都匹配对应子值时,匹配成功。

标识符

如果模式和被匹配值都是标识符,则比较其字面 Name 值在忽略宏作用域后的相等性。 “看起来”相同的标识符会匹配,它们是否指向同一绑定并不重要。 这一设计使引用模式匹配可用于无法访问编译期环境、因而无法按引用比较名称的上下文。

由于引用模式匹配基于解析器发出的节点种类,外观相同的引用如果来自不同语法类别,也可能不匹配。 拿不准时,在引用中写明语法类别会有所帮助。

语法模式匹配所绑定的变量具有 TSyntax k 类型,其中 k 描述可能的语法种类。 重复中的变量具有 TSyntaxArray k 类型;如果重复以字符串 sep 分隔,则类型为 TSepArray k sep有类型语法一节会更详细地介绍 TSyntax

语法模式匹配

列表推导是一种受标准集合构造记法启发、用于书写列表的记法。 列表推导由方括号构成,其中先是结果项,随后是若干个限定子;每个限定子要么从另一个列表引入变量,要么施加必须满足的条件。 限定子是嵌套的:每个新变量的值都会针对之前的每个值求值。

syntax qbind := ident "←" term syntax qpred := term syntax qualifier := atomic(qbind) <|> qpred syntax "[" term "|" qualifier,* "]" : term

列表推导可以脱糖为一系列对 List.flatMap 的调用。 变量引入会被翻译成在该变量值表达式上调用 flatMap,而谓词会被翻译为条件表达式:谓词为真或假时分别返回一个值或零个值。 最后一个 flatMap 的函数体就是结果项。

这种脱糖可以实现为使用准引用模式的宏:

macro_rules | `(term|[$e | $qs,* ]) => do let init `([$e]) qs.getElems.foldrM (β := Term) (init := init) fun | `(qualifier|$x $e'), r => `(($e' : List _) |>.flatMap fun $x => $r) | `(qualifier|$e':term), r => `((if $e' then [()] else []) |>.flatMap fun () => $r) | other, _ => Macro.throwErrorAt other "Unknown qualifier"

起初,限定子序列的类型是 TSepArray `qualifier ",",表示它是以逗号分隔的限定子序列。 TSepArray.getElems 将其转换为 TSyntaxArray `qualifier,后者是 Array (TSyntax `qualifier) 的缩写。 这样便可用广义字段记法调用 Array.foldrM。 谓词分支中的 term 标注是必需的,以防匹配值的语法种类为 `qualifier;必须从该值外解开一个 node

列表推导的行为符合预期:

["2; true", "2; false", "4; true", "4; false"]#eval [ s!"{x}; {y}" | x (1...5).toList, x % 2 = 0, y [true, false] ]
["2; true", "2; false", "4; true", "4; false"]

23.5.5. 定义宏🔗

定义宏主要有两种方式:Lean.Parser.Command.macro_rules : commandmacro_rules 命令和 Lean.Parser.Command.macro : commandmacro 命令。 Lean.Parser.Command.macro_rules : commandmacro_rules 命令将宏关联到现有语法,而 Lean.Parser.Command.macro : commandmacro 命令会同时定义新语法以及把它翻译为现有语法的宏。 Lean.Parser.Command.macro : commandmacro 命令可视为 Lean.Parser.Command.notation : commandnotation 的推广:它允许以编程方式生成展开结果,而不只是通过代入生成。

23.5.5.1. macro_rules 命令🔗

语法使用 macro_rules 的基于规则的宏

Lean.Parser.Command.macro_rules : commandmacro_rules 命令接受一系列以语法模式匹配指定的重写规则,并把每条规则添加为一个宏。 这些规则按顺序尝试,且先于之前定义的宏;之后的宏定义还可继续添加宏规则。

command ::= ...
    | docComment?
      (@[attrInstance,*])?
      `attrKind` 匹配 `("scoped" <|> "local")?`,用于属性之前,例如 `@[local simp]`。attrKind macro_rules ((kind := ident))?
        (| `((p : identp:ident|)?适合 p : identp 的语法 ) => term)*

宏中的模式必须是引用模式。 它们可以匹配任意语法类别的语法,但一个给定模式只能匹配一种语法种类。 如果引用没有指定类别或解析器,它可以匹配项或命令(序列),但不能二者都匹配。 有歧义时会选择项解析器。

在内部,宏存储于一张从每个语法种类映射到其宏的表中。 Lean.Parser.Command.macro_rules : commandmacro_rules 命令可以显式标注语法种类。

如果显式提供了语法种类,宏定义会检查每个引用模式是否具有该种类。 如果引用的解析结果是一个选择节点(即解析有歧义),则模式会针对具有指定种类的每个备选项复制一次。 如果没有任何备选项具有指定种类,就会报错。

如果没有显式提供种类,则每个模式使用解析器确定的种类。 这些模式不必全都具有相同的语法种类;每种至少被一个模式使用的语法种类都会定义宏。 如果引用模式的解析结果是选择节点(即解析有歧义),就会报错。

Lean.Parser.Command.macro_rules : commandmacro_rules 关联的文档注释会在语法本身没有文档注释时显示给用户。 否则显示语法本身的文档注释。

记法运算符一样,宏规则也可声明为 scopedlocal。 作用域宏仅在当前命名空间打开时有效,而局部宏规则仅在当前节作用域内有效。

习语括号

习语括号是使用应用函子时的一种替代语法。 如果习语括号包含函数应用,则函数会被包在 pure 中,并使用 <*> 依次应用于每个参数。 Lean 默认不支持习语括号,但可以使用宏定义它们。

syntax (name := idiom) "⟦" (term:arg)+ "⟧" : term macro_rules | `($f $args*) => do let mut out `(pure $f) for arg in args do out `($out <*> $arg) return out

这套新语法可以立即使用。

def addFirstThird [Add α] (xs : List α) : Option α := Add.add xs[0]? xs[2]? none#eval addFirstThird (α := Nat) []
none
none#eval addFirstThird [1]
none
some 4#eval addFirstThird [1,2,3,4]
some 4
有作用域的宏

作用域宏规则只在其命名空间内有效。 当命名空间 ConfusingNumbers 打开时,数值字面量会被赋予错误含义。

namespace ConfusingNumbers

以下宏识别作为奇数数值字面量的项,并将其替换为数值的两倍。 如果它无条件替换为两倍数值,宏展开就会陷入无限循环,因为同一规则总会匹配输出。

scoped macro_rules | `($n:num) => do if n.getNat % 2 = 0 then Lean.Macro.throwUnsupported let n' := (n.getNat * 2) `($(Syntax.mkNumLit (info := n.raw.getHeadInfo) (toString n')))

命名空间结束后,该宏不再使用。

end ConfusingNumbers

不打开命名空间时,数值字面量按通常方式工作。

(3, 4)#eval (3, 4)
(3, 4)

命名空间打开时,宏会把 3 替换为 6

open ConfusingNumbers (6, 4)#eval (3, 4)
(6, 4)

通常,用宏改变数值或其他字面量的解释并没有用处。 不过,在给 trivial 这类可扩展策略添加新规则时,作用域宏非常有用:这些规则适合命名空间中的内容,却不应始终启用。

在幕后,一条 Lean.Parser.Command.macro_rules : commandmacro_rules 命令会为其引用模式所匹配的每种语法种类生成一个宏函数。 该函数有一个抛出 unsupportedSyntax 异常的默认分支,以便继续尝试其他宏。

一条包含两条规则的 Lean.Parser.Command.macro_rules : commandmacro_rules 命令,并不总是等价于两条各含一个匹配的独立命令。 首先,一条 Lean.Parser.Command.macro_rules : commandmacro_rules 中的规则按从上到下顺序尝试,但最近声明的宏会最先尝试,因此拆开时顺序需要反转。 此外,如果宏中较早的规则抛出 unsupportedSyntax 异常,后续规则不会再尝试;若它们位于不同的 Lean.Parser.Command.macro_rules : commandmacro_rules 命令中,则仍会被尝试。

一组与两组宏规则

arbitrary! 宏旨在展开为某个给定类型的任意选定值。

syntax (name := arbitrary!) "arbitrary! " term:arg : term macro_rules | `(arbitrary! ()) => `(()) | `(arbitrary! Nat) => `(42) | `(arbitrary! ($t1 × $t2)) => `((arbitrary! $t1, arbitrary! $t2)) | `(arbitrary! Nat) => `(0)

用户可以定义更多组宏规则来扩展它,例如这条针对 Empty 且会失败的规则:

macro_rules | `(arbitrary! Empty) => throwUnsupported (42, 42)#eval arbitrary! (Nat × Nat)
(42, 42)

如果所有宏规则都定义为独立分支,那么结果会改用后定义的 Nat 分支。 这是因为单条 Lean.Parser.Command.macro_rules : commandmacro_rules 命令中的规则按从上到下顺序检查,而较晚定义的 Lean.Parser.Command.macro_rules : commandmacro_rules 命令优先于较早定义的命令。

macro_rules | `(arbitrary! ()) => `(()) macro_rules | `(arbitrary! Nat) => `(42) macro_rules | `(arbitrary! ($t1 × $t2)) => `((arbitrary! $t1, arbitrary! $t2)) macro_rules | `(arbitrary! Nat) => `(0) macro_rules | `(arbitrary! Empty) => throwUnsupported (0, 0)#eval arbitrary! (Nat × Nat)
(0, 0)

此外,如果任一规则抛出 unsupportedSyntax 异常,该命令中的后续规则都不会再检查。

macro_rules | `(arbitrary! (List Nat)) => throwUnsupported | `(arbitrary! (List $_)) => `([]) macro_rules | `(arbitrary! (Array Nat)) => `(#[42]) macro_rules | `(arbitrary! (Array $_)) => throwUnsupported

List Nat 分支精译失败,因为宏展开没有把 arbitrary! : termarbitrary! 语法翻译为精译器支持的内容。

#eval elaboration function for `arbitrary!` has not been implemented arbitrary! (List Nat)arbitrary! (List Nat)
elaboration function for `arbitrary!` has not been implemented
  arbitrary! (List Nat)

Array Nat 分支成功,因为第二组宏规则抛出异常后,会继续尝试第一组宏规则。

#[42]#eval arbitrary! (Array Nat)
#[42]

23.5.5.2. macro 命令🔗

Lean.Parser.Command.macro : commandmacro 命令会同时定义一条新的语法规则,并将其与一个关联。 Lean.Parser.Command.notation : commandnotation 只能定义新的项语法,其展开是将参数代入其中的项;与之不同,Lean.Parser.Command.macro : commandmacro 命令可以在任意语法类别中定义语法,并能在 MacroM 单子中使用任意代码生成展开结果。 由于宏比记法灵活得多,Lean 无法自动生成逆展开器;这意味着通过 Lean.Parser.Command.macro : commandmacro 命令实现的新语法可用于 Lean 的输入,但若不做进一步工作,Lean 的输出不会使用它。

语法宏声明
command ::= ...
    | docComment?
      (@[attrInstance,*])?
      `attrKind` 匹配 `("scoped" <|> "local")?`,用于属性之前,例如 `@[local simp]`。attrKind macro(:prec)? ((name := ident))? ((priority := prio))? macroArg* : ident =>
        macroRhs
语法宏参数

宏的参数要么是语法项(用法与 Lean.Parser.Command.syntax : commandsyntax 命令中相同),要么是附带名称的语法项。

macroArg ::=
    stx
macroArg ::= ...
    | ident:stx

在展开中,附加到语法项的名称会被绑定;它们的类型是适用于相应语法种类的 TSyntax。 如果解析器匹配的语法没有已定义的种类(例如因为名称应用于复杂说明),那么类型为 TSyntax Name.anonymous

文档注释与新语法关联;属性种类(无、localscoped)像记法一样控制宏的可见性:scoped 宏在定义它的命名空间中,或任何打开该命名空间的节作用域中可用,而 local 宏仅在局部节作用域中可用。

在幕后,Lean.Parser.Command.macro : commandmacro 命令本身由一个宏实现,该宏把它展开为一条 Lean.Parser.Command.syntax : commandsyntax 命令和一条 Lean.Parser.Command.macro_rules : commandmacro_rules 命令。 应用于宏命令的任何属性都会应用于语法定义,但不会应用于 Lean.Parser.Command.macro_rules : commandmacro_rules 命令。

23.5.5.3. 宏属性🔗

可使用 Lean.Parser.Attr.macro : attrmacro 属性手动把添加到某种语法种类。 这种指定宏的底层方式通常没有用处,除非它是由那些自身会生成宏定义的宏进行代码生成所得的结果。

属性macro 属性

Lean.Parser.Attr.macro : attrmacro 属性指定,应把某个函数视为指定语法种类的

attr ::= ...
    | macro ident
宏属性
/-- 根据某个项在语法上的 N 份副本生成列表 -/ syntax (name := rep) "[" num " !!! " term "]" : term @[macro rep] def expandRep : Macro | `([ $n:num !!! $e:term]) => let e' := Array.replicate n.getNat e `([$e',*]) | _ => throwUnsupported

对这个新表达式求值,可以看出宏已经存在。

["hello", "hello", "hello"]#eval [3 !!! "hello"]
["hello", "hello", "hello"]