LLVM 项目博客

LLVM 项目新闻和来自底层的详细信息

学习 LLVM TableGen 的工具

TableGen 是一种在 LLVM 项目中使用的语言,用于生成各种文件,当手动维护非常困难时。

例如,它用于定义可以在特定体系结构上使用的所有指令。信息在 TableGen 中定义,我们可以根据该单个源文件生成许多东西。C++ 代码、文档、命令行选项等等。

TableGen 已经存在于 LLVM 第一个正式版本发布之前,超过 20 年了。

如今,在 LLVM 项目仓库 中,有超过一千个 TableGen 源文件,总计超过 50 万行代码。使其成为仓库中第五大流行语言。

语言 文件 空白 注释 代码
C++ 29642 958542 1870101 5544445
C/C++ 头文件 11844 316806 499845 1486165
C 10535 259900 1603594 1011269
汇编 10694 478035 1222315 820236
TableGen 1312 94112 83616 580289

(从 这个提交 计数,表格剩余部分省略)

随着像 MLIR 这样的项目 拥抱 TableGen,它只会不断增长。因此,如果你正在为 LLVM 做贡献,你最终会遇到它。

这可能是一个问题,因为 TableGen 仅存在于 LLVM 中。与 C++ 等语言不同,TableGen 没有大量的资源。

所以,除了加入一个新项目之外,你还需要学习一个新的领域特定语言 (DSL)。你来到 LLVM 不是为了学习 DSL,你可能是来编写编译器的。

我不能说这个问题何时会被解决,但情况并没有看起来那么糟糕。最近,TableGen 工具有了很大的改进,这意味着你可以将更多的精力投入到当初吸引你来到 LLVM 的目标上。

TableGen 简介

假设你想表示体系结构的寄存器。我在这里将以 Arm 的 AArch64 为例。

你可以在 TableGen 中将其描述为

$ cat register.td

class Register<int _size, string _alias=""> {
  int size = _size;
  string alias = _alias;
}

// 64 bit general purpose registers are X<N>.
def X0: Register<8> {}
// Some have special alternate names.
def X29: Register<8, "frame pointer"> {}
// Some registers omitted...

默认情况下,TableGen 编译器 llvm-tblgen 创建“记录” - 如以下所示。

$ ./bin/llvm-tblgen register.td

------------- Classes -----------------
class Register<int Register:_size = ?, string Register:_alias = ""> {
 int size = Register:_size;
 string alias = Register:_alias;
}
------------- Defs -----------------
def X0 {        // Register
 int size = 8;
 string alias = "";
}
def X29 {       // Register
 int size = 8;
 string alias = "frame pointer";
}

这是 TableGen 编译器的中间表示 (IR),类似于 LLVM 的“LLVM IR”。

使用 LLVM 时,你会选择一个“目标”,即你想要为其生成指令的处理器体系结构。TableGen 的等效项是“后端”。这些后端不生成指令,而是输出特定于该后端用例的格式。

例如,有一个后端可以生成 C++ 代码用于 搜索 数据表。其他例子包括 C 头文件和 reStructuredText 文档。

                       TableGen source
                               |
 +--llvm-tblgen----------------|------------------------+
 |                             v                        |
 |              +----- Expanded records ----+           |
 |              |                           |           |
 |              v                           v           |
 | +-------------------------+    +-------------------+ |
 | | --gen-searchable-tables |    | Other backends... | |
 | +-------------------------+    +-------------------+ |
 |              |                           |           |
 +--------------|---------------------------|-----------+
                v                           v
    .inc file with C++ code       Other output formats...
    for table searching.

主要编译器是 llvm-tblgen,但还有一些其他编译器是特定于 LLVM 的子项目。例如 clang-tblgenlldb-tblgen。唯一的区别是每个编译器中包含的后端,语言是相同的。

你可以使用你的寄存器定义来生成 C++ 代码,以便在某种引导加载程序中初始化它们。也许你还会对其进行文档化并生成该过程的图表。使用足够的后端,你就可以从相同的 TableGen 源代码中完成所有这些工作。

你可以将这些后端编写在 TableGen 编译器内的 C++ 中,或者作为使用编译器的 JSON 输出 (--dump-json) 的外部后端。因此,你可以使用任何具有 JSON 解析器 (例如 Python) 的语言。

有 TableGen,还有用 TableGen 构建的东西

与其说这是一个工具,不如说是一种思维方式。它最好用来自 文档 的一段话来概括

