Lean 语言参考手册

23.4. 定义新语法🔗

Lean 对语法的统一表示非常一般且灵活。 这意味着,对 Lean 解析器的扩展并不需要同时扩展已解析语法的表示方式。

23.4.1. 语法模型🔗

Lean 的解析器会产生一棵具体语法树,其类型为 Lean.SyntaxLean.Syntax 是一个归纳类型,用来表示 Lean 的全部语法,包括命令、项、策略以及任何自定义扩展。 所有这些都由少数几种基本构件来表示:

原子

原子是语法中的基本终结符,包括字面量(例如字符和数字的字面量)、括号、运算符和关键字。

标识符

标识符表示名字,例如 xNatNat.add。 标识符语法中包含一个预解析名称列表,记录该标识符可能指向哪些名字。

节点

节点表示对非终结符的解析结果。 节点包含一个 语法种类,用于标识该节点来自哪条语法规则;它还包含一个由子 Syntax 值组成的数组。

缺失语法

当解析器遇到错误时,它会返回部分结果,这样 Lean 就能对尚未写完的程序或包含错误的程序提供一些反馈。 部分结果中会包含一个或多个缺失语法的位置。

原子与标识符统称为 记号

🔗归纳类型

Lean 的语法树。

语法树在 Lean 中无处不在:解析器产生语法树,宏展开器变换语法树,精译器再精译 语法树。反精译器也会产生语法树,并把它们呈现给用户。

Lean.Syntax.missing : Lean.Syntax

因解析错误而缺失的一段语法。

Syntax 使用索引运算符时,若索引越界也会返回 Syntax.missing

Lean.Syntax.node (info : Lean.SourceInfo)
  (kind : Lean.SyntaxNodeKind) (args : Array Lean.Syntax) :
  Lean.Syntax

语法树中可以含有更多子语法的节点;kind 决定节点的解释。

解析器产生的节点通常令 infoLean.SourceInfo.none,源信息保存在相应标识符 和原子的字段中。该字段有两种用途:

  1. 反精译器用它把节点与实现交互功能所用的元数据关联起来。

  2. 引用创建的节点用该字段把语法标记为合成语法(存储 Lean.SourceInfo.fromRef 的结果),即使其首尾记号本身不是合成的。

Lean.Syntax.atom (info : Lean.SourceInfo) (val : String) :
  Lean.Syntax

语法中不是标识符的原子组成部分。

以下各项都是原子:

  • 关键字,如 deffuninductive

  • 字面量,如数字字面量或字符串字面量

  • 标点和分隔符,如 ()=>

标识符由 Lean.Syntax.ident 构造子表示。原子也对应 syntax 声明中的引号字符串。

Lean.Syntax.ident (info : Lean.SourceInfo)
  (rawVal : Substring.Raw) (val : Lean.Name)
  (preresolved : List Lean.Syntax.Preresolved) : Lean.Syntax

标识符。

除源信息外,标识符还具有以下字段:

  • rawVal 是输入文件中的字面子串

  • val 是解析后的 Lean 名称,可能包含宏作用域。

  • preresolved 是它可能指向的声明列表,由引用填充。

🔗归纳类型

标识符在被引用位置的上下文中可能指向的绑定。

引用中的标识符既可能指向全局声明,也可能指向引用处作用域内的命名空间。这些信息 保存在 Syntax.ident 构造子中,是卫生宏实现的一部分。

Lean.Syntax.Preresolved.namespace (ns : Lean.Name) :
  Lean.Syntax.Preresolved

一个可能的命名空间引用。

Lean.Syntax.Preresolved.decl (n : Lean.Name)
  (fields : List String) : Lean.Syntax.Preresolved

一个可能的全局常量或节变量引用,并带有后续字段访问。

23.4.2. 语法节点种类🔗

语法节点种类通常用来标识产生该节点的解析器。 运算符或记法被赋予的名称(以及它们自动生成的内部名称)就会出现在这里。 虽然只有节点本身包含标识其种类的字段,但按照约定,标识符的种类是 identKind,而原子的种类则按照约定就是它们内部保存的字符串。 Lean 的解析器会把每个关键字原子 KW 包装进一个单元素节点,其种类为 `token.KW。 语法值的种类可以通过 Syntax.getKind 提取出来。

🔗定义

指定 Syntax.node 值的解释;它是 Name 的缩写。

节点种类可以是任意名称,不必指向环境中的声明。不过按照约定,节点种类通常对应于 生成它的 ParserParserDesc 声明。解析基础设施还使用若干不对应解析器声明的 内建种类,例如 nullKindchoiceKind

🔗定义

检查语法是否具有给定的种类或伪种类。

“伪种类”是按约定赋予非 Syntax.node 值的种类:Syntax.ident 使用 identKindSyntax.missing 使用 `missing,原子使用其字符串字面量。

🔗定义

取得 Syntax.node 的种类,或其他 Syntax 值的伪种类。

“伪种类”是按约定赋予非 Syntax.node 值的种类:Syntax.ident 使用 identKindSyntax.missing 使用 `missing,原子使用其字符串字面量。

🔗定义

Syntax.node 根部的种类改为 k

所有其他 Syntax 值均原样返回。

23.4.3. 记号与字面量种类🔗

解析器生成的基本记号都关联着若干具名种类。 通常,单记号语法产生式由一个包含单个 atomnode 构成;保存在节点中的种类使得这个值能够被识别。 解析器不会解释字面量原子:字符串原子会连同前后的双引号以及其中包含的任何转义序列一起保存,而十六进制数字则会被保存为一个以 "0x" 开头的字符串。 提供了 辅助函数(例如 Lean.TSyntax.getString)来按需执行这些解码操作。

🔗定义

赋予标识符的伪种类:`ident

