38.1 Tree-sitter 语言语法库

加载语言语法库

Tree-sitter 依靠语言语法库解析对应语言的文本。 在 Emacs 中,语言语法库由一个符号表示。 例如,C 语言语法库对应符号 cc 可作为 language 参数传递给 tree-sitter 函数。

Tree-sitter 语言语法库以动态库形式分发。 要在 Emacs 中使用某语言语法库,需确保系统已安装对应动态库。 Emacs 按以下顺序在多个位置查找语言语法库:

在上述每个目录中,Emacs 会查找后缀名为变量 dynamic-library-suffixes 指定后缀的文件。

若 Emacs 无法找到该库或加载出错,会触发 treesit-load-language-error 错误。 该错误信号的数据可能为以下之一:

(not-found error-msg …)

表示 Emacs 未找到该语言语法库。

(symbol-error error-msg)

表示 Emacs 在库中未找到所有语言语法库都应导出的预期函数。

(version-mismatch error-msg)

表示语言语法库版本与 tree-sitter 库版本不兼容。

在以上所有情况中,error-msg 可能会提供失败的详细信息。

Function: treesit-language-available-p language &optional detail

如果 language 对应的语言语法库存在且可加载, 该函数返回非 nil

detail 为非 nil,语言可用时返回 (t . nil), 不可用时返回 (nil . data)datatreesit-load-language-error 的信号数据。

按照惯例,language 对应动态库的文件名为 libtree-sitter-language.ext, 其中 ext 为系统专属的动态库后缀。 同样按照惯例,该库提供的函数名为 tree_sitter_language。 若某语言语法库未遵循此惯例,可在变量 treesit-load-name-override-list 中添加一项:

(language library-base-name function-name)

其中 library-base-name 为动态库文件名的基础名 (通常为 libtree-sitter-language), function-name 为库提供的函数 (通常为 tree_sitter_language)。例如:

(cool-lang "libtree-sitter-coool" "tree_sitter_cooool")

适用于那些不愿遵守惯例的 “特殊(cool)” 语言。

Function: treesit-library-abi-version &optional min-compatible

该函数返回 tree-sitter 库支持的语言语法库应用二进制接口 (ABI) 版本。默认返回库支持的最新 ABI 版本; 若 min-compatible 为非 nil,则返回库仍兼容的最旧 ABI 版本。 语言语法库必须基于 tree-sitter 库支持的新旧版本之间的 ABI 版本编译, 否则 Emacs 无法加载。

Function: treesit-language-abi-version language

该函数返回 Emacs 为 language 加载的语法库的 ABI 版本。 若 language 不可用,函数返回 nil

具体语法树

语法树是解析器生成的结果。在语法树中,每个节点代表一段文本, 节点间以父子关系相连。例如,若源代码文本为:

1 + 2

其语法树可表示为:

                  +--------------+
                  | root "1 + 2" |
                  +--------------+
                         |
        +--------------------------------+
        |       expression "1 + 2"       |
        +--------------------------------+
           |             |            |
+------------+   +--------------+   +------------+
| number "1" |   | operator "+" |   | number "2" |
+------------+   +--------------+   +------------+

也可以用 S 表达式表示:

(root (expression (number) (operator) (number)))

节点类型

rootexpressionnumberoperator 这类名称指定了节点的 类型(type)。但并非语法树中所有节点都有类型。 无类型的节点称为 匿名节点(anonymous nodes),有类型的节点称为 具名节点(named nodes)。 匿名节点是固定拼写的标记,包括括号 ‘]’ 等标点符号 以及 return 等关键字。

字段名

为便于分析语法树,许多语言语法会为子节点分配 字段名(field names)。 例如,function_definition 节点可能包含 declaratorbody

(function_definition
 declarator: (declaration)
 body: (compound_statement))

浏览语法树

为帮助理解语言语法并调试使用语法树的 Lisp 程序, Emacs 提供 “浏览(explore)” 模式,可实时显示当前缓冲区源代码的语法树。 Emacs 还附带 “查看模式(inspect mode)”,在模式行中显示光标处节点的信息。

Command: treesit-explore-mode

该模式会弹出一个窗口,显示当前缓冲区源代码的语法树。 在源代码缓冲区中选中文本时,语法树显示中对应的节点会高亮; 点击语法树中的节点时,源代码缓冲区中对应文本会高亮。

Command: treesit-inspect-mode

该次要模式在模式行上显示 起始于光标处的节点。 例如,模式行可显示:

parent field: (node (child (...)))

其中 nodechild 等为起始于光标处的节点, parentnode 的父节点。node 以粗体显示。 field-namenode 及其子节点的字段名。

若没有节点起始于光标处(即光标位于某节点中间), 模式行会显示包含光标位置的最早节点及其直接父节点。

该次要模式不会自行创建解析器, 它使用 (treesit-parser-list) (see 使用 Tree-sitter 解析器) 中的第一个解析器。

阅读语法定义

语言语法库作者会定义编程语言的 语法规则(grammar), 决定解析器如何从程序文本构建具体语法树。 要高效使用语法树,需要查阅对应的 语法文件(grammar file)

语法文件通常位于语言语法项目仓库中的 grammar.js。 各语言语法库的主页链接可在 tree-sitter 主页上找到。

语法定义使用 JavaScript 编写。 例如,匹配 function_definition 节点的规则可能如下:

function_definition: $ => seq(
  $.declaration_specifiers,
  field('declarator', $.declaration),
  field('body', $.compound_statement)
)

规则由接收单个参数 $ 的函数表示, $ 代表整个语法。函数本身由其他函数构建: seq 函数将多个子规则按顺序组合; field 函数为子节点标注字段名。 若将上述定义写成 巴科斯-诺尔范式(Backus-Naur Form) (BNF) 语法,形式为:

function_definition :=
  <declaration_specifiers> <declaration> <compound_statement>

解析器返回的节点则形如:

(function_definition
  (declaration_specifier)
  declarator: (declaration)
  body: (compound_statement))

以下是语法定义中常见的函数列表, 每个函数接收其他规则作为参数并返回新规则。

seq(rule1, rule2, …)

依次匹配各规则。

choice(rule1, rule2, …)

匹配参数中的任意一条规则。

repeat(rule)

匹配 rule 零次或多次, 类似于正则表达式中的 ‘*’ 运算符。

repeat1(rule)

匹配 rule 一次或多次, 类似于正则表达式中的 ‘+’ 运算符。

optional(rule)

匹配 rule 零次或一次, 类似于正则表达式中的 ‘?’ 运算符。

field(name, rule)

rule 匹配的子节点分配字段名 name

alias(rule, alias)

使 rule 匹配的节点在解析器生成的语法树中显示为 alias。例如:

alias(preprocessor_call_exp, call_expression)

会使所有匹配 preprocessor_call_exp 的节点显示为 call_expression

以下是阅读语言语法时相对次要的语法函数。

token(rule)

标记 rule 生成单个叶节点。 即不会生成带有多个独立子节点的父节点,而是合并为单个叶节点。 See 获取节点

token.immediate(rule)

通常语法规则会忽略前置空白符; 该函数使 rule 仅在无前置空白时匹配。

prec(n, rule)

rule 设置 n 级优先级。

prec.left([n,] rule)

标记 rule 为左结合,可指定优先级 n。

prec.right([n,] rule)

标记 rule 为右结合,可指定优先级 n。

prec.dynamic(n, rule)

功能类似 prec,但优先级在运行时生效。

tree-sitter 项目文档中有 更多关于编写语法规则的内容, 尤其建议阅读 “The Grammar DSL” 一节。