尽管非常通用,TableGen 仍然有一些缺陷,这些缺陷已经被指出过很多次。共同的主题是,虽然 TableGen 允许你构建领域特定语言,但你创建的最终语言缺乏其他 DSL 的能力,这反过来又会大大增加 TableGen 文件的大小和复杂性。

同时,TableGen 允许你通过定制后端来创建对基本概念的几乎任何含义,这可能会扭曲原始设计,并使新手难以理解邪恶的 TableGen 文件。”

这意味着你将处理 TableGen,以及用 TableGen 构建的东西。这些通常比语言本身更复杂。

这就像学习 C++ 并努力使用 Boost 一样。有人可能会对你说,“Boost 不是必需的,为什么不将其删除并省去麻烦呢?” 作为一个 C++ 新手,你可能不知道它们之间的界限。

当然,如果你想贡献的项目使用了 Boost,这对你就没什么帮助了。你不得不同时处理两者。用 LLVM 的术语来说,TableGen 语言和使用它的后端是一个整体。

我提到这一点是为了让你能够区分不理解其中哪一个。知道哪个让你困惑是一个很大的优势,可以帮助你找到帮助。

对于任何任务,你可能都需要了解一个或两个“用 TableGen 构建的东西”,甚至不需要完全了解它们。

不要认为你的 TableGen 之旅必须以理解它的所有用法而告终。这是可能的,但并非必需,几乎没有人能学完所有东西。相反,把你的精力放在你真正感兴趣的事情上。

编译器探索器

当然,我们在编译器探索器中也有 TableGen!如果它不在编译器探索器中,它算得上是一种真正的语言吗?

(当然它是一种语言,但如果你的最爱语言不在里面,编译器探索器有 出色的文档 和友好的维护者)

编译器探索器是一组针对不同语言和不同体系结构的不同版本编译器,你只需使用浏览器标签即可访问它们。

它是一个用于学习、教学、排查、优化和 更多 事物的绝佳工具。我在这里不会详细介绍它,只会说几句关于 TableGen 的包含情况。

显而易见的是,llvm-tblgen 不生成指令 (尽管假设的后端可以),因此没有将代码编译为二进制文件或执行代码的选项。

默认情况下,记录以纯文本形式打印。你可以通过添加编译器选项或打开“覆盖”菜单并选择“操作”来选择后端。

重要的是要注意,TableGen 后端对源代码中包含的内容有非常具体的期望。就好像你有一个 C++ 编译器,它除非在源代码中看到 arm_is_cool,否则不会为 Arm 编译一样。

