Lean 语言参考

23.5. 宏🔗

Macros 是在 精化策略执行 期间发生的从 SyntaxSyntax 的转换。 用宏转换的结果替换语法称为 宏扩展。 多个宏可以与单个 语法类型 关联,并且按定义顺序尝试它们。 宏在 monad 中运行,该 monad 可以访问一些编译时元数据,并且能够发出错误消息或委托给后续宏,但宏 monad 的功能远不如精化monad 强大。

宏与 语法类型 关联。 内部表将语法类型映射到 Syntax MacroM Syntax 类型的宏。 宏通过抛出 unsupportedSyntax 异常委托给表中的下一个条目。 当一个给定的 Syntax是一个宏时,存在与其语法类型关联且不会抛出 unsupportedSyntax 的宏。 如果宏引发任何其他异常,则会向用户报告错误。 语法类别与宏展开无关;然而,由于每种语法类型通常与单个语法类别相关联,因此它们在实践中不会产生干扰。

Macro Error Reporting

当以下宏的参数为文字数字 5 时,将报告错误。 它扩展到所有其他情况下的论点。

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 的术语时,精化成功:

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. 卫生🔗

如果宏的扩展无法导致标识符捕获,则该宏为 hygienic标识符捕获是指标识符最终引用了源代码中该标识符出现的范围之外的绑定位点。 标识符捕获有两种类型:

  • 如果宏的扩展引入了绑定器,那么作为宏参数的标识符最终可能会引用引入的绑定器(如果它们的名称恰好匹配)。

  • 如果宏的扩展旨在引用一个名称,但该宏在本地绑定该名称或引入了新的全局名称的上下文中使用,则它最终可能会引用错误的名称。

第一种变量捕获可以通过确保宏引入的每个绑定都使用新生成的全局唯一名称来避免,而第二种变量捕获可以通过始终使用完全限定名称来引用常量来避免。 每次调用宏时都必须再次生成新名称,以避免递归宏中的变量捕获。 这些技术很容易出错。 变量捕获问题很难测试,因为它们依赖于名称选择的巧合,并且一致地应用这些技术会产生嘈杂的代码。

Lean 具有自动卫生功能:几乎在所有情况下,宏都会自动卫生。 通过使用 macroscopes 对宏引入的标识符进行注释,可以避免引入的绑定捕获,该标识符唯一标识宏展开的每次调用。 如果标识符的绑定和使用具有相同的宏范围,则它们是通过宏展开的同一步骤引入的,并且应该相互引用。 同样,宏生成的代码中全局名称的使用不会被扩展上下文中的本地绑定捕获,因为这些使用站点具有绑定出现中不存在的宏范围。 通过使用在宏主体中生成的代码中在引用时匹配的一组全局名称来注释潜在的全局名称引用,可以防止新引入的全局名称的捕获。 用潜在引用对象注释的标识符称为 预解析标识符,并且 Syntax.ident 构造函数上的 Syntax.Preresolved 字段用于存储潜在引用对象。 在精化期间,如果标识符具有与其关联的预解析全局名称,则其他全局名称不会被视为有效的引用目标。

宏作用域和预解析标识符的引入发生在 quotation 期间。 通过引用以外的其他方式构造语法的宏也应该通过其他方式确保卫生。 有关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 中使用相同的宏机制,并且它使宏的编写比 elaborators 容易得多。

🔗def
Lean.MacroM (α : Type) : Type
Lean.MacroM (α : Type) : Type

The MacroM monad is the main monad for macro expansion. It has the information needed to handle hygienic name generation, and is the monad that macro definitions live in.

Notably, this is a (relatively) pure monad: there is no IO and no access to the Environment. That means that things like declaration lookup are impossible here, as well as IO.Ref or other side-effecting operations. For more capabilities, macros can instead be written as elab using adaptExpander.

🔗def

expandMacro? stx returns some stxNew if stx is a macro, and stxNew is its expansion.

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

Add a new trace message, with the given trace class and message.

23.5.2.1. 异常和错误🔗

unsupportedSyntax 异常用于宏展开期间的控制流。 它表明当前宏无法扩展接收到的语法,但尚未发生错误。 throwErrorthrowErrorAt 引发的异常终止宏展开,并向用户报告错误。

🔗def

Throw an unsupportedSyntax exception.

🔗constructor of Lean.Macro.Exception

An unsupported syntax exception. We keep this separate because it is used for control flow: if one macro does not support a syntax then we try the next one.

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

Throw an error with the given message, using the ref for the location information.

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

Throw an error with the given message and location information.

23.5.2.2. 卫生相关操作🔗

