TableGen 后端

介绍

TableGen 后端是 TableGen 功能的核心。源文件提供了被解析并最终成为记录实例集合的类和记录,但后端负责以对用户有意义的方式(通常是 C++ 头文件或警告、选项和错误消息的文本列表)解释和打印记录。

LLVM、Clang 和 MLIR 都使用 TableGen,但目标截然不同。LLVM 使用它来自动化生成关于指令、调度、内核和架构特性的海量信息。一些后端生成的输出被多个源文件使用,因此它们的创建方式需要便于使用预处理器技巧。一些后端还可以打印 C++ 代码结构,以便可以直接按原样包含它们。

另一方面,Clang 主要将其用于诊断消息(错误、警告、提示)和属性,因此更侧重于文本方面。

MLIR 使用 TableGen 来定义操作、操作方言和操作特性。

有关 TableGen 的深入描述,请参阅 TableGen 程序员参考,有关编写新后端的指南,请参阅 TableGen 后端开发者指南

LLVM 后端

警告

这部分内容不完整。以下每个部分都需要三个小节:对其用途的描述(包括用户列表)、从通用输入生成的输出,以及最终为何需要新后端(如果存在类似的东西)。

总的来说,每个后端将采用相同的 TableGen 文件类型,并将其转换为针对不同目标/用途的类似输出。TableGen 文件、后端及其用户之间存在隐式约定。

例如,一个全局约定是每个后端都生成宏保护部分。根据文件是被头文件还是源文件包含,甚至在每个文件的哪个上下文中使用包含,您必须在包含它之前定义一个宏,以获得正确的输出

#define GET_REGINFO_TARGET_DESC
#include "ARMGenRegisterInfo.inc"

并且只包含生成文件的一部分。如果您需要从同一个源 TableGen 文件中以多种格式(实例化、初始化、getter/setter 函数等)获取相同的信息,而无需多次重新编译 TableGen 文件,这将非常有用。

有时,可以在同一个包含文件之前定义多个宏以输出多个块

#define GET_REGISTER_MATCHER
#define GET_SUBTARGET_FEATURE_NAME
#define GET_MATCHER_IMPLEMENTATION
#include "ARMGenAsmMatcher.inc"

宏将在使用时自动取消定义,在包含文件中。

在所有 LLVM 后端上,llvm-tblgen 二进制文件将在根 TableGen 文件 <Target>.td 上执行,该文件应包含所有其他文件。这保证了所需的所有信息都可访问,并且 TableGen 文件中不需要重复。

CodeEmitter

目的:CodeEmitterGen 使用指令及其字段的描述来构建自动代码发射器:一个函数,给定一个 MachineInstr,返回指令的值(当前为 32 位无符号)。

输出:C++ 代码,通过覆盖虚拟函数 <Target>CodeEmitter::function() 来实现目标的 CodeEmitter 类。

用法:用于直接包含在 <Target>MCCodeEmitter.cpp 的末尾。

RegisterInfo

目的:此 tablegen 后端负责为代码生成器发出目标寄存器文件的描述。它使用 Register、RegisterAliases 和 RegisterClass 类的实例来收集此信息。

输出:C++ 代码,其中包含表示寄存器映射、属性、掩码等的枚举和结构。

用法:在 <Target>BaseRegisterInfo<Target>MCTargetDesc(头文件和源文件)上都使用,并使用宏定义它们用于声明还是初始化问题。

InstrInfo

目的:此 tablegen 后端负责为代码生成器发出目标指令集的描述。(与 CodeEmitter 有什么区别?)

输出:C++ 代码,其中包含表示指令映射、属性、掩码等的枚举和结构。

用法:在 <Target>BaseInstrInfo<Target>MCTargetDesc(头文件和源文件)上都使用,并使用宏定义它们用于声明还是初始化问题。

AsmWriter

目的:为当前目标发出汇编打印器。

输出<Target>InstPrinter::printInstruction() 的实现,以及其他内容。

用法:直接包含到 InstPrinter/<Target>InstPrinter.cpp 中。

AsmMatcher

目的:发出目标说明符匹配器,用于转换 MCInst 结构中解析的汇编操作数。它还发出用于自定义操作数解析的匹配器。有关详细文档,请参阅 AsmMatcherEmitter.cpp 文件。

