TableGen 概述¶
简介¶
TableGen 的目的是帮助人们开发和维护特定领域信息的记录。由于可能存在大量此类记录,因此它专门设计用于编写灵活的描述,并将这些记录的共同特征分解出来。这减少了描述中的重复量,降低了出错的机会,并使构建特定领域信息更加容易。
TableGen 前端解析文件,实例化声明,并将结果交给特定领域的 后端 进行处理。 有关 TableGen 的深入描述,请参阅 TableGen 程序员参考手册。 有关运行各种 TableGen 风格的 *-tblgen
命令的详细信息,请参阅 tblgen - 描述到 C++ 代码。
TableGen 当前的主要用户是 LLVM 目标无关代码生成器 和 Clang 诊断和属性。
请注意,如果您经常使用 TableGen 并且使用 emacs 或 vim,您可以在 LLVM 发行版的 llvm/utils/emacs
和 llvm/utils/vim
目录中分别找到 emacs “TableGen 模式”和 vim 语言文件。
TableGen 程序¶
TableGen 文件由 TableGen 程序解释:llvm-tblgen 在您的构建目录下的 bin 中可用。 它没有安装在系统中(或您设置 sysroot 的位置),因为它在 LLVM 的构建过程之外没有用处。
运行 TableGen¶
TableGen 的运行方式与任何其他 LLVM 工具一样。 第一个(可选)参数指定要读取的文件。 如果未指定文件名,则 llvm-tblgen
从标准输入读取。
为了有用,必须使用 后端 之一。 这些后端可以在命令行中选择(输入“llvm-tblgen -help
”以获取列表)。 例如,要获取子类化特定类型的所有定义的列表(这对于构建这些记录的枚举列表很有用),请使用 -print-enums
选项。
$ llvm-tblgen X86.td -print-enums -class=Register
AH, AL, AX, BH, BL, BP, BPL, BX, CH, CL, CX, DH, DI, DIL, DL, DX, EAX, EBP, EBX,
ECX, EDI, EDX, EFLAGS, EIP, ESI, ESP, FP0, FP1, FP2, FP3, FP4, FP5, FP6, IP,
MM0, MM1, MM2, MM3, MM4, MM5, MM6, MM7, R10, R10B, R10D, R10W, R11, R11B, R11D,
R11W, R12, R12B, R12D, R12W, R13, R13B, R13D, R13W, R14, R14B, R14D, R14W, R15,
R15B, R15D, R15W, R8, R8B, R8D, R8W, R9, R9B, R9D, R9W, RAX, RBP, RBX, RCX, RDI,
RDX, RIP, RSI, RSP, SI, SIL, SP, SPL, ST0, ST1, ST2, ST3, ST4, ST5, ST6, ST7,
XMM0, XMM1, XMM10, XMM11, XMM12, XMM13, XMM14, XMM15, XMM2, XMM3, XMM4, XMM5,
XMM6, XMM7, XMM8, XMM9,
$ llvm-tblgen X86.td -print-enums -class=Instruction
ABS_F, ABS_Fp32, ABS_Fp64, ABS_Fp80, ADC32mi, ADC32mi8, ADC32mr, ADC32ri,
ADC32ri8, ADC32rm, ADC32rr, ADC64mi32, ADC64mi8, ADC64mr, ADC64ri32, ADC64ri8,
ADC64rm, ADC64rr, ADD16mi, ADD16mi8, ADD16mr, ADD16ri, ADD16ri8, ADD16rm,
ADD16rr, ADD32mi, ADD32mi8, ADD32mr, ADD32ri, ADD32ri8, ADD32rm, ADD32rr,
ADD64mi32, ADD64mi8, ADD64mr, ADD64ri32, ...
默认后端打印出所有记录。 还有一个通用后端,它将所有记录输出为 JSON 数据结构,使用 -dump-json 选项启用。
如果您计划使用 TableGen,您很可能必须编写一个 后端,该后端提取特定于您需要的信息并以适当的方式格式化它。 您可以通过在 C++ 中扩展 TableGen 本身来做到这一点,或者通过用任何可以消耗 JSON 输出的语言编写脚本来做到这一点。
示例¶
在没有其他参数的情况下,llvm-tblgen 解析指定的文件并打印出所有类,然后打印出所有定义。 这是一个很好的方法来查看各种定义完全展开后的样子。 在 X86.td
文件上运行此命令会打印出以下内容(在撰写本文时):
...
def ADD32rr { // Instruction X86Inst I
string Namespace = "X86";
dag OutOperandList = (outs GR32:$dst);
dag InOperandList = (ins GR32:$src1, GR32:$src2);
string AsmString = "add{l}\t{$src2, $dst|$dst, $src2}";
list<dag> Pattern = [(set GR32:$dst, (add GR32:$src1, GR32:$src2))];
list<Register> Uses = [];
list<Register> Defs = [EFLAGS];
list<Predicate> Predicates = [];
int CodeSize = 3;
int AddedComplexity = 0;
bit isReturn = 0;
bit isBranch = 0;
bit isIndirectBranch = 0;
bit isBarrier = 0;
bit isCall = 0;
bit canFoldAsLoad = 0;
bit mayLoad = 0;
bit mayStore = 0;
bit isImplicitDef = 0;
bit isConvertibleToThreeAddress = 1;
bit isCommutable = 1;
bit isTerminator = 0;
bit isReMaterializable = 0;
bit isPredicable = 0;
bit hasDelaySlot = 0;
bit usesCustomInserter = 0;
bit hasCtrlDep = 0;
bit isNotDuplicable = 0;
bit hasSideEffects = 0;
InstrItinClass Itinerary = NoItinerary;
string Constraints = "";
string DisableEncoding = "";
bits<8> Opcode = { 0, 0, 0, 0, 0, 0, 0, 1 };
Format Form = MRMDestReg;
bits<6> FormBits = { 0, 0, 0, 0, 1, 1 };
ImmType ImmT = NoImm;
bits<3> ImmTypeBits = { 0, 0, 0 };
bit hasOpSizePrefix = 0;
bit hasAdSizePrefix = 0;
bits<4> Prefix = { 0, 0, 0, 0 };
bit hasREX_WPrefix = 0;
FPFormat FPForm = ?;
bits<3> FPFormBits = { 0, 0, 0 };
}
...
此定义对应于 x86 体系结构的 32 位寄存器-寄存器 add
指令。 def ADD32rr
定义了一个名为 ADD32rr
的记录,并且行末的注释指示了该定义的超类。 记录的主体包含 TableGen 为该记录组装的所有数据,表明该指令是 “X86” 命名空间的一部分,该模式指示代码生成器如何选择该指令,它是一个双地址指令,具有特定的编码等等。 记录中信息的内容和语义特定于 X86 后端的需要,仅作为示例显示。
如您所见,代码生成器支持的每个指令都需要大量信息,并且手动指定所有信息将是不可维护的,容易出错的,并且首先就令人厌烦。 因为我们正在使用 TableGen,所以所有信息都来自以下定义
let Defs = [EFLAGS],
isCommutable = 1, // X = ADD Y,Z --> X = ADD Z,Y
isConvertibleToThreeAddress = 1 in // Can transform into LEA.
def ADD32rr : I<0x01, MRMDestReg, (outs GR32:$dst),
(ins GR32:$src1, GR32:$src2),
"add{l}\t{$src2, $dst|$dst, $src2}",
[(set GR32:$dst, (add GR32:$src1, GR32:$src2))]>;
此定义使用了自定义类 I
(从自定义类 X86Inst
扩展而来),该类在特定于 X86 的 TableGen 文件中定义,以分解其类的指令共享的共同特征。 TableGen 的一个关键特性是,它允许最终用户定义他们在描述其信息时喜欢使用的抽象。
语法¶
TableGen 的语法大致基于 C++ 模板,具有内置类型和规范。 此外,TableGen 的语法引入了一些自动化概念,如 multiclass、foreach、let 等。
基本概念¶
TableGen 文件由两个关键部分组成:“类” 和 “定义”,两者都被视为 “记录”。
TableGen 记录 具有唯一的名称、值列表和超类列表。 值列表是 TableGen 为每个记录构建的主要数据; 它保存应用程序的特定领域信息。 此数据的解释留给特定的 后端,但结构和格式规则由 TableGen 负责并固定。
TableGen 定义 是 “记录” 的具体形式。 这些通常没有任何未定义的值,并用 “def
” 关键字标记。
def FeatureFPARMv8 : SubtargetFeature<"fp-armv8", "HasFPARMv8", "true",
"Enable ARMv8 FP">;
在此示例中,FeatureFPARMv8 是用某些值初始化的 SubtargetFeature
记录。 类的名称通过关键字 class 在同一文件或某些其他包含的文件中定义。 大多数目标 TableGen 文件都包含 include/llvm/Target
中的通用文件。
TableGen 类 是用于构建和描述其他记录的抽象记录。 这些类允许最终用户为他们定位的域(例如 LLVM 代码生成器中的 “Register”、“RegisterClass” 和 “Instruction”)或为实现者构建抽象,以帮助分解记录的共同属性(例如 “FPInst”,用于表示 X86 后端中的浮点指令)。 TableGen 跟踪用于构建定义的所有类,因此后端可以找到特定类的所有定义,例如 “Instruction”。
class ProcNoItin<string Name, list<SubtargetFeature> Features>
: Processor<Name, NoItineraries, Features>;
在这里,类 ProcNoItin 接收类型为 string 的参数 Name 和目标功能列表,通过向下传递参数以及硬编码 NoItineraries 来专门化类 Processor。
TableGen multiclass 是抽象记录的组,这些记录一次性实例化。 每次实例化都可能导致多个 TableGen 定义。 如果一个 multiclass 从另一个 multiclass 继承,则子 multiclass 中的定义将成为当前 multiclass 的一部分,就像它们在当前 multiclass 中声明一样。
multiclass ro_signed_pats<string T, string Rm, dag Base, dag Offset, dag Extend,
dag address, ValueType sty> {
def : Pat<(i32 (!cast<SDNode>("sextload" # sty) address)),
(!cast<Instruction>("LDRS" # T # "w_" # Rm # "_RegOffset")
Base, Offset, Extend)>;
def : Pat<(i64 (!cast<SDNode>("sextload" # sty) address)),
(!cast<Instruction>("LDRS" # T # "x_" # Rm # "_RegOffset")
Base, Offset, Extend)>;
}
defm : ro_signed_pats<"B", Rm, Base, Offset, Extend,
!foreach(decls.pattern, address,
!subst(SHIFT, imm_eq0, decls.pattern)),
i8>;
有关 TableGen 的深入描述,请参阅 TableGen 程序员参考手册。
TableGen 后端¶
TableGen 文件在没有后端的情况下没有实际意义。 运行 *-tblgen
时的默认操作是以文本格式打印信息,但这仅对调试 TableGen 文件本身有用。 然而,TableGen 的强大之处在于将源文件解释为内部表示,可以将其生成为您想要的任何内容。
TableGen 当前的用法是创建包含表的大型包含文件,您可以直接包含这些文件(如果输出是您正在编码的语言),或者通过围绕文件包含的宏在预处理中使用。
如果后端已经以 C 格式打印表格,或者如果输出只是字符串列表(用于错误和警告消息),则可以使用直接输出。 如果需要在不同上下文中使用相同的信息(如指令名称),则应使用预处理输出,因此您的后端应打印元信息列表,该列表可以形成为不同的编译时格式。
有关可用后端的列表,请参阅 TableGen 后端,有关如何编写和调试新后端的信息,请参阅 TableGen 后端开发者指南。
工具和资源¶
除了本文档外,TableGen 的工具和资源列表可以在 TableGen 的 README 中找到。
TableGen 的不足¶
尽管 TableGen 非常通用,但它仍存在一些已被多次指出的不足之处。 共同的主题是,尽管 TableGen 允许您构建特定领域的语言,但您创建的最终语言缺乏其他 DSL 的功能,这反过来又大大增加了 TableGen 文件的大小和复杂性。
与此同时,TableGen 允许您通过定制的后端创建基本概念的几乎任何含义,这可能会歪曲原始设计,并使新手很难理解这个可怕的 TableGen 文件。
有些人赞成进一步扩展语义,但要确保后端遵守严格的规则。 其他人建议我们应该转向更少、更强大的、为特定目的设计的 DSL,甚至重用现有的 DSL。