Hygiene 是通过将 宏范围 添加到语法中出现的标识符来实现的。 通常,quotation 的过程会添加所有必要的作用域,但直接构造语法的宏必须将宏作用域添加到它们引入的标识符中。

🔗def

Increments the macro scope counter so that inside the body of x the macro scope is fresh.

🔗def
Lean.Macro.addMacroScope (n : Lean.Name) : Lean.MacroM Lean.Name
Lean.Macro.addMacroScope (n : Lean.Name) : Lean.MacroM Lean.Name

Add a new macro scope to the name n.

23.5.2.3. 查询环境🔗

宏对查询环境的支持非常有限。 他们可以检查常量是否存在并解析名称,但无法进行进一步的内省。

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

Returns true if the environment contains a declaration with name declName

🔗def

Gets the current namespace given the position in the file.

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

Resolves the given name to an overload list of namespaces.

🔗def

Resolves the given name to an overload list of global definitions. The List String in each alternative is the deduced list of projections (which are ambiguous with name components).

Remark: it will not trigger actions associated with reserved names. Recall that Lean has reserved names. For example, a definition foo has a reserved name foo.def for theorem containing stating that foo is equal to its definition. The action associated with foo.def automatically proves the theorem. At the macro level, the name is resolved, but the action is not executed. The actions are executed by the elaborator when converting Syntax into Expr.

23.5.3. 引述🔗

Quotation 标记用于表示为 Syntax 类型数据的代码。 引用的代码已被解析,但未详细说明 - 虽然它在语法上必须正确,但不一定有意义。 引用使得以编程方式生成代码变得更加容易:无需对 Lean 解析器将生成的 node 值的特定嵌套进行逆向工程,而是可以直接调用解析器来创建它们。 这在面对可能改变解析树的内部结构而不影响用户可见的具体语法的语法重构时也更加稳健。 Lean 中的报价被 `( and ) 包围。

被引用的语法类别或解析器可以通过将其名称放在左反引号和括号后面,后跟竖线(|)来指示。 作为特殊情况,名称 tactic 可用于解析策略或策略的序列。 如果未提供语法类别或解析器,Lean 会尝试将引用解析为术语和非空命令序列。 术语引用比命令引用具有更高的优先级,因此在有歧义的情况下,选择将解释为术语;这可以通过明确指示引用是命令序列来覆盖。

Term vs Command Quotation Syntax

在下面的示例中,引用的内容可以是函数应用程序,也可以是命令序列。 两者都匹配文件的同一区域,因此 本地最长匹配规则 不相关。 术语引用的优先级高于命令引用,因此引用被解释为术语。 条款期望其 反引号 具有类型 TSyntax `term rather than 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!")
syntaxQuotations

Lean 的语法包括术语、命令、策略和策略序列的引用,以及允许引用 Lean 可以解析的任何输入的通用引用语法。 术语引用的优先级最高,其次是策略引用、一般引用,最后是命令引用。

term ::=
      Syntax quotation for terms. `(term)
    | `(command+)
    | `(tactic|tactic)
    | `(tactic|tactic;*)
    | `(p : identp:ident|Parse a p : identp here )

引用不是类型 Syntax,而是类型为 m Syntax 的单子操作。 引用是一元的,因为它通过添加 宏范围 和预解析标识符来实现 卫生,如 卫生部分 中所述。 要使用的特定 monad 是引用的隐式参数,任何具有 MonadQuotation 类型类实例的 monad 都适用。 MonadQuotation 扩展了 MonadRef,这使引用能够访问宏扩展器或精化器当前正在处理的语法的源位置。 MonadQuotation 还包括将 宏范围 添加到标识符并为子任务使用新的宏范围的功能。 支持报价的 Monad 包括 MacroMTermElabMCommandElabMTacticM

23.5.3.1. 准报价🔗

Quasiquotation 是一种可能包含 antiquotations 的引用形式,反引用 是未引用的引用区域,而是计算结果语法的表达式。 准引用本质上是一个模板;外部引用区域提供了一个固定的框架,始终产生相同的外部语法,而反引号产生最终语法中不同的部分。 Lean 中的所有引用都是准引用,因此不需要特殊语法来区分准引用和其他引用。 引用过程不会将宏作用域添加到通过反引号插入的标识符,因为这些标识符要么来自另一个引用(在这种情况下它们已经具有宏作用域),要么来自宏的输入(在这种情况下它们不应该具有宏作用域,因为它们不是由宏引入的)。

基本反引号由美元符号 ($) 和紧随其后的标识符组成。 这意味着相应变量的值(应该是语法树)将被替换到引用语法的这个位置。 通过将整个表达式括在括号中,可以将其用作反引号。

Lean 的解析器根据解析器在给定位置的期望为每个反引号分配一个语法类别。 如果解析器需要语法类别 c,则反引号的类型为 TSyntax c

某些语法类别可以与其他类别的元素相匹配。 例如,数字和字符串文字除了是它们自己的语法类别之外,也是有效的术语。 反引号可以通过在反引号后面加上冒号和类别名称来注释预期类别,这会导致解析器验证带注释的类别在给定位置是否可接受,并构造解析树中所需的任何中间层。

syntaxAntiquotations
antiquot ::=
      $ident(:ident)?
    | $(term)(:ident)?

启动反引号的美元符号(“$”)与后面的标识符或括号内的术语之间不允许有空格。 同样,注释反引号的语法类别的冒号周围不允许有空格。

Quasiquotation

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

open Lean in example [Monad m] [MonadQuotation m] (x : Term) (n : Nat) : m Syntax := `($x + $(quote (n + 2)))
Antiquotation Annotations

