TableGen 概述

简介

TableGen 的目的是帮助人们开发和维护特定领域信息的记录。由于这些记录可能数量庞大,因此它专门设计用于允许编写灵活的描述,并对这些记录的共同特征进行分解。这减少了描述中的重复,降低了错误的可能性,并使构建特定领域的信息变得更容易。

TableGen 前端解析文件,实例化声明,并将结果传递给特定领域的后端进行处理。有关 TableGen 的深入描述,请参阅TableGen 程序员参考。有关运行各种 TableGen 版本的*-tblgen命令的详细信息,请参阅tblgen - 将描述转换为 C++ 代码

TableGen 的主要用户是LLVM 目标无关代码生成器Clang 诊断和属性

请注意,如果您经常使用 TableGen 并且使用 emacs 或 vim,您可以在 LLVM 发行版的llvm/utils/emacsllvm/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 的语法还引入了一些自动化概念,例如多类、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 代码生成器中的“寄存器”、“寄存器类”和“指令”)或实现者构建抽象,以帮助分解记录的常见属性(例如“FPInst”,用于表示 X86 后端中的浮点指令)。TableGen 会跟踪用于构建定义的所有类,以便后端可以找到特定类的所有定义,例如“指令”。

class ProcNoItin<string Name, list<SubtargetFeature> Features>
      : Processor<Name, NoItineraries, Features>;

这里,类 ProcNoItin 接收类型为stringName参数和目标特征列表,通过将参数传递下去以及硬编码 NoItineraries 来专门化类 Processor。

TableGen 多类是一组一次性实例化的抽象记录。每次实例化都可能导致多个 TableGen 定义。如果多类继承自另一个多类,则子多类中的定义将成为当前多类的一部分,就像它们在当前多类中声明一样。

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 的README中还可以找到 TableGen 工具和资源列表。

TableGen 缺陷

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

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

有些人赞成进一步扩展语义,但要确保后端遵守严格的规则。另一些人建议我们应该转向更少、更强大的、针对特定目的设计的 DSL,甚至重用现有的 DSL。