输出:汇编器解析器的匹配器函数、声明等。

用法:在后端的 AsmParser/<Target>AsmParser.cpp 中用于构建 AsmParser 类。

反汇编器

目的:包含各种体系结构的反汇编器表发射器。有关详细文档,请参阅 DisassemblerEmitter.cpp 文件。

输出:解码表、静态解码函数等。

用法:直接包含在 Disassembler/<Target>Disassembler.cpp 中,以处理所有默认解码,在所有手工解码之后。

PseudoLowering

目的:生成伪指令降低。

输出:实现 <Target>AsmPrinter::emitPseudoExpansionLowering()

用法:直接包含到 <Target>AsmPrinter.cpp 中。

CallingConv

目的:负责发出此目标支持的调用约定的描述。

输出:实现静态函数来处理通过匹配样式链接的调用约定,在不匹配时返回 false。

用法:在 ISelLowering 和 FastIsel 中用作函数指针,指向 CC 选择函数返回的实现。

DAGISel

目的:生成 DAG 指令选择器。

输出:创建用于自动化 DAG 选择的巨大函数。

用法:包含在 <Target>ISelDAGToDAG.cpp 中,在目标的 SelectionDAGISel 实现内部。

DFAPacketizer

目的:此类解析 Schedule.td 文件并生成一个 API,该 API 可用于推理是否可以将指令添加到 VLIW 架构上的数据包中。该类在内部生成确定性有限自动机 (DFA),该自动机对将机器指令映射到功能单元的所有可能映射进行建模,因为指令被添加到数据包中。

输出:GPU 后端(Hexagon、AMD)的调度表。

用法:直接包含在 <Target>InstrInfo.cpp 中。

FastISel

目的:此 tablegen 后端发出供“快速”指令选择算法使用的代码。有关背景信息,请参阅 lib/CodeGen/SelectionDAG/FastISel.cpp 顶部的注释。此文件扫描目标 tablegen 指令信息文件,并提取具有显而易见的模式的指令,并发出代码以按类型和运算符查找这些指令。

输出:生成 PredicateFastEmit 方法。

用法:实现目标的 FastISel 类的私有方法。

Subtarget

目的:生成子目标枚举。

输出:枚举、全局变量、子目标信息的本地表。

用法:填充 <Target>SubtargetMCTargetDesc/<Target>MCTargetDesc 文件(头文件和源文件)。

Intrinsic

目的:生成(目标)内联函数信息。

OptParserDefs

目的:打印类的枚举值。

SearchableTables

目的:生成自定义可搜索表。

输出:枚举、全局表和查找辅助函数。

用法:此后端允许从 TableGen 记录生成自由格式的、特定于目标的表。ARM 和 AArch64 目标使用此后端生成系统寄存器表;AMDGPU 目标使用它生成关于复杂图像和内存缓冲区指令的元数据。

有关详细描述,请参阅 SearchableTables 参考

CTags

目的:此 tablegen 后端以 ctags(1) 格式发出定义索引。辅助脚本 utils/TableGen/tdtags 提供了一个更易于使用的界面;运行 'tdtags -H' 获取文档。

X86EVEX2VEX

目的:此 X86 特定 tablegen 后端发出将 EVEX 编码指令映射到其 VEX 编码相同指令的表。

Clang 后端

ClangAttrClasses

目的:创建 Attrs.inc,其中包含 Attr.td 中未设置 ASTNode = 0 的任何属性的语义属性类声明。此文件作为 Attr.h 的一部分包含在内。

ClangAttrParserStringSwitches

目的:创建 AttrParserStringSwitches.inc,其中包含用于解析器相关字符串开关的 StringSwitch::Case 语句。每个开关都有自己的宏(例如 CLANG_ATTR_ARG_CONTEXT_LISTCLANG_ATTR_IDENTIFIER_ARG_LIST),预计在包含 AttrParserStringSwitches.inc 之前定义,并在之后取消定义。

ClangAttrImpl

目的:创建 AttrImpl.inc,其中包含 Attr.td 中未设置 ASTNode = 0 的任何属性的语义属性类定义。此文件作为 AttrImpl.cpp 的一部分包含在内。