此示例要求 m 是一个可以进行报价的 monad。

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|' or no space before spliced term $ e:num)
<example>:1:34-1:36: unexpected token '$'; expected '`(tactic|' 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 ')'
Expanding Quasiquotation

打印 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. 接头🔗

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

剪接后缀由星号或有效原子后跟星号 (*) 组成。 后缀可以跟在任何标识符或术语反引号后面。 带有拼接后缀 * 的反引号对应于 manymany1 的使用;语法规则中的 *+ 后缀均对应于 * 剪接后缀。 带有在星号之前包含原子的剪接后缀的反引号对应于 sepBysepBy1 的使用。 拼接后缀 ? 对应于语法规则中 optional? 后缀的使用。 由于 ? 是有效的标识符字符,因此标识符必须加括号才能将其用作后缀。

虽然语法的重复说明符和反引号后缀之间存在重叠,但它们具有不同的语法。 定义语法时,后缀 *+,*,+,*,?,+,? 内置于 Lean。 除了 , 之外,没有更短的方法来指定分隔符。 反引号后缀要么只是 *,要么是提供给 sepBysepBy1 的任何原子,后跟 *。 语法重复+*对应于拼接后缀*;重复 ,*,+,*,?,+,? 对应于 ,*。 语法和拼接中的可选后缀?相互对应。

语法重复

拼接后缀

+*

*

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

,*

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

S*

?

?

Suffixed Splices

此示例要求 m 是一个可以进行报价的 monad。

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,*]) -- Using logInfo here causes the syntax to be rendered via -- the pretty printer. logInfo stx #[0, 1, 2, 3]#eval ex3 4
#[0, 1, 2, 3]
Non-Comma Separators

以下列表的非常规语法通过长破折号或双星号而不是逗号分隔数字元素。

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]
Optional Splices

以下语法声明可以选择匹配两个标记之间的术语。 嵌套的 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 还具有 token antiquotations 功能,它允许原子的源信息替换为其他语法的源信息。 生成的合成源信息被标记为 canonical,以便它将用于错误消息、证明状态和其他反馈。 这主要用于控制 Lean 向用户报告的错误消息或其他信息的放置。 令牌反引号不允许通过求值插入任意原子。 标记反引号由一个原子(即关键字)组成

syntaxToken Antiquotations

令牌反引号将令牌上的源信息(类型为 SourceInfo)替换为其他语法中的源信息。

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

23.5.4. 匹配语法🔗

See Also

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

准引号可用于模式匹配来识别与模板匹配的语法。 正如用作术语的引用中的反引号是被视为普通非引用表达式的区域一样,模式中的反引号也是被视为普通 Lean 模式的区域。 引号模式的编译方式与其他模式不同,因此它们不能与单个 Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 表达式中的非引号模式混合。 与普通引用一样,引用模式首先由 Lean 的解析器处理。 然后解析器的输出被编译成代码来确定是否存在匹配。 语法匹配假定匹配的语法是由 Lean 的解析器通过引用或直接在用户代码中生成的,并使用它来省略一些检查。 例如,如果在给定位置中只能出现特定关键字,则可以省略该检查。

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

原子