名称 `ident 实际上并不用作 Syntax.node 值的种类;按约定,它用作 Syntax.ident 值的种类。

🔗定义

`str 是字符串字面量(如 "foo")的节点种类。

🔗定义

`interpolatedStrKinds!"value = {x}""value = {x}" 这类插值字符串 字面量的节点种类。

🔗定义

`interpolatedStrLitKinds!"value = {x}""value = {}" 这类插值 字符串字面量片段的节点种类。

🔗定义

`char 是字符字面量(如 'A')的节点种类。

🔗定义

`num 是数字字面量(如 420xa1)的节点种类。

🔗定义

`scientific 是科学计数法浮点字面量(如 1.23e-3)的节点种类。

🔗定义

`name 是名称字面量(如 `foo)的节点种类。

🔗定义

`fieldIdx 是投影索引(如 x.2 中的 2)的节点种类。

23.4.4. 内部种类🔗

🔗定义

`group 用于 Lean.Parser.group 产生的节点,避免其在 optional 内与空种类混淆。

🔗定义

`null 是没有其他种类适用时的后备种类。重复运算符会产生空节点,而空的空节点 表示可选解析失败。

many 等原始列表解析器使用空种类。

🔗定义

`choice 种类用于表示有歧义的解析结果。

解析器优先选择更长的匹配,但最长匹配并不总是唯一的。所有解析结果都会保存起来, 直到有类型信息时再决定使用哪一个。

🔗定义

`hygieneInfoLean.Parser.hygieneInfo 的节点种类。该解析器不消耗输入,却产生 捕获当前位置卫生信息的“不可见记号”。

它们可通过 Lean.HygieneInfo.mkIdent 生成仿佛由宏输入而非宏实现引入的标识符。

23.4.5. 源位置🔗

原子、标识符和节点可以选择性地包含 源信息,用来跟踪它们与原始文件的对应关系。 解析器会为所有记号保存源信息,但不会为节点保存;已解析节点的位置信息是由其首尾记号重建出来的。 并非所有 Syntax 数据都来自解析器:它也可能是 宏展开 的结果,这时它通常同时混有生成出来的语法和解析得到的语法;或者也可能是对内部项进行 反精译 以展示给用户的结果。 在这些使用场景中,节点自身也可能包含源信息。

源信息分为两种:

原始

原始源信息来自解析器。 除了原始源位置之外,它还包含被解析器跳过的前导和尾随空白,因此原始字符串可以被重建出来。 为了避免分配子串副本,这些空白会保存为原始源码字符串表示中的偏移量(也就是 Substring)。

合成

合成源信息来自元程序(包括宏)或 Lean 内部。 由于没有需要重建的原始字符串,因此它不会保存前导和尾随空白。 合成源位置用于在项被自动转换后依然提供准确反馈,也用于跟踪精译后表达式与其在 Lean 输出中的呈现之间的对应关系。 合成位置可以被标记为 规范;在这种情况下,一些通常会忽略合成位置的操作会把它当作非合成位置来处理。

🔗归纳类型

把语法与其来源上下文关联起来的源信息。

SourceInfo 的主要用途是把解析器和宏展开器的输出关联到原始源文件。解析器产生的 Syntax.node 通常不携带源信息;解析器只把源信息附在原子和标识符上。引用产生的 Syntax.node 则带有合成源信息,既把它关联到一个原始参考位置,也表明其中的原始 原子可能并非来自当前正在精译的 Lean 文件。

源信息还用于把 Lean 的输出关联到其所表示的内部数据,这是许多交互功能的基础。 在这种用途下,Syntax.node 也可以携带源信息。

Lean.SourceInfo.original (leading : Substring.Raw)
  (pos : String.Pos.Raw) (trailing : Substring.Raw)
  (endPos : String.Pos.Raw) : Lean.SourceInfo

解析器从原始输入产生的记号;除位置信息外,还包含前导和尾随空白。

leading 前导空白是在解析完成后由 Syntax.updateLeading 推断的,因为解析过程中, 尤其存在回溯时,“前一个记号”并没有良好定义。

Lean.SourceInfo.synthetic (pos endPos : String.Pos.Raw)
  (canonical : Bool := false) : Lean.SourceInfo

合成语法是由元程序或 Lean 自身(例如引用)产生的语法。它带有来自原始语法的 源码范围,以便与源文件关联。

反精译器也用此构造子编码产生该语法的核心语言表达式。

合成语法的 canonical 标志用于这样的语法:它并非原始输入的字面组成部分,但在 悬停信息和错误消息中应被视作“仿佛由用户写下”。它通常用于在宏展开改变标识符 名称后仍把绑定位置连接到用户的原始语法,也用于应接收定点消息的记号。

一般而言,宏展开应只在一个规范记号中使用某一片输入语法;一个例外是同一标识符 被用来声明两个绑定器,例如依赖 if 的宏展开:

`(if $h : $cond then $t else $e) ~>
`(dite $cond (fun $h => $t) (fun $h => $t))

在这些情况下,用户悬停在 h 上会看到两个绑定位置的信息。

Lean.SourceInfo.none : Lean.SourceInfo

没有位置信息的合成记号。

23.4.6. 检查语法🔗

检查 Syntax 值主要有三种方式:

Repr 实例

Repr Syntax 实例会用 Syntax 类型的各个构造子给出非常详细的语法表示。

ToString 实例

ToString Syntax 实例会生成一种紧凑视图,用特定约定来表示某些语法种类,从而更便于快速阅读。 这个实例会省略源位置信息。

美化器

Lean 的美化器会尝试把语法渲染成它在源文件中的样子;但如果语法的嵌套结构与预期形状不符,它就会失败。

将语法表示为构造子

可以在 Lean.Parser.Command.eval : command#eval 的上下文中对语法进行引用,从而查看 Repr 实例对它的表示;后者能在命令精译单子 CommandElabM 中运行动作。 为了减小示例输出的体积,这里使用辅助函数 removeSourceInfo 在显示前移除源信息。

partial def removeSourceInfo : Syntax Syntax | .atom _ str => .atom .none str | .ident _ str x pre => .ident .none str x pre | .node _ k children => .node .none k (children.map removeSourceInfo) | .missing => .missing Lean.Syntax.node (Lean.SourceInfo.none) `«term_+_» #[Lean.Syntax.node (Lean.SourceInfo.none) `num #[Lean.Syntax.atom (Lean.SourceInfo.none) "2"], Lean.Syntax.atom (Lean.SourceInfo.none) "+", Lean.Syntax.missing]#eval do let stx `(2 + $(.missing)) logInfo (repr (removeSourceInfo stx.raw))
Lean.Syntax.node
  (Lean.SourceInfo.none)
  `«term_+_»
  #[Lean.Syntax.node (Lean.SourceInfo.none) `num #[Lean.Syntax.atom (Lean.SourceInfo.none) "2"],
    Lean.Syntax.atom (Lean.SourceInfo.none) "+", Lean.Syntax.missing]