在 LLVM 仓库中,所有必要的类都为你设置好了,但在编译器探索器中则没有。因此,如果你想尝试使用现有后端,我建议你提供类的存根实现,或从 LLVM 项目仓库中复制一些。你也可以使用来自 include/llvm/*.td 的标准包含文件。

目前,无法在编译器探索器中开发后端,但你可以选择 JSON 后端,并将该 JSON 复制到本地脚本中。

多文件项目(“IDE 模式”)也按预期工作,因此,如果你愿意,你可以拥有自己的 包含文件

最后,请记住,你可以分享编译器探索器示例。如果你正在提出或回答有关 TableGen 的问题,如果可以,请始终包含一个编译器探索器链接!

Jupyter 笔记本

Jupyter 创建交互式笔记本。笔记本是一个包含文本、代码和运行该代码结果的单个文档。这使你能够编辑代码并重新运行它,以更新笔记本中的结果。

这非常适合记笔记或从少量代码片段构建大型示例。你可以将文档导出为任何人都可以编辑的笔记本,或者导出为非交互式格式,例如 PDF 或 Markdown。

TableGen 可以通过使用 TableGen Jupyter 内核在笔记本中使用。安装说明请访问 这里,你也可以观看我在这里谈论更多关于它的内容 这里

**注意:** 还有一个 MLIR 内核 用于 Jupyter,以及许多其他内核。

我们旨在提供与其他语言相同的体验,因此我不会关注如何使用笔记本,而是关注我们能够用它来做什么。

TableGen 教程笔记本

这个笔记本是 TableGen 的入门教程。你可以在 GitHub 上阅读它,或者 下载 它并在 Jupyter 中阅读它。

使用 Jupyter 时,你可以编辑文档以添加自己的示例或扩展你感兴趣的示例。

“如何编写 TableGen 后端”笔记本

这个笔记本使用 Python 而不是 TableGen,它向你展示了如何编写后端。

2021 年 EU LLVM 开发者会议的演讲 “如何编写 TableGen 后端” 由 Min-Yih Hsu 发表,它是该笔记本的基础。该 笔记本 实际上是 Min 自己用 C++ 实现的 Python 移植版。

它向你展示了如何获取 llvm-tblgen 的 JSON 输出,并使用 Python 处理它以创建 SQL 查询。

这里独特之处在于,我们现在在多种媒体形式和多种编程语言中拥有相同的内容。选择最适合你的那些。

参考“有 TableGen,还有用 TableGen 构建的东西”,教程笔记本是 TableGen。编写后端笔记本是“用 TableGen 构建的东西”。

限制

笔记本的主要限制是我们没有输出过滤。这意味着如果你执行 include “llvm/Target/Target.td",你将获得大约 320,000 行输出 (在你添加任何自己的代码之前)。这超过了默认笔记本从内核接受的数量,当我删除该限制时,浏览器标签崩溃了。

在大多数情况下,这不是问题,可能的解决方案存在很大的权衡,因此我们不会急于修复它。如果它确实影响了你,请将你的反馈添加到 跟踪问题 中。

TableGen 语言服务器

MLIR 项目已经为 语言服务器协议 (LSP) 实现了一个服务器。该服务器支持 TableGen 和 MLIR 中使用的另外两种语言

语言服务器协议向兼容的编辑器提供有关语言和项目的结构的信息。例如,包含文件在哪里?特定类型的定义在哪里?

如果你使用过 LSP 兼容编辑器 (例如 Visual Studio Code),你可能在不知不觉中使用过语言服务器。“转到定义”是它们提供的最常见的功能。

语言服务器协议允许你打开一个项目,转到你想更改的代码,并直接从那里跳转到仓库中其他相关的部分。在 LLVM 项目中,有超过 500,000 行 TableGen 代码,这意味着你可以忽略大量代码!

设置

你需要一个服务器二进制文件 tblgen-lsp-server 的副本。你可以从适用于你平台的 发布包 中获得它,也可以自己构建它。

这是自己动手构建的方法

$ cmake -G Ninja <path-to>/llvm-project/llvm -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="mlir"
$ ninja tblgen-lsp-server

运行完这些命令后,tblgen-lsp-server 可以在 <build-dir>/bin/ 中找到。

服务器读取一个编译数据库文件 tablegen_compile_commands.yml,该文件在使用 CMake 配置 LLVM 时为您创建。

它与使用 CMAKE_EXPORT_COMPILE_COMMANDS 生成的 compile_commands.json 文件具有类似的作用,但这两个文件无关。

只要您签出的 llvm-project 包含 此提交,编译数据库就包含来自所有启用项目的 TableGen 文件(在此提交之前,它仅限于 MLIR)。

例如,此配置命令包含有关来自 LLVM、Clang、MLIR 和 LLDB 子项目的 TableGen 文件的信息

$ cmake -G Ninja <path-to>/llvm-project/llvm -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang;llvm;lldb;mlir"

这也适用于 -DLLVM_TARGETS_TO_BUILD=。仅启用一个目标意味着编译数据库仅包含与该目标相关的文件。

注意:您不需要构建项目即可将它的 TableGen 文件包含在编译数据库中。配置就足够了。

接下来,为您的编辑器配置 LSP 客户端。

如果您使用的是 Visual Studio Code,请安装 MLIR 扩展。然后按照 此处 的设置说明告诉扩展程序服务器和编译数据库的位置。

如果您使用的是其他编辑器,请参考其文档了解如何设置语言服务器。设置编译数据库路径可能需要使用服务器的命令行选项。运行 tblgen-lsp-server --help 以查看所有可用选项。

示例

此示例假定您已使用启用的 AArch64 目标配置了 LLVM。(它默认情况下已启用)

  • 打开文件 llvm/lib/Target/AArch64/AArch64.td
  • 将光标放在 SubtargetFeature 类型的使用位置上。
  • 在菜单栏中,选择“转到”然后“转到定义”。
  • 这将带您到 llvm/include/llvm/Target/Target.td,其中定义了 SubtargetFeature

限制

语言服务器突出显示了一些 LLVM 目标(如 AArch64)使用 TableGen 的反模式。

您可能会发现自己处于使用某个类但没有定义它或包含任何定义它的文件的文件中。这是因为该文件打算包含在另一个文件中,该文件确实包含对该类的定义。

example.td:
  class Example {}

uses_example.td:
  def example: Example {}

main.td:
  include "example.td"
  include "uses_example.td"

上面的示例显示了这种反模式

  • 文件 example.td 定义了类 Example
  • uses_example.td 使用类 Example,但没有包含 example.td
  • main.td 包含 example.tduses_example.td
  • main.td 是被编译的文件。
  • 当您在 uses_example.td 中时,语言服务器不知道 Example 在哪里定义,
  • 当您在 main.td 中时,语言服务器知道 Example 在哪里定义。

也许我们可以通过改进语言服务器或重新组织包含文件来解决这个问题,这样我们就不会有看起来孤立的文件。

转储

printf 怎么办?它是最棒的调试工具。

TableGen 的等效项是 dump,以及它的伙伴 repr

def op;
class A {
  string A = "some text";
  dag X =(op op);
}
def a : A;

dump "The Value of a is: \n" # !repr(a);

dump 打印到 stderr

<source>:8:1: note: The Value of a is:
a {	// A
  string A = "some text";
  dag X = (op op);
}

dump "The Value of a is: \n" # !repr(a);
^

这是 最近 添加的。因此,您需要一个最近的构建,或 18.0 或更高版本(在撰写本文时尚未发布)。

当然,您现在可以在 Compiler Explorer 上尝试一下!

断言

断言检查程序中特定位置的条件是否为真。断言由以下部分组成

  • 关键字 assert
  • 一个条件(通常是调用 感叹号运算符 中的一个)。
  • 一条消息。

如果条件为假,则会生成一个编译器错误,并显示您提供的消息。

例如,下面的代码检查您是否尝试创建大小小于 0 的寄存器。

class Register<int _size> {
  assert !gt(_size, 0),
       "Register size must be > 0, not " # _size # "." ;
  int size = _size;
}

def X0: Register<8> {}
def X1: Register<-8> {}

(在 Compiler Explorer 上尝试一下)

寄存器 X0_size=8,因此条件 !gt(_size, 0)(在 C 语法中为 _size > 0)为真,因此不会生成任何错误。

寄存器 X1_size=-8,因此条件为假,并生成错误。编译器输出如下所示

<source>:2:11: error: assertion failed
   assert !gt(_size, 0),
          ^
note: Register size must be > 0, not -8.

在学习新代码时,添加自己的断言以检查您的假设很有帮助。此外,在为其他人编写的代码中添加断言是阻止他们错误使用它的好方法。与文档不同,您不会错过断言错误。

在文件中查找

这是最后一个,因为在理想情况下,它应该是最后的选择,但它通常不是最不重要的选择。Grep、ack、在文件中查找,无论您称之为何,如果您对语言语法有一点了解,搜索文本的效果超乎想象。

我为什么要提这样一个显而易见的想法?好吧,显而易见是主观的,并且有一种特殊情况使它比平时更有效。

在 LLVM 项目存储库中,我们拥有当今使用的绝大多数 TableGen 代码。您想知道如何使用某个特定功能吗?它都在那里,存在于 500,000 多行源代码中。您会对一个简单的查询即使在这样庞大的代码库中也能找到什么感到惊讶。

想想您要找的东西。您认为它的源代码会是什么样?如果它是一个类,它是否有模板参数,如果有,它的名称后面是否会有一个 <?如果它是一个错误消息,哪些部分是常量,哪些部分会被插入模板消息?

预期换行符 可能是静态字符串,因此您可以搜索消息本身。相比之下,class Foo 没有属性 Bar 更可能是通过代入类名和属性名创建的。因此,此项的最佳搜索词是 没有属性

编译器也有测试,其中大多数位于 此文件夹 中。该文件夹包含语言功能的最小示例。尝试将搜索范围缩小到此位置。

结论

学习 TableGen 不必令人害怕。不要认为因为它是一个孤立的 DSL,所以它就不具备您对最喜欢的语言的期望。

请记住,TableGen 也是一个工具,而不是一个目标本身。如果您能够通过对 TableGen 及其后端的有限但准确的理解来实现您的目标,那就太好了。学到您想学或需要学的东西即可。

除了这些工具之外,还有一个积极的社区随时准备在 Discord论坛 上回答您的问题。

如果您发现问题或想要贡献改进,请这样做。在 GitHub 上打开一个 问题拉取请求

看看您使用的其他语言。它们有这些工具吗?应该有吗?它们可能是挫败感和您最喜欢的语言之间的区别。

致谢

感谢 Andrzej Warzyński、Francesco Petrogalli、Min-Yih Hsu 和 Sally Neale(Arm)审阅本文。