LLVM笔记(8) - tablegen介绍
之前写tablegen的介绍写到一半放弃了, 最近培训需要再拉出来看看, 顺便做个笔记.
1. 语法介绍
官方文档见这里, 以下是文档的翻译.
1.1. 类型系统
tablegen是强类型语言, 其类型系统同时包含low-level(i.e. bit int)与high-level(i.e. dag). 以下是tablegen支持的内建类型.
bit: 布尔值, 0或1.
int: 表示32bit整型.
string: 字符串.
code: 一个代码片段, 通常以多行字符串的形式表示.
bits
list
class type: class类型.
dag: 表示一个有向图.
1.2. 值与表达式
tablengen支持以下表达式.
?: 未初始化值.
0b10: bit串, 注意该位串不会被扩展或截断, 因此对类型为bits
1: 十进制常量.
0x11: 十六进制常量.
"abc": 字符串常量, 可以赋值给string类型或code类型.
value: 引用一个value.
value{n}: 访问value的第n位.
value.field: 访问value的field成员.
[x, y, z]: 队列容器赋值.
{a, b}: 位串赋值, 其总长度为位串a加位串b的长度和.
还有很多表达式就不一一列出了, 读者请自行参考文档. 另外tablegen支持的内建函数表达式(i.e. foreach strconcat listconcat)也请参见文档说明.
1.3. 类(class)与定义(definition)
类与定义是tablegen语言中关键组成, 它们又被称作条目(records).
一个records包含一个唯一的名字, 一组成员变量及一组(扩展的)超类. 对records的解释由具体的后端处理, 但records的语法格式与结构由tablegen的前端进行检查.
类是一种抽象的records, 类定义为用户提供抽象架构公共属性的能力. 类似的, multiclass提供了一组抽象的records, 对multiclass的实例化会创建一组定义.
定义是一种具体的records, 它们通过def关键字被定义.
来看个例子加深理解.
class C { bit V = 1; }
def X : C;
def Y : C {
string Greeting = "hello";
}
以上代码片段声明了类C, 类C有两个实例, X与Y. 由于定义X与定义Y均继承自类C, 所以它们都包含成员V, 而定义Y包含一个额外的字符串成员Greeting.
1.4. 赋值表达式
let表达式允许我们修改变量的值.
class C { bit V = 0; }
def D : C { let V = 1; }
类C中定义了一个bit类型变量V, 其默认值为0, 在实例D中将V修改为1.
1.5. 类的模板参数
tablegen提供了参数化类定义, 允许在定义类时指定一系列参数并绑定到对应变量.
class C<bit v> { bit V = v; }
def X : C<1>;
def Y : C<0>;
def Z : C<1> { let V = 0; }
以上文例子为例, 类C是一个模板类, 其接受一个bit类型变量v并使用该值初始化其成员变量V.
定义X与定义Y均继承自类C, 根据传入的参数, 定义X是一个V为1的类实例(等于1.4.中的D), 定义Y是一个V为0的类实例.
特殊的, 定义Z中的V值为0还是为1呢? 在模板初始化时V被设置为1, 然后let表达式生效, 所以V最终为0 (可以理解为赋值操作在模板实例化之后生效).
1.5. multiclass定义与实例
模板类给我们提供了描述一组具有公共属性的类的能力, 进一步的tablegen还提供了一种定义多个类的方式.
multiclass允许我们同时定义一组类, 继承自multiclass类会被扩展成多个类定义实例. 让我们来看个例子.
class C<bit v, bit i> { bit V = v; bit I = i; }
multiclass D<bit v> {
def X : C<v, 0>;
def Y : C<v, 1>;
}
defm V0 : D<0>;
defm V1 : D<1>;
模板类C接受两个参数v与i, 即有四种类实例. 如果使用之前的写法就需要编写四个类定义.
利用multiclass我们定义一个类D, 当实例化一个类D时我们实际实例化了两个类定义X与Y. 通过这种方式我们可以进一步减少重复代码的编写.
注意: 实例化一个multiclass时需要使用defm替换def, 并且该实例的名字是defm后接的名字与multiclass中def的名字拼接而成. i.e. 上文中实际定义了四个类实例, 名字分别为V0_X, V0_Y, V1_X, V1_Y.
从以上举例可以看到tablegen是非常灵活的语言, 易于使用者编写.
2. 语言参考
这里介绍如何编写tablegen代码.
2.1. 注释
使用//或/.../方式添加注释.
2.2. 文件包含
使用include "***"方式包含其它文件
TODO
3. tablegen后端
前文提到过tablegen由两部分组成, 前端的parser的作用是将tablegen description file (td file)翻译成records, records的含义则由后端解析.
当前clang与llvm都用到tablegen来生成代码, 前端部分不很熟悉, 因此本文简要介绍下后端自动生成代码.
这里可以了解tablegen自动生成的代码模块.
3.1. tablegen源码目录
tablegen代码由两部分组成, 前端的lexer与parser在lib/TableGen/目录下, 作用是解析td file.
后端代码在utils/TableGen/目录下, 是我们关心的逻辑代码. 在解析每个模块时我们会一一介绍.
3.2. 如何使用
这块之前有做过简要介绍. 这次以RISCV架构为例:
[00:58:41] hansy@hansy:~/llvm-mono/llvm (master)$ llvm-tblgen -I lib/Target/RISCV/ -I ./include/ ./lib/Target/RISCV/RISCV.td -o 123.inc
[00:59:36] hansy@hansy:~/llvm-mono/llvm (master)$ vim 123.inc
llvm-tblgen是tablegen源码编译出的工具, 它必须接受一个td file输入(RISCV.td), 该td file中包含的头文件索引路径通过-I参数传递, 默认不添加参数时会生成全量代码, 这里就不展示了.
这里再稍微提下llvm构建时是如何调用tablegen的. 首先在lib/Target/RISCV/CMakeLists.txt中包含如下代码:
set(LLVM_TARGET_DEFINITIONS RISCV.td)
tablegen(LLVM RISCVGenAsmMatcher.inc -gen-asm-matcher)
tablegen(LLVM RISCVGenAsmWriter.inc -gen-asm-writer)
tablegen(LLVM RISCVGenCompressInstEmitter.inc -gen-compress-inst-emitter)
tablegen(LLVM RISCVGenDAGISel.inc -gen-dag-isel)
tablegen(LLVM RISCVGenDisassemblerTables.inc -gen-disassembler)
tablegen(LLVM RISCVGenGlobalISel.inc -gen-global-isel)
tablegen(LLVM RISCVGenInstrInfo.inc -gen-instr-info)
tablegen(LLVM RISCVGenMCCodeEmitter.inc -gen-emitter)
tablegen(LLVM RISCVGenMCPseudoLowering.inc -gen-pseudo-lowering)
tablegen(LLVM RISCVGenRegisterBank.inc -gen-register-bank)
tablegen(LLVM RISCVGenRegisterInfo.inc -gen-register-info)
tablegen(LLVM RISCVGenSubtargetInfo.inc -gen-subtarget)
tablegen(LLVM RISCVGenSystemOperands.inc -gen-searchable-tables)
其中函数tablegen (defined in cmake/modules/TableGen.cmake)作用是根据传入参数配置命令行并调用llvm-tablegen. 可以看到生成的代码放在build/lib/Target/RISCV/目录下.
由于不同架构的不同硬件特性, 需要生成的代码也不相同, 下文会简要介绍几个常见的文件, 细节的内容可以根据以上介绍自行查找代码.
3.3. 指令描述(-gen-instr-info)
TODO