ClangAttrList

目的:创建 AttrList.inc,当需要语义属性标识符列表时使用。例如,AttrKinds.h 包含此文件以生成 attr::Kind 枚举值的列表。此列表分为多个类别:属性、可继承属性和可继承参数属性。此分类基于 Attr.td 中的信息自动进行,并用于实现 dyn_cast 和类似 API 所需的 classof 功能。

ClangAttrPCHRead

目的:创建 AttrPCHRead.inc,用于在 ASTReader::ReadAttributes 函数中反序列化属性。

ClangAttrPCHWrite

目的:创建 AttrPCHWrite.inc,用于在 ASTWriter::WriteAttributes 函数中序列化属性。

ClangAttrSpellings

目的:创建 AttrSpellings.inc,用于实现 __has_attribute 功能测试宏。

ClangAttrSpellingListIndex

目的:创建 AttrSpellingListIndex.inc,用于将解析的属性拼写(包括使用的语法或作用域)映射到属性拼写列表索引。这些拼写列表索引值是通过 AttributeList::getAttributeSpellingListIndex 公开的内部实现细节。

ClangAttrVisitor

目的:创建 AttrVisitor.inc,在实现递归 AST 访问器时使用。

ClangAttrTemplateInstantiate

目的:创建 AttrTemplateInstantiate.inc,它实现了 instantiateTemplateAttribute 函数,该函数在实例化需要克隆属性的模板时使用。

ClangAttrParsedAttrList

目的:创建 AttrParsedAttrList.inc,用于生成 AttributeList::Kind 解析的属性枚举。

ClangAttrParsedAttrImpl

目的:创建 AttrParsedAttrImpl.inc,AttributeList.cpp 使用它来实现 AttributeList 类上的多个函数。此功能通过 AttrInfoMap ParsedAttrInfo 数组实现,该数组包含每个解析的属性对象一个元素。

ClangAttrParsedAttrKinds

目的:创建 AttrParsedAttrKinds.inc,用于实现 AttributeList::getKind 函数,将字符串(和语法)映射到解析的属性 AttributeList::Kind 枚举。

ClangAttrDump

目的:创建 AttrDump.inc,它转储关于属性的信息。它用于实现 ASTDumper::dumpAttr

ClangDiagsDefs

生成 Clang 诊断定义。

ClangDiagGroups

生成 Clang 诊断组。

ClangDiagsIndexName

生成 Clang 诊断名称索引。

ClangCommentNodes

生成 Clang AST 注释节点。

ClangDeclNodes

生成 Clang AST 声明节点。

ClangStmtNodes

生成 Clang AST 语句节点。

ClangSACheckers

生成 Clang 静态分析检查器。

ClangCommentHTMLTags

为文档注释中使用的 HTML 标签名称生成高效的匹配器。

ClangCommentHTMLTagsProperties

为 HTML 标签属性生成高效的匹配器。

ClangCommentHTMLNamedCharacterReferences

生成将命名字符引用转换为 UTF-8 序列的函数。

ClangCommentCommandInfo

为文档注释中使用的命令生成命令属性。

ClangCommentCommandList

生成文档注释中使用的命令列表。

ArmNeon

为 clang 生成 arm_neon.h。

ArmNeonSema

为 clang 生成 ARM NEON 语义分析支持。

ArmNeonTest

为 clang 生成 ARM NEON 测试。

AttrDocs

目的:从 AttrDocs.td 创建 AttributeReference.rst,用于记录面向用户的属性。

通用后端

JSON 参考

目的:输出每个 def 中的所有值,作为可以被各种语言轻松解析的 JSON 数据结构。对于编写自定义后端而无需修改 TableGen 本身,或对传递给内置后端的相同 TableGen 数据执行辅助分析非常有用。

输出:

输出文件的根是一个 JSON 对象(即字典),包含以下固定键

  • !tablegen_json_version:一个数字版本字段,如果对该数据的结构进行不兼容的更改,该字段将增加。此处描述的格式对应于版本 1。

  • !instanceof:一个字典,其键是在 TableGen 输入中定义的类名。对于每个键,对应的值是一个字符串数组,给出从该类派生的 def 记录的名称。例如,root["!instanceof"]["Instruction"] 将列出从 Instruction 类派生的所有记录的名称。