关键字原子(例如 termIfThenElse : term`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to return `t` or `e` depending on whether `c` is true or false. The explicit argument `c : Prop` does not have any actual computational content, but there is an additional `[Decidable c]` argument synthesized by typeclass inference which actually determines how to evaluate `c` to true or false. Write `if h : c then t else e` instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact that `c` is true/false. ifLean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match)会生成类型为 token. 后跟原子的单例节点。 在许多情况下,没有必要检查特定的原子值,因为语法只允许单个关键字,并且不会执行任何检查。 如果匹配的术语的语法需要检查,则比较节点类型。

文字(例如字符串或数字文字)通过其底层字符串表示形式进行比较。 模式 `(0x15) and the quotation `(21) 不匹配。

节点

如果匹配的模式和值都表示 Syntax.node,则当两者具有相同的语法类型、相同的子项数并且每个子模式与相应的子值匹配时,存在匹配。

标识符

如果匹配的模式和值都是标识符,则比较它们的文字 Name 值是否相等模宏范围。 “看起来”相同的标识符匹配,并且它们是否引用相同的绑定并不重要。 此设计选择允许在无法访问可通过引用比较名称的编译时环境的上下文中使用引用模式匹配。

由于引用模式匹配基于解析器发出的节点类型,因此如果来自不同语法类别,看起来相同的引用可能不匹配。 如果有疑问,在引用中包含语法类别会有所帮助。

由语法模式匹配绑定的变量的类型为 TSyntax k,其中 k 描述潜在的语法类型。 重复中的变量的类型为 TSyntaxArray k,如果重复使用字符串 sep 分隔,则为 TSepArray k sep 类型。 TSyntax有关类型化语法的部分中进行了更详细的描述。

Syntax Pattern Matching

列表推导式是一种用于编写列表的表示法,其灵感来自于标准集生成器表示法。 列表推导式由方括号组成,方括号包含结果项,后跟一些限定符;每个限定符要么从其他列表中引入一个变量,要么强加一个必须满足的条件。 限定符是嵌套的:每个新变量的值都会针对每个先前的值进行评估。

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

列表推导式可以脱糖为对 List.flatMap 的一系列调用。 变量引入将转换为变量值表达式上的 flatMap,而谓词将转换为条件,如果谓词为 true 或 false,则返回 1 或 0 值。 最终 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, which is an abbreviation for 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 命令🔗

syntaxRule-Based Macros With macro_rules

宏中的模式必须是引用模式。 它们可以匹配任何语法类别的语法,但给定的模式只能匹配单一语法类型。 如果没有为引用指定类别或解析器,则它可能匹配术语或命令(序列),但绝不会两者都匹配。 为了避免歧义,选择术语“解析器”。

在内部,宏在一个表中进行跟踪,该表将每个 语法类型 映射到其宏。 Lean.Parser.Command.macro_rules : commandmacro_rules命令可以用语法类型明确地注释。

如果显式提供了语法类型,则宏定义会检查每个引用模式是否具有该类型。 如果引用的解析结果是 选择节点(即,如果解析不明确),则对于具有指定类型的每个替代项,模式都会重复一次。 如果没有一个替代方案具有指定的类型,则这是一个错误。

如果没有明确提供种类,则解析器确定的种类将用于每个模式。 这些模式不需要全部具有相同的语法类型;宏是为至少一种模式使用的每种语法类型定义的。 如果引用模式的解析结果是 选择节点(即,如果解析不明确),则这是一个错误。

如果语法本身没有文档注释,则会向用户显示与 Lean.Parser.Command.macro_rules : commandmacro_rules 关联的文档注释。 否则,将显示语法本身的文档注释。

符号运算符 一样,宏规则可以声明为 scopedlocal。 作用域宏仅在当前命名空间打开时才有效,本地宏规则仅在当前 节范围 中有效。

Idiom Brackets

习语括号是使用应用函子的另一种语法。 如果习语括号包含函数应用程序,则该函数将包装在 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
Scoped Macros

作用域宏规则仅在其命名空间中有效。 当命名空间 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 命令中,则将尝试它们。

One vs. Two Sets of Macro 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.notation : commandnotation 只能定义新术语语法,并且其中扩展是要替换参数的术语,Lean.Parser.Command.macro : commandmacro 命令可以在任何 语法类别 中定义语法,并且可以使用 MacroM monad 中的任意代码来生成扩展。 由于宏比符号灵活得多,Lean 无法自动生成解扩展器;这意味着通过 Lean.Parser.Command.macro : commandmacro 命令实现的新语法可用于 Lean 的输入,但 Lean 的输出在没有进一步工作的情况下不会使用它。

syntaxMacro Arguments

宏的参数可以是语法项(如 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 属性将 手动添加到语法类型中。 这种指定宏的低级方法通常没有用处,除非宏本身生成宏定义的代码生成结果。

attributeThe macro Attribute

Lean.Parser.Attr.macro : attrmacro 属性指定将函数视为指定语法类型的

attr ::= ...
    | macro ident
The Macro Attribute
/-- Generate a list based on N syntactic copies of a term -/ 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"]