在第二个示例中,由引用插入的 宏作用域 可以在对 List.length 的调用上看到。

Lean.Syntax.node (Lean.SourceInfo.none) `Lean.Parser.Term.app #[Lean.Syntax.ident (Lean.SourceInfo.none) "List.length".toRawSubstring (Lean.Name.mkNum (Lean.Name.mkStr (Lean.Name.mkStr (Lean.Name.mkNum `List.length.«_@».Manual.NotationsMacros.SyntaxDef 1704743902) "_hygCtx") "_hyg") 2) [Lean.Syntax.Preresolved.decl `List.length []], Lean.Syntax.node (Lean.SourceInfo.none) `null #[Lean.Syntax.node (Lean.SourceInfo.none) `«term[_]» #[Lean.Syntax.atom (Lean.SourceInfo.none) "[", Lean.Syntax.node (Lean.SourceInfo.none) `null #[Lean.Syntax.node (Lean.SourceInfo.none) `str #[Lean.Syntax.atom (Lean.SourceInfo.none) "\"Rose\""], Lean.Syntax.atom (Lean.SourceInfo.none) ",", Lean.Syntax.node (Lean.SourceInfo.none) `str #[Lean.Syntax.atom (Lean.SourceInfo.none) "\"Daffodil\""], Lean.Syntax.atom (Lean.SourceInfo.none) ",", Lean.Syntax.node (Lean.SourceInfo.none) `str #[Lean.Syntax.atom (Lean.SourceInfo.none) "\"Lily\""]], Lean.Syntax.atom (Lean.SourceInfo.none) "]"]]]#eval do let stx `(List.length ["Rose", "Daffodil", "Lily"]) logInfo (repr (removeSourceInfo stx.raw))

这里可以看到 预解析标识符 List.length 的内容:

Lean.Syntax.node
  (Lean.SourceInfo.none)
  `Lean.Parser.Term.app
  #[Lean.Syntax.ident
      (Lean.SourceInfo.none)
      "List.length".toRawSubstring
      (Lean.Name.mkNum (Lean.Name.mkStr (Lean.Name.mkStr (Lean.Name.mkNum `List.length.«_@».Manual.NotationsMacros.SyntaxDef 1704743902) "_hygCtx") "_hyg") 2)
      [Lean.Syntax.Preresolved.decl `List.length []],
    Lean.Syntax.node
      (Lean.SourceInfo.none)
      `null
      #[Lean.Syntax.node
          (Lean.SourceInfo.none)
          `«term[_]»
          #[Lean.Syntax.atom (Lean.SourceInfo.none) "[",
            Lean.Syntax.node
              (Lean.SourceInfo.none)
              `null
              #[Lean.Syntax.node (Lean.SourceInfo.none) `str #[Lean.Syntax.atom (Lean.SourceInfo.none) "\"Rose\""],
                Lean.Syntax.atom (Lean.SourceInfo.none) ",",
                Lean.Syntax.node (Lean.SourceInfo.none) `str #[Lean.Syntax.atom (Lean.SourceInfo.none) "\"Daffodil\""],
                Lean.Syntax.atom (Lean.SourceInfo.none) ",",
                Lean.Syntax.node (Lean.SourceInfo.none) `str #[Lean.Syntax.atom (Lean.SourceInfo.none) "\"Lily\""]],
            Lean.Syntax.atom (Lean.SourceInfo.none) "]"]]]

ToString 实例对 Syntax 各构造子的表示如下:

  • ident 构造子会被表示为其底层名称。源信息和预解析名称不会显示。

  • atom 构造子会被表示为字符串。

  • missing 构造子会被表示为 <missing>

  • node 构造子的表示取决于它的种类。 如果种类是 `null,那么该节点会按其子节点顺序用方括号表示。 否则,该节点会表示为其种类,后跟子节点,两者都包在圆括号中。

将语法表示为字符串

可以在 Lean.Parser.Command.eval : command#eval 的上下文中对语法进行引用,从而查看其字符串表示;后者能在命令精译单子 CommandElabM 中运行动作。

(«term_+_» (num "2") "+" <missing>)#eval do let stx `(2 + $(.missing)) logInfo (toString stx)
(«term_+_» (num "2") "+" <missing>)

在第二个示例中,由引用插入的 宏作用域 可以在对 List.length 的调用上看到。