对于每个 def 记录,根对象还具有记录名称的键。对应的值是一个子对象,包含以下固定键

  • !superclasses:一个字符串数组,给出此记录从中派生的所有类的名称。

  • !fields:一个字符串数组,给出此记录中所有使用 field 关键字定义的变量的名称。

  • !name:一个字符串,给出记录的名称。这始终与 JSON 根对象中对应于此记录字典的键相同。(如果记录是匿名的,则名称是任意的。)

  • !anonymous:一个布尔值,指示记录的名称是由 TableGen 输入指定的(如果为 false),还是由 TableGen 本身发明的(如果为 true)。

  • !locs:一个字符串数组,给出与此记录关联的源位置。对于从 multiclass 实例化的记录,这给出了每个 defdefm 的位置,从最内层的 multiclass 开始,到顶层的 defm 结束。每个字符串都包含文件名和行号,以冒号分隔。

对于记录中定义的每个变量,该记录的 def 对象还具有变量名称的键。对应的值是变量值转换为 JSON 的结果,使用下面描述的约定。

一些 TableGen 数据类型直接转换为相应的 JSON 类型

  • 完全未定义的值(例如,对于在记录的某些超类中声明但未初始化,并且从未由此记录本身或任何其他超类初始化的变量)将作为 JSON null 值发出。

  • intbit 值作为数字发出。请注意,TableGen int 值能够容纳太大而无法在 IEEE 双精度中精确表示的整数。JSON 输出中的整数文字将显示完整的精确整数值。因此,如果您需要检索具有完整精度的大整数,则应使用能够将此类文字转换回 64 位整数而不会丢失精度的 JSON 读取器,例如 Python 的标准 json 模块。

  • stringcode 值作为 JSON 字符串发出。

  • list<T> 值(对于任何元素类型 T)作为 JSON 数组发出。数组的每个元素都使用相同的约定依次表示。

  • bits 值也作为数组发出。bits 数组从最低有效位到最高有效位排序。因此,索引为 i 的元素对应于 TableGen 源代码中描述为 x{i} 的位。但是,请注意,这意味着脚本语言可能会以与它在 TableGen 源代码或诊断 -print-records 输出中出现的顺序相反的顺序显示数组。

所有其他 TableGen 值类型都作为 JSON 对象发出,其中包含两个标准字段:kind 是描述对象表示哪种值的鉴别器,printable 是一个字符串,给出与 -print-records 中将出现的相同的值的表示形式。

  • 指向 def 对象的引用具有 kind=="def",并且有一个额外的字段 def,给出被引用对象的名称。

  • 指向同一记录中另一个变量的引用具有 kind=="var",并且有一个额外的字段 var,给出被引用变量的名称。

  • 指向同一记录中 bits 类型变量的特定位的引用具有 kind=="varbit",并且有两个额外的字段:var 给出被引用变量的名称,而 index 给出位的索引。

  • 类型为 dag 的值具有 kind=="dag",并且有两个额外的字段。operator 给出 dag 初始化器开括号后的初始值;args 是一个数组,给出后续的参数。 args 的元素是长度为 2 的数组,给出每个参数的值,后跟其带冒号后缀的名称(如果有)。例如,在 dag 值 (Op 22, "hello":$foo) 的 JSON 表示中(假设 Op 是在其他地方定义的记录的名称,带有一个 def 语句)

    • operator 将是一个对象,其中 kind=="def"def=="Op"

    • args 将是数组 [[22, null], ["hello", "foo"]]

  • 如果在输出中出现任何其他类型的值或复杂表达式,它将具有 kind=="complex",并且没有额外的字段。后端不应需要这些值。如果需要,可以使用标准的 printable 字段以 TableGen 源代码语法提取它们的表示形式。

SearchableTables 参考

TableGen 包含文件 SearchableTable.td 提供了用于生成 C++ 可搜索表的类。以下部分描述了这些表。要生成 C++ 代码,请使用 --gen-searchable-tables 选项运行 llvm-tblgen,这将调用后端,从您提供的记录生成表。

为可搜索表生成的每个数据结构都由 #ifdef 保护。这允许您包含生成的 .inc 文件,并仅选择某些数据结构包含在内。以下示例显示了这些保护中使用的宏名称。

通用枚举类型

GenericEnum 类使定义 C++ 枚举类型和该类型的枚举元素变得容易。要定义类型,请定义一个父类为 GenericEnum 且名称为所需枚举类型的记录。此类提供三个字段,您可以使用 let 语句在记录中设置这些字段。

  • string FilterClass。枚举类型将为每个派生自此类别的记录包含一个元素。这些记录被收集起来以组装完整的元素集。

  • string NameField在收集的记录中指定元素名称的字段的名称。如果记录没有此类字段,则将使用记录的名称。

  • string ValueField在收集的记录中指定元素数值的字段的名称。如果记录没有此类字段,则将为其分配一个整数值。值按字母顺序分配,从 0 开始。

这是一个示例,其中元素的数值被显式指定为 BEntry 类的模板参数。结果 C++ 代码如下所示。

def BValues : GenericEnum {
  let FilterClass = "BEntry";
  let NameField = "Name";
  let ValueField = "Encoding";
}

class BEntry<bits<16> enc> {
  string Name = NAME;
  bits<16> Encoding = enc;
}

def BFoo   : BEntry<0xac>;
def BBar   : BEntry<0x14>;
def BZoo   : BEntry<0x80>;
def BSnork : BEntry<0x4c>;
#ifdef GET_BValues_DECL
enum BValues {
  BBar = 20,
  BFoo = 172,
  BSnork = 76,
  BZoo = 128,
};
#endif

在以下示例中,元素的数值是自动分配的。请注意,值是从 0 开始按元素名称的字母顺序分配的。

def CEnum : GenericEnum {
  let FilterClass = "CEnum";
}

class CEnum;

def CFoo : CEnum;
def CBar : CEnum;
def CBaz : CEnum;
#ifdef GET_CEnum_DECL
enum CEnum {
  CBar = 0,
  CBaz = 1,
  CFoo = 2,
};
#endif

通用表

GenericTable 类用于定义可搜索的通用表。TableGen 生成 C++ 代码来定义表条目,并生成基于主键搜索表的函数的声明和定义。要定义表,请定义一个父类为 GenericTable 且名称是条目全局表名称的记录。此类提供六个字段。

  • string FilterClass。该表将为每个派生自此类别的记录包含一个条目。

  • string FilterClassField。这是 FilterClass 的可选字段,应为 bit 类型。如果指定,则只有此字段为 true 的记录才会在表中具有相应的条目。如果此字段未包含在 Fields 列表中,则不会包含在生成的 C++ 字段中。

  • string CppTypeName。保存条目的表的 C++ 结构体/类类型的名称。如果未指定,则使用 FilterClass 名称。

  • list<string> Fields在收集的记录中包含表条目数据的字段名称列表。此列表的顺序决定了 C++ 初始化器中值的顺序。有关这些字段类型的详细信息,请参见下文。

  • list<string> PrimaryKey。构成主键的字段列表。

  • string PrimaryKeyName。生成的 C++ 函数的名称,该函数对主键执行查找。

  • bit PrimaryKeyEarlyOut。请参见下面的第三个示例。

  • bit PrimaryKeyReturnRange。设置为 1 时,修改查找函数的定义以返回结果范围,而不是指向对象的单个指针。当多个对象满足查找函数指定的条件时,此功能非常有用。目前,仅主查找函数支持此功能。有关更多详细信息,请参阅下面的第二个示例。

TableGen 尝试推断每个表字段的类型,以便它可以格式化发出的表中的 C++ 初始化器。它可以推断 bitbits<n>stringIntrinsicInstruction。这些可以在主键中使用。任何其他字段类型都必须显式指定;这可以通过下面的第二个示例所示的方式完成。此类字段不能在主键中使用。

字段类型的一个特殊情况与代码有关。任意代码由字符串表示,但必须作为不带引号的 C++ 初始化器发出。如果代码字段是使用代码字面量 ([{...}]) 定义的,则 TableGen 将知道在不带引号的情况下发出它。但是,如果它是使用字符串字面量或复杂字符串表达式定义的,则 TableGen 将不知道。在这种情况下,您可以通过在 GenericTable 记录中包含以下行来强制 TableGen 将该字段视为代码,其中 *xxx* 是代码字段名称。