(Term.app `List.length._@.Manual.NotationsMacros.SyntaxDef.3168789510._hygCtx._hyg.2 [(«term[_]» "[" [(str "\"Rose\"") "," (str "\"Daffodil\"") "," (str "\"Lily\"")] "]")])#eval do let stx `(List.length ["Rose", "Daffodil", "Lily"]) logInfo (toString stx)
(Term.app
 `List.length._@.Manual.NotationsMacros.SyntaxDef.3168789510._hygCtx._hyg.2
 [(«term[_]» "[" [(str "\"Rose\"") "," (str "\"Daffodil\"") "," (str "\"Lily\"")] "]")])

把语法做美化打印,通常在需要把它包含进面向用户的消息时最有用。 通常,Lean 会在需要时自动调用美化器。 不过,如果有需要,也可以显式调用 ppTerm

美化打印后的语法

可以在 Lean.Parser.Command.eval : command#eval 的上下文中对语法进行引用,从而查看它的字符串表示;后者能在命令精译单子 CommandElabM 中运行动作。 由于新的语法声明也会给美化器提供如何显示它们的说明,因此美化器需要一个配置对象。 这个上下文可以用一个辅助函数来构造:

def getPPContext : CommandElabM PPContext := do return { env := ( getEnv), opts := ( getOptions), currNamespace := ( getCurrNamespace), openDecls := ( getOpenDecls) } 2 + 5#eval show CommandElabM Unit from do let stx `(2 + 5) let fmt ppTerm ( getPPContext) stx logInfo fmt
2 + 5

在第二个示例中,由引用插入到 List.length 上的 宏作用域 会让它显示成带匕首符号()的形式。

List.length✝ ["Rose", "Daffodil", "Lily"]#eval do let stx `(List.length ["Rose", "Daffodil", "Lily"]) let fmt ppTerm ( getPPContext) stx logInfo fmt
List.length✝ ["Rose", "Daffodil", "Lily"]

美化打印会自动换行并插入缩进。 通常会有一个 强制转换 把美化器的输出转为 logInfo 所期望的类型,并使用默认的布局宽度。 如果显式调用 pretty 并传入具名参数,就可以控制这个宽度。

List.length✝ ["Rose", "Daffodil", "Lily", "Rose", "Daffodil", "Lily", "Rose", "Daffodil", "Lily"]#eval do let flowers := #["Rose", "Daffodil", "Lily"] let manyFlowers := flowers ++ flowers ++ flowers let stx `(List.length [$(manyFlowers.map (quote (k := `term))),*]) let fmt ppTerm ( getPPContext) stx logInfo (fmt.pretty (width := 40))
List.length✝
  ["Rose", "Daffodil", "Lily", "Rose",
    "Daffodil", "Lily", "Rose",
    "Daffodil", "Lily"]

23.4.7. 带类型的语法🔗

语法还可以额外带上一个类型注解,用来指明它属于哪个 语法类别TSyntax 结构包含一个类型层面的语法类别列表,以及一棵语法树。 这个语法类别列表通常恰好只包含一个元素;在这种情况下,列表结构本身不会显示出来。

🔗结构体

带类型语法;它跟踪其中 Syntax 可能具有的种类。

语法引用会产生或要求种类正确的 TSyntax,但除此之外并无强制保证;直接使用构造子 即可轻易绕过这一约束。

Lean.TSyntax.mk
raw : Lean.Syntax

底层的 Syntax 值。

🔗定义

SyntaxNodeKinds 是用列表实现的 SyntaxNodeKind 集合。

单元素 SyntaxNodeKinds 极为常见,可直接写成名称字面量而不是列表;只有空集合或 多元素种类集合才需要列表语法。

准引用 会阻止替换那些并非来自正确语法类别的带类型语法。 对于 Lean 的许多内建语法类别,都有一组 强制转换,可以把某一类语法适当地包装成另一类别的语法,例如从字符串字面量语法到项语法的强制转换。 此外,许多只对某些语法类别有效的辅助函数,也只会为相应的带类型语法定义。

TSyntax 的构造子是公开的,因此并没有机制阻止用户构造出破坏内部不变量的值。 使用 TSyntax 应被视为减少常见错误的一种方式,而不是彻底杜绝错误。

除了 TSyntax 之外,还有一些类型表示语法数组,既有带分隔符的,也有不带分隔符的。 这些对应于语法声明或反引用中的 重复元素。 TSyntaxArray ksArray (TSyntax ks) 的一个 缩写,而 TSepArray ks sep 是一个结构;这意味着可以用 广义字段记法 将数组函数应用于 TSyntaxArray,但不能应用于 TSepArrayTSepArray ksTSyntaxArray ks 之间既有 强制转换,也有显式转换函数。 这种转换会在底层数组中插入或移除分隔符元素,其耗时与元素个数成线性关系。

🔗定义

某一种类集合 ks 的带类型语法数组。

🔗不透明定义

不重新分配内存,把 TSyntaxArray 转换成 Array Syntax

🔗结构体

由给定分隔符交错分隔的带类型语法数组;每个语法元素的种类来自 ks

分隔数组由 ,* 等重复运算符产生。强制转换Array (TSyntax ks) 或从中转换时,会按需插入或移除分隔符。无类型版本是 Lean.Syntax.SepArray

elemsAndSeps : Array Lean.Syntax

元素与分隔符按 #[el1, sep1, el2, sep2, el3] 的顺序排列。

🔗定义

从分隔数组中提取所有非分隔符元素。

🔗定义
Lean.Syntax.TSepArray.elemsAndSeps {ks : Lean.SyntaxNodeKinds} {sep : String} (self : Lean.Syntax.TSepArray ks sep) : Array Lean.Syntax
Lean.Syntax.TSepArray.elemsAndSeps {ks : Lean.SyntaxNodeKinds} {sep : String} (self : Lean.Syntax.TSepArray ks sep) : Array Lean.Syntax

元素与分隔符按 #[el1, sep1, el2, sep2, el3] 的顺序排列。

🔗定义

从元素构造带类型分隔数组,并添加适当的分隔符。所提供的数组不应包含分隔符。

类似于 Syntax.SepArray.ofElems,但用于带类型语法。

🔗定义

在分隔数组末尾添加元素,并在需要时添加分隔符。

23.4.8. 别名🔗

为常用的带类型语法形式提供了若干别名。 这些别名使代码可以在更高的抽象层次上书写。

🔗定义

表示 Lean 项的语法。

🔗定义

表示命令的语法。

🔗定义

表示宇宙层级的语法。

🔗定义

表示策略的语法。

🔗定义

表示优先级(例如运算符优先级)的语法。

🔗定义

表示优先权(例如实例声明优先权)的语法。

🔗定义

表示标识符的语法。

🔗定义

表示字符串字面量的语法。

🔗定义

表示字符字面量的语法。

🔗定义

表示以反引号开头的名称字面量的语法。

🔗定义

表示数字字面量的语法。

🔗定义

表示可含小数部分和指数部分的科学计数法数字字面量的语法。

🔗定义

表示宏卫生信息的语法。

23.4.9. 构造语法的辅助函数🔗

🔗定义
Lean.mkIdent (val : Lean.Name) : Lean.Ident
Lean.mkIdent (val : Lean.Name) : Lean.Ident

从名称创建标识符。所得标识符没有源位置。

🔗定义
Lean.mkIdentFrom (src : Lean.Syntax) (val : Lean.Name) (canonical : Bool := false) : Lean.Ident
Lean.mkIdentFrom (src : Lean.Syntax) (val : Lean.Name) (canonical : Bool := false) : Lean.Ident

创建标识符,其位置从 src 复制。

若要无变量捕获风险地指向特定常量,请改用 mkCIdentFrom

🔗定义
Lean.mkIdentFromRef {m : Type Type} [Monad m] [Lean.MonadRef m] (val : Lean.Name) (canonical : Bool := false) : m Lean.Ident
Lean.mkIdentFromRef {m : Type Type} [Monad m] [Lean.MonadRef m] (val : Lean.Name) (canonical : Bool := false) : m Lean.Ident

创建标识符,其位置从 getRef 返回的语法复制。

若要无变量捕获风险地指向特定常量,请改用 mkCIdentFromRef

🔗定义
Lean.mkCIdent (c : Lean.Name) : Lean.Ident
Lean.mkCIdent (c : Lean.Name) : Lean.Ident

创建指向常量 c 的标识符。该标识符没有源位置。

mkIdent 变体确保标识符不会意外被捕获。

🔗定义
Lean.mkCIdentFrom (src : Lean.Syntax) (c : Lean.Name) (canonical : Bool := false) : Lean.Ident
Lean.mkCIdentFrom (src : Lean.Syntax) (c : Lean.Name) (canonical : Bool := false) : Lean.Ident

创建指向常量 c 的标识符。该标识符的位置从 src 复制。

mkIdentFrom 变体确保标识符不会意外被捕获。

🔗定义
Lean.mkCIdentFromRef {m : Type Type} [Monad m] [Lean.MonadRef m] (c : Lean.Name) (canonical : Bool := false) : m Lean.Syntax
Lean.mkCIdentFromRef {m : Type Type} [Monad m] [Lean.MonadRef m] (c : Lean.Name) (canonical : Bool := false) : m Lean.Syntax

创建指向常量 c 的标识符。该标识符的位置从 getRef 返回的语法复制。

mkIdentFrom 变体确保标识符不会意外被捕获。

🔗定义

创建表示 Lean 项应用的语法,同时避免产生退化的空应用。

🔗定义
Lean.Syntax.mkCApp (fn : Lean.Name) (args : Lean.TSyntaxArray `term) : Lean.Term
Lean.Syntax.mkCApp (fn : Lean.Name) (args : Lean.TSyntaxArray `term) : Lean.Term

创建表示 Lean 常量应用的语法,同时避免产生退化的空应用。

🔗定义

创建给定种类的字面量。调用者负责确保所提供的字面量是该种类的合法原子。

若提供 info,则从中复制字面量的源信息。

🔗定义

为给定字符创建字面量语法。

若提供 info,则从中复制字面量的源信息。

🔗定义

为给定字符串创建字面量语法。

若提供 info,则从中复制字面量的源信息。

🔗定义

为以字符串提供的数字创建字面量语法。调用者必须确保该字符串是 num 记号解析器的 合法记号。

若提供 info,则从中复制字面量的源信息。

🔗定义

为自然数创建字面量语法。

若提供 info,则从中复制字面量的源信息。

🔗定义

为科学计数法数字创建字面量语法。调用者必须确保所提供的字符串是合法的科学计数法 字面量。

若提供 info,则从中复制字面量的源信息。

🔗定义

为名称创建字面量语法。调用者必须确保所提供的字符串是合法的名称字面量。

若提供 info,则从中复制字面量的源信息。

🔗定义

创建可选节点。

可选节点是包含零个或一个元素的空种类节点。

🔗定义

创建仿佛由 Lean.Parser.group 解析得到的分组节点。

🔗定义
Lean.mkHole (ref : Lean.Syntax) (canonical : Bool := false) : Lean.Term
Lean.mkHole (ref : Lean.Syntax) (canonical : Bool := false) : Lean.Term

创建空洞(_),并从 ref 复制空洞的位置。

23.4.9.1. 引用数据🔗

Quote 类型类允许把值转换成表示它们的带类型语法。 例如,quote 5 表示 .node .none `num #[.atom .none "5"]。 这个类型类按语法种类参数化;这使得同一个值可以在不同种类下得到合适的表示。 Quote 的实例解析会将带类型语法的 强制转换 考虑在内。 语法种类的默认值是 `term

Quote.quote 的结果并不保证一定能够成功精译。 一般来说,生成的语法会包含所有显式参数的引用形式,而省略隐式参数。

🔗类型类
Lean.Quote (α : Type) (k : Lean.SyntaxNodeKind := `term) : Type
Lean.Quote (α : Type) (k : Lean.SyntaxNodeKind := `term) : Type

把运行时值转换为表示该值的表层语法。

实例不必保证结果语法总能重新精译为等价值;例如,语法可以省略通常能够自动找到的 隐式实参。

Lean.Quote.mk
quote : α  Lean.TSyntax k

返回给定值的语法。

定义 Quote 的实例时,应使用 mkCIdentmkCApp,以避免生成的语法中发生变量捕获。

定义 Quote 实例

为了引用一个类型为 Tree 的树,这里使用 mkCIdentmkCApp 来确保名字相近的局部绑定不会造成干扰。 使用双反引号可以确保构造子名称没有拼写错误,并且能够被正确解析。

inductive Tree (α : Type u) : Type u where | leaf | branch (left : Tree α) (val : α) (right : Tree α) instance [Quote α] : Quote (Tree α) where quote := quoteTree where quoteTree | .leaf => mkCIdent ``Tree.leaf | .branch l v r => mkCApp ``Tree.branch #[quoteTree l, quote v, quoteTree r]

23.4.10. 解码带类型语法🔗

对于字面量,Lean 的解析器会生成一个只包含单个 atom 的节点。 内部原子保存着带源信息的字符串,而节点的种类则指定了应如何解释该原子。 这可能涉及解码字符串转义序列,或解释十六进制数字字面量。 本节中的辅助函数会执行正确的解释。

🔗定义
Lean.TSyntax.getId (s : Lean.Ident) : Lean.Name
Lean.TSyntax.getId (s : Lean.Ident) : Lean.Name

从标识符语法中提取解析后的名称。

若语法畸形,则返回 Name.anonymous

🔗定义

解码带反引号的名称字面量并返回名称。

若语法畸形,则返回 Lean.Name.anonymous

🔗定义

把数字字面量解释为自然数。

若语法畸形,则返回 0

🔗定义

提取科学计数法数字字面量的组成部分。

返回三元组 (n, sign, e) : Nat × Bool × Nat;该数字的值为:

if sign then n * 10 ^ (-e) else n * 10 ^ e

若语法畸形,则返回 (0, false, 0)

🔗定义

解码字符串字面量,去掉引号并反转义转义字符。

若语法畸形,则返回 ""

🔗定义

解码字符字面量。

若语法畸形,则返回 (default : Char)

🔗定义

解码宏卫生信息。

23.4.11. 语法类别🔗

Lean 的解析器中包含一张 语法类别 表,它们对应于上下文无关文法中的非终结符。 其中一些最重要的类别包括项、命令、宇宙层级、优先权、优先级,以及表示字面量等记号的那些类别。 通常,每个 语法种类 都对应一个类别。 可以使用 Lean.Parser.Command.syntaxCat : commanddeclare_syntax_cat 来声明新类别。

语法声明语法类别

声明一个新的语法类别。

command ::= ...
    | docComment?
      declare_syntax_cat ident ((behavior := (catBehaviorBoth | catBehaviorSymbol)))?

前导标识符行为是一项高级特性,通常不需要修改。 它控制解析器在遇到标识符时的行为,有时会让该标识符被当作一个非保留关键字处理。 这用于避免把每个 策略 的名字都变成保留关键字。

🔗归纳类型

指定解析表查询函数在遇到标识符时的行为。

Lean.Parser.prattParser 分别用一张表保存前导解析器和尾随解析器;表把记号映射到解析器。 关键字记号与标识符记号不同,故即使拼写相同也不会混淆。替代的前导标识符行为提供了 更大的灵活性,使某些场景可以避免保留关键字。

当前导记号在语法上是标识符时,当前语法类别的 LeadingIdentBehavior 控制解析表查询, 允许在标识符和关键字之间进行受控的双关。这用于避免为每个内建策略(如 applyassumption)都创建保留符号,从而让策略名称仍可用作标识符。

Lean.Parser.LeadingIdentBehavior.default :
  Lean.Parser.LeadingIdentBehavior

若前导记号是标识符,只运行与辅助记号“ident”关联的标识符解析器。

Lean.Parser.LeadingIdentBehavior.symbol :
  Lean.Parser.LeadingIdentBehavior

若前导标识符为 <foo> 且记号 <foo> 关联了解析器 P,则运行 P;否则只运行 与辅助记号“ident”关联的标识符解析器。

Lean.Parser.LeadingIdentBehavior.both :
  Lean.Parser.LeadingIdentBehavior

若前导记号是标识符 <foo>,同时运行与 <foo> 关联的解析器以及与辅助记号 “ident”关联的标识符解析器。

23.4.12. 语法规则🔗

每个 语法类别 都关联着一组 语法规则,它们对应于上下文无关文法中的产生式。 语法规则可以使用 Lean.Parser.Command.syntax : commandsyntax 命令来定义。

语法语法规则
command ::= ...
    | docComment?
      attributes?
      `attrKind` 匹配 `("scoped" <|> "local")?`,用于属性之前,例如 `@[local simp]`。attrKind
      syntax(:prec)? ((name := ident))? ((priority := prio))? stx* : ident

与运算符和记法声明一样,文档注释的内容会在用户与新语法交互时显示给他们。 还可以添加属性,以便在生成的定义上调用编译期元程序。

语法规则与 节作用域 的交互方式和属性、运算符、记法相同。 默认情况下,语法规则在任何传递导入了其定义所在模块的模块中都可供解析器使用;但也可以将其声明为 scopedlocal,分别把可用范围限制为当前命名空间已被打开的上下文,或者当前的 节作用域

当某个类别的多条语法规则都能匹配当前输入时,会使用 局部最长匹配规则 来从中选择一条。 与记法和运算符一样,如果最长匹配并列,就使用声明的优先权来决定采用哪个解析结果。 如果这样仍不能消除歧义,那么所有并列结果都会被保留下来。 精译器预计会尝试它们全部;当且仅当恰好有一个能够成功精译时,整体才算成功。

语法规则的优先级紧跟在 Lean.Parser.Command.syntax : commandsyntax 关键字之后,它会限制解析器:只有当前优先级上下文至少达到所给值时,才使用这条新语法。 与运算符和记法一样,语法规则也可以手动指定名字;如果没有指定,就会生成一个原本未使用的名字。 无论是手动提供还是自动生成,这个名字都会作为生成的 node 的语法种类。

语法声明的主体比记法的主体更加灵活。 字符串字面量指定要匹配的原子。 子项可以来自任意语法类别,而不只是项;它们还可以是可选的,或可重复的,并且可以带或不带逗号分隔符。 语法规则中的标识符表示语法类别,而不像在记法中那样为子项命名。

最后,语法规则还要指明它扩展的是哪个语法类别。 在不存在的类别中声明语法规则会报错。

语法语法说明符

语法类别 stx 是可出现在 Lean.Parser.Command.syntax : commandsyntax 命令主体中的说明符语法。

字符串字面量会被解析为 原子(包括 if#evalwhere 等关键字):

stx ::=
    str

字符串中的前导和尾随空格不会影响解析,但在 Lean 的 证明状态 和错误消息中显示该语法时,它们会使 Lean 在相应位置插入空格。 通常,在语法规则中作为原子出现的合法标识符会变成保留关键字。 如果在字符串字面量前加上一个和号(&),就会抑制这一行为:

stx ::= ...
    | &str

标识符指定给定位置上期望的语法类别,并且可以选择性地提供一个优先级:

stx ::= ...
    | ident(:prec)?

* 修饰符是克林星号,用于匹配前述语法的零次或多次重复。 它也可以写作 many

stx ::= ...
    | stx *

+ 修饰符匹配前述语法的一次或多次重复。 它也可以写作 many1

stx ::= ...
    | stx +

? 修饰符会让子项变为可选,它匹配前述语法的零次或一次重复,但不能更多。 它也可以写作 optional

stx ::= ...
    | stx ?
stx ::= ...
    | optional(stx)

,* 修饰符匹配前述语法的零次或多次重复,并在其间穿插逗号。 它也可以写作 sepBy

stx ::= ...
    | stx ,*

,+ 修饰符匹配前述语法的一次或多次重复,并在其间穿插逗号。 它也可以写作 sepBy1

stx ::= ...
    | stx ,+

,*,? 修饰符匹配前述语法的零次或多次重复,并在其间穿插逗号,同时允许在最后一次重复后再跟一个可选尾随逗号。 它也可以通过带 allowTrailingSep 修饰符的 sepBy 来书写。

stx ::= ...
    | stx ,*,?

,+,? 修饰符匹配前述语法的一次或多次重复,并在其间穿插逗号,同时允许在最后一次重复后再跟一个可选尾随逗号。 它也可以通过带 allowTrailingSep 修饰符的 sepBy1 来书写。

stx ::= ...
    | stx ,+,?

<|> 运算符也可以写作 orelse,它匹配两边任一语法。 不过,如果第一条分支消耗了任何记号,那么解析就会提交到这条分支,之后失败也不会回溯:

stx ::= ...
    | stx <|> stx
stx ::= ...
    | orelse(stx, stx)

! 运算符匹配其参数的补集。 如果它的参数匹配失败,那么它就会成功,并重置解析状态。

stx ::= ...
    | ! stx

语法说明符可以用括号分组。

stx ::= ...
    | (stx)

重复也可以用 manymany1 来定义。 后者要求重复的语法至少出现一次。

stx ::= ...
    | many(stx)
stx ::= ...
    | many1(stx)

带分隔符的重复可以用 sepBysepBy1 来定义;它们分别匹配零次或多次出现,以及一次或多次出现,并由某种其他语法分隔。 它们有三种形式:

  • 两参数版本使用字符串字面量中给出的原子来解析分隔符,并且不允许尾随分隔符。

  • 三参数版本使用第三个参数来解析分隔符,而字符串原子只用于美化打印。

  • 四参数版本可以选择性地允许分隔符在序列末尾额外再出现一次。 第四个参数必须字面上就是关键字 allowTrailingSep

stx ::= ...
    | sepBy(stx, str)
stx ::= ...
    | sepBy(stx, str, stx)
stx ::= ...
    | sepBy(stx, str, stx, allowTrailingSep)
stx ::= ...
    | sepBy1(stx, str)
stx ::= ...
    | sepBy1(stx, str, stx)
stx ::= ...
    | sepBy1(stx, str, stx, allowTrailingSep)
解析配对的圆括号与方括号

可以使用语法规则来定义一种只由配对圆括号和方括号组成的语言。 第一步是声明一个新的 语法类别

declare_syntax_cat balanced

接下来,可以为圆括号和方括号添加规则。 为了排除空字符串,基例由空的括号对构成。

syntax "(" ")" : balanced syntax "[" "]" : balanced syntax "(" balanced ")" : balanced syntax "[" balanced "]" : balanced syntax balanced balanced : balanced

为了让 Lean 的解析器能够在这些规则上工作,还必须把这个新语法类别嵌入到某个已经可解析的类别中:

syntax (name := termBalanced) "balanced " balanced : term

这些项无法被精译,但如果到达精译错误,就说明解析已经成功:

/-- error: elaboration function for `termBalanced` has not been implemented balanced () -/ #guard_msgs in example := balanced () /-- error: elaboration function for `termBalanced` has not been implemented balanced [] -/ #guard_msgs in example := balanced [] /-- error: elaboration function for `termBalanced` has not been implemented balanced [[]()([])] -/ #guard_msgs in example := balanced [[] () ([])]

同样地,如果括号不匹配,解析就会失败:

example := balanced [() (unexpected token ']'; expected ')' or balanced]]
<example>:1:25-1:26: unexpected token ']'; expected ')' or balanced
解析逗号分隔的重复

下面这条语法可以添加一种列表字面量变体:它要求使用双层方括号,并允许尾随逗号:

syntax "[[" term,*,? "]]" : term

再加上一条说明如何把它翻译成普通列表字面量的 ,就可以在测试中使用它。

macro_rules | `(term|[[$e:term,*]]) => `([$e,*]) ["Dandelion", "Thistle"]#eval [["Dandelion", "Thistle",]]
["Dandelion", "Thistle"]

23.4.13. 缩进🔗

在内部,解析器会维护一个已保存的源位置。 语法规则可以包含与这些已保存位置交互的指令;当条件不满足时,这会导致解析失败。 像 Lean.Parser.Term.do : termdo 这样的缩进敏感构造会先保存一个源位置,在把这个已保存位置纳入考虑的同时解析其组成部分,然后再恢复原来的位置。

具体来说,缩进敏感性是通过把 withPositionwithPositionAfterLinebreak(它们会在开始解析某段其他语法时保存源位置)与 colGtcolGecolEq 组合起来指定的;后者会将当前列与最近一次保存位置的列进行比较。 lineEq 也可用于确保两个位置位于源文件的同一行上。

🔗解析器别名
withPosition(p)

参数元数是各实参元数之和。`withPosition(p)` 在运行 `p` 时把“已保存位置”设为当前位置。这本身不产生效果,但其他解析器会读取该位置以实现组合行为:`colGt`、`colGe` 和 `colEq` 将已保存位置的列与当前位置的列比较,用于实现类似 Python 的缩进敏感代码块;`lineEq` 确保当前位置仍与已保存位置处于同一行,用于实现复合词法单元。已保存位置只存在于只读状态中,因此这是一个定界解析器:离开 `withPosition(..)` 块后,已保存位置会恢复原值。此解析器与 `p` 的元数相同,只转发 `p` 的结果。

🔗解析器别名
withoutPosition(p)

参数元数是各实参元数之和。`withoutPosition(p)` 在没有已保存位置的情况下运行 `p`,因此 `colGt` 等位置检查解析器不会生效。它通常用于 `(...)` 之类的括号结构,让用户能在局部关闭空白敏感性。此解析器与 `p` 的元数相同,只转发 `p` 的结果。

🔗解析器别名
withPositionAfterLinebreak

元数:1。除非恰好只有一个实参,否则自动用 `null` 节点包裹实参。

🔗解析器别名
colGt

元数:0。除非恰好只有一个实参,否则自动用 `null` 节点包裹实参。解析器 `colGt` 要求下一个词法单元的起始列严格大于已保存位置的列(参见 `withPosition`)。它可用于策略实参等空白敏感语法,确保后续策略不会被解释成实参。示例中,`revert` 后接 `colGt ident` 列表;否则会把 `exact` 解释为标识符,并尝试回退名为 `exact` 的变量。此解析器元数为 0,不捕获任何内容。

🔗解析器别名
colGe

元数:0。除非恰好只有一个实参,否则自动用 `null` 节点包裹实参。解析器 `colGe` 要求下一个词法单元的起始列不小于已保存位置的列(参见 `withPosition`),同时允许更深缩进。它可用于空白敏感语法,确保代码块不越出给定缩进作用域。例如,Lean 的 `else if` 语法用它保证 `else` 的缩进不小于与之匹配的 `if`。此解析器元数为 0,不捕获任何内容。

🔗解析器别名
colEq

元数:0。除非恰好只有一个实参,否则自动用 `null` 节点包裹实参。解析器 `colEq` 确保下一个词法单元恰好从已保存位置所在列开始(参见 `withPosition`)。它可用于 `by` 块、`do` 块等空白敏感语法,其中各行必须对齐。此解析器元数为 0,不捕获任何内容。

🔗解析器别名
lineEq

元数:0。除非恰好只有一个实参,否则自动用 `null` 节点包裹实参。解析器 `lineEq` 要求当前词法单元与已保存位置位于同一行(参见 `withPosition`)。它可确保复合词法单元不会被换行拆开。例如,`else if` 使用 `lineEq` 解析,以确保两个词法单元位于同一行。此解析器元数为 0,不捕获任何内容。

对齐的列

这个用于记录笔记的语法接受一个项目符号列表,其中每一项都必须在同一列对齐。

syntax "note " ppLine withPosition((colEq "◦ " str ppLine)+) : term

这个语法没有关联的精译器或宏,但下面的示例可以被解析器接受:

#check elaboration function for `«termNote__◦__»` has not been implemented note ◦ "One" ◦ "Two" note "One" "Two"
elaboration function for `«termNote__◦__»` has not been implemented
  note
    ◦ "One"
    ◦ "Two"
    

这条语法并不要求列表相对于起始记号缩进;若要提出这一要求,则需要额外的 withPositioncolGt

#check elaboration function for `«termNote__◦__»` has not been implemented note ◦ "One" ◦ "Two" note "One" "Two"
elaboration function for `«termNote__◦__»` has not been implemented
  note
    ◦ "One"
    ◦ "Two"
    

下面这些示例在语法上无效,因为项目符号所在的列并不一致。

#check  note    ◦ "One"   expected end of input "Two"
<example>:4:3-4:4: expected end of input
#check  note   ◦ "One"     expected end of input "Two"
<example>:4:5-4:6: expected end of input