string TypeOf_xxx = "code";

这是一个 TableGen 可以推断字段类型的示例。请注意,表条目记录是匿名的;条目记录的名称无关紧要。

def ATable : GenericTable {
  let FilterClass = "AEntry";
  let FilterClassField = "IsNeeded";
  let Fields = ["Str", "Val1", "Val2"];
  let PrimaryKey = ["Val1", "Val2"];
  let PrimaryKeyName = "lookupATableByValues";
}

class AEntry<string str, int val1, int val2, bit isNeeded> {
  string Str = str;
  bits<8> Val1 = val1;
  bits<10> Val2 = val2;
  bit IsNeeded = isNeeded;
}

def : AEntry<"Bob",   5, 3, 1>;
def : AEntry<"Carol", 2, 6, 1>;
def : AEntry<"Ted",   4, 4, 1>;
def : AEntry<"Alice", 4, 5, 1>;
def : AEntry<"Costa", 2, 1, 1>;
def : AEntry<"Dale",  2, 1, 0>;

这是生成的 C++ 代码。lookupATableByValues 的声明受 GET_ATable_DECL 保护,而定义受 GET_ATable_IMPL 保护。

#ifdef GET_ATable_DECL
const AEntry *lookupATableByValues(uint8_t Val1, uint16_t Val2);
#endif

#ifdef GET_ATable_IMPL
constexpr AEntry ATable[] = {
  { "Costa", 0x2, 0x1 }, // 0
  { "Carol", 0x2, 0x6 }, // 1
  { "Ted", 0x4, 0x4 }, // 2
  { "Alice", 0x4, 0x5 }, // 3
  { "Bob", 0x5, 0x3 }, // 4
  /* { "Dale", 0x2, 0x1 }, // 5 */ // We don't generate this line as `IsNeeded` is 0.
};

const AEntry *lookupATableByValues(uint8_t Val1, uint16_t Val2) {
  struct KeyType {
    uint8_t Val1;
    uint16_t Val2;
  };
  KeyType Key = { Val1, Val2 };
  auto Table = ArrayRef(ATable);
  auto Idx = std::lower_bound(Table.begin(), Table.end(), Key,
    [](const AEntry &LHS, const KeyType &RHS) {
      if (LHS.Val1 < RHS.Val1)
        return true;
      if (LHS.Val1 > RHS.Val1)
        return false;
      if (LHS.Val2 < RHS.Val2)
        return true;
      if (LHS.Val2 > RHS.Val2)
        return false;
      return false;
    });

  if (Idx == Table.end() ||
      Key.Val1 != Idx->Val1 ||
      Key.Val2 != Idx->Val2)
    return nullptr;
  return &*Idx;
}
#endif

ATable 中的表条目按 Val1 排序,并在每个 Val1 值内按 Val2 排序。这允许对表进行二分查找,该查找在查找函数中由 std::lower_bound 执行。查找函数返回对找到的表条目的引用,如果未找到条目,则返回空指针。如果表具有单个主键字段,该字段是整数且密集编号,则会生成直接查找而不是二分查找。

此示例包含一个 TableGen 无法推断其类型的字段。Kind 字段使用上面定义的枚举类型 CEnum。为了告知 TableGen 类型,从 GenericTable 派生的记录必须包含一个名为 TypeOf_field 的字符串字段,其中 *field* 是需要类型的字段的名称。

def CTable : GenericTable {
  let FilterClass = "CEntry";
  let Fields = ["Name", "Kind", "Encoding"];
  string TypeOf_Kind = "CEnum";
  let PrimaryKey = ["Encoding"];
  let PrimaryKeyName = "lookupCEntryByEncoding";
}

class CEntry<string name, CEnum kind, int enc> {
  string Name = name;
  CEnum Kind = kind;
  bits<16> Encoding = enc;
}

def : CEntry<"Apple", CFoo, 10>;
def : CEntry<"Pear",  CBaz, 15>;
def : CEntry<"Apple", CBar, 13>;

这是生成的 C++ 代码。

#ifdef GET_CTable_DECL
const CEntry *lookupCEntryByEncoding(uint16_t Encoding);
#endif

#ifdef GET_CTable_IMPL
constexpr CEntry CTable[] = {
  { "Apple", CFoo, 0xA }, // 0
  { "Apple", CBar, 0xD }, // 1
  { "Pear", CBaz, 0xF }, // 2
};

const CEntry *lookupCEntryByEncoding(uint16_t Encoding) {
  struct KeyType {
    uint16_t Encoding;
  };
  KeyType Key = { Encoding };
  auto Table = ArrayRef(CTable);
  auto Idx = std::lower_bound(Table.begin(), Table.end(), Key,
    [](const CEntry &LHS, const KeyType &RHS) {
      if (LHS.Encoding < RHS.Encoding)
        return true;
      if (LHS.Encoding > RHS.Encoding)
        return false;
      return false;
    });

  if (Idx == Table.end() ||
      Key.Encoding != Idx->Encoding)
    return nullptr;
  return &*Idx;
}

在上面的示例中,让我们添加一个编码与记录 CEntry<"Pear",  CBaz, 15> 相同的记录。

def CFoobar : CEnum;
def : CEntry<"Banana", CFoobar, 15>;

下面是新的生成的 CTable

#ifdef GET_Table_IMPL
constexpr CEntry Table[] = {
  { "Apple", CFoo, 0xA }, // 0
  { "Apple", CBar, 0xD }, // 1
  { "Banana", CFoobar, 0xF }, // 2
  { "Pear", CBaz, 0xF }, // 3
};

由于 Banana 在字典序中排在前面,因此在 CEntry 表中,名称为 Banana 的记录将出现在名称为 Pear 的记录之前。因此,即使在某些情况下正确的结果可能是名称为 Pear 的记录,lookupCEntryByEncoding 函数始终会返回指向名称为 Banana 的记录的指针。这种场景使得现有的查找函数不足,因为它们总是返回指向表中单个条目的指针,但实际上它应该返回一系列结果,因为多个条目都符合查找函数所寻找的条件。在这种情况下,需要修改查找函数的定义以返回结果范围,这可以通过设置 PrimaryKeyReturnRange 来完成。

def CTable : GenericTable {
  let FilterClass = "CEntry";
  let Fields = ["Name", "Kind", "Encoding"];
  string TypeOf_Kind = "CEnum";
  let PrimaryKey = ["Encoding"];
  let PrimaryKeyName = "lookupCEntryByEncoding";
  let PrimaryKeyReturnRange = true;
}

这是修改后的查找函数。

llvm::iterator_range<const CEntry *> lookupCEntryByEncoding(uint16_t Encoding) {
  struct KeyType {
    uint16_t Encoding;
  };
  KeyType Key = {Encoding};
  struct Comp {
    bool operator()(const CEntry &LHS, const KeyType &RHS) const {
      if (LHS.Encoding < RHS.Encoding)
        return true;
      if (LHS.Encoding > RHS.Encoding)
        return false;
      return false;
    }
    bool operator()(const KeyType &LHS, const CEntry &RHS) const {
      if (LHS.Encoding < RHS.Encoding)
        return true;
      if (LHS.Encoding > RHS.Encoding)
        return false;
      return false;
    }
  };
  auto Table = ArrayRef(Table);
  auto It = std::equal_range(Table.begin(), Table.end(), Key, Comp());
  return llvm::make_range(It.first, It.second);
}

新的查找函数将返回一个迭代器范围,其中第一个指针指向第一个结果,最后一个指针指向表中最后一个匹配的结果。但是,请注意,仅 PrimaryKeyName 支持发出修改后的定义。

当设置为 1 时,PrimaryKeyEarlyOut 字段会修改查找函数,使其测试主键的第一个字段,以确定它是否在收集的记录的主键范围内。如果不在,则该函数返回空指针,而不执行二分查找。这对于仅为较大枚举空间的某些元素提供数据的表很有用。主键的第一个字段必须是整数类型;它不能是字符串。

let PrimaryKeyEarlyOut = 1 添加到上面的 ATable

def ATable : GenericTable {
  let FilterClass = "AEntry";
  let Fields = ["Str", "Val1", "Val2"];
  let PrimaryKey = ["Val1", "Val2"];
  let PrimaryKeyName = "lookupATableByValues";
  let PrimaryKeyEarlyOut = 1;
}

导致查找函数按如下方式更改

const AEntry *lookupATableByValues(uint8_t Val1, uint16_t Val2) {
  if ((Val1 < 0x2) ||
      (Val1 > 0x5))
    return nullptr;

  struct KeyType {
  ...

我们可以构造两个具有相同 FilterClass 的 GenericTable,以便它们从相同的整体记录集中选择,但为它们分配不同的 FilterClassField 值,以便它们包含该类记录的不同子集。

例如,我们可以创建两个仅包含偶数或奇数记录的表。字段 IsEvenIsOdd 将不会包含在生成的 C++ 字段中,因为它们未包含在 Fields 列表中。

class EEntry<bits<8> value> {
  bits<8> Value = value;
  bit IsEven = !eq(!and(value, 1), 0);
  bit IsOdd = !not(IsEven);
}

foreach i = {1-10} in {
  def : EEntry<i>;
}

def EEntryEvenTable : GenericTable {
  let FilterClass = "EEntry";
  let FilterClassField = "IsEven";
  let Fields = ["Value"];
  let PrimaryKey = ["Value"];
  let PrimaryKeyName = "lookupEEntryEvenTableByValue";
}

def EEntryOddTable : GenericTable {
  let FilterClass = "EEntry";
  let FilterClassField = "IsOdd";
  let Fields = ["Value"];
  let PrimaryKey = ["Value"];
  let PrimaryKeyName = "lookupEEntryOddTableByValue";
}

生成的表是

constexpr EEntry EEntryEvenTable[] = {
  { 0x2 }, // 0
  { 0x4 }, // 1
  { 0x6 }, // 2
  { 0x8 }, // 3
  { 0xA }, // 4
};

constexpr EEntry EEntryOddTable[] = {
  { 0x1 }, // 0
  { 0x3 }, // 1
  { 0x5 }, // 2
  { 0x7 }, // 3
  { 0x9 }, // 4
};

搜索索引

SearchIndex 类用于为通用表定义额外的查找函数。要定义额外的函数,请定义一个父类为 SearchIndex 且名称是所需查找函数名称的记录。此类提供三个字段。

  • GenericTable Table。要接收另一个查找函数的表的名称。

  • list<string> Key。构成辅助键的字段列表。

  • bit EarlyOut。请参见通用表中的第三个示例。

这是一个添加到上述 CTable 的辅助键的示例。生成的函数基于 NameKind 字段查找条目。

def lookupCEntry : SearchIndex {
  let Table = CTable;
  let Key = ["Name", "Kind"];
}

使用 SearchIndex 会生成以下额外的 C++ 代码。

const CEntry *lookupCEntry(StringRef Name, unsigned Kind);

...

const CEntry *lookupCEntryByName(StringRef Name, unsigned Kind) {
  struct IndexType {
    const char * Name;
    unsigned Kind;
    unsigned _index;
  };
  static const struct IndexType Index[] = {
    { "APPLE", CBar, 1 },
    { "APPLE", CFoo, 0 },
    { "PEAR", CBaz, 2 },
  };

  struct KeyType {
    std::string Name;
    unsigned Kind;
  };
  KeyType Key = { Name.upper(), Kind };
  auto Table = ArrayRef(Index);
  auto Idx = std::lower_bound(Table.begin(), Table.end(), Key,
    [](const IndexType &LHS, const KeyType &RHS) {
      int CmpName = StringRef(LHS.Name).compare(RHS.Name);
      if (CmpName < 0) return true;
      if (CmpName > 0) return false;
      if ((unsigned)LHS.Kind < (unsigned)RHS.Kind)
        return true;
      if ((unsigned)LHS.Kind > (unsigned)RHS.Kind)
        return false;
      return false;
    });

  if (Idx == Table.end() ||
      Key.Name != Idx->Name ||
      Key.Kind != Idx->Kind)
    return nullptr;
  return &CTable[Idx->_index];
}