1 TableGen 程序员参考手册

1.1 简介

TableGen 的目的是基于源文件中的信息生成复杂的输出文件,这些源文件比输出文件更容易编写代码,并且随着时间的推移更容易维护和修改。信息以声明式风格编码,涉及类和记录,然后由 TableGen 处理。内部化的记录被传递给各种后端,后端从记录的子集中提取信息并生成一个或多个输出文件。这些输出文件通常是 C++ 的 .inc 文件,但也可能是后端开发人员需要的任何类型的文件。

本文档详细描述了 LLVM TableGen 工具。它面向使用 TableGen 为项目生成代码的程序员。如果您正在寻找简单的概述,请查看TableGen 概述。用于调用 TableGen 的各种 *-tblgen 命令在 tblgen Family - Description to C++ Code 中描述。

后端的一个例子是 RegisterInfo,它为特定的目标机器生成寄存器文件信息,供 LLVM 目标无关代码生成器使用。有关 LLVM TableGen 后端的描述,请参阅 TableGen 后端,有关编写新后端的指南,请参阅 TableGen 后端开发者指南

以下是后端可以执行的一些操作。

  • 为特定的目标机器生成寄存器文件信息。

  • 为目标生成指令定义。

  • 生成代码生成器用于将指令与中间表示 (IR) 节点匹配的模式。

  • 为 Clang 生成语义属性标识符。

  • 为 Clang 生成抽象语法树 (AST) 声明节点定义。

  • 为 Clang 生成 AST 语句节点定义。

1.1.1 概念

TableGen 源文件包含两个主要项:抽象记录具体记录。在本和其他 TableGen 文档中,抽象记录称为。(这些类与 C++ 类不同,并且不映射到 C++ 类。)此外,具体记录通常简称为记录,尽管有时术语记录同时指类和具体记录。区别在上下文中应该很清楚。

类和具体记录都有唯一的名称,可以是程序员选择的,也可以由 TableGen 生成的。与该名称关联的是一个字段列表,其中包含值,以及一个可选的父类列表(有时称为基类或超类)。字段是后端将处理的主要数据。请注意,TableGen 不为字段分配任何含义;含义完全取决于后端和包含这些后端输出的程序。

注意

术语“父类”可以指作为另一个类的父类的类,也可以指具体记录从中继承的类。术语的这种非标准用法出现是因为 TableGen 对类和具体记录的处理方式类似。

后端处理 TableGen 解析器构建的具体记录的某个子集,并发出输出文件。这些文件通常是 C++ 的 .inc 文件,程序通过包含这些文件来获取记录中的数据。但是,后端可以生成任何类型的输出文件。例如,它可以生成一个数据文件,其中包含标记了标识符和替换参数的消息。在像 LLVM 代码生成器这样复杂的用例中,可能有很多具体记录,其中一些记录可能具有数量惊人的字段,从而导致输出文件很大。

为了降低 TableGen 文件的复杂性,类用于抽象记录字段组。例如,一些类可以抽象机器寄存器文件的概念,而其他类可以抽象指令格式,还有一些类可以抽象单个指令。TableGen 允许任意的类层次结构,以便两个概念的抽象类可以共享第三个超类,该超类从两个原始概念中抽象出共同的“子概念”。

为了使类更有用,具体记录(或另一个类)可以将一个类请求为父类,并向其传递模板参数。这些模板参数可以在父类的字段中使用,以自定义方式初始化它们。也就是说,记录或类 A 可以使用一组模板参数请求父类 S,而记录或类 B 可以使用另一组参数请求 S。如果没有模板参数,则需要更多的类,每个模板参数组合都需要一个类。

类和具体记录都可以包含未初始化的字段。未初始化的“值”用问号 (?) 表示。类通常具有未初始化的字段,这些字段预计在这些类被具体记录继承时被填充。即便如此,具体记录的某些字段可能仍然未初始化。

TableGen 提供了 multiclass 以在一个地方收集一组记录定义。Multiclass 是一种宏,可以“调用”以一次定义多个具体记录。Multiclass 可以从其他 multiclass 继承,这意味着 multiclass 继承其父 multiclass 中的所有定义。

附录 C:示例记录 说明了 Intel X86 目标中的一个复杂记录及其简单的定义方式。

1.2 源文件

TableGen 源文件是纯 ASCII 文本文件。文件可以包含语句、注释和空行(请参阅词法分析)。TableGen 文件的标准文件扩展名是 .td

TableGen 文件可能会变得非常大,因此有一种包含机制,允许一个文件包含另一个文件的内容(请参阅包含文件)。这允许将大文件分解为小文件,并且还提供了一种简单的库机制,其中多个源文件可以包含相同的库文件。

TableGen 支持一个简单的预处理器,可用于条件化 .td 文件的部分内容。有关更多信息,请参阅预处理工具

1.3 词法分析

此处使用的词法和语法表示法旨在模仿 Python 的表示法。特别是,对于词法定义,产生式在字符级别上操作,并且元素之间没有隐含的空格。语法定义在标记级别上操作,因此标记之间有隐含的空格。

TableGen 支持 BCPL 风格的注释 (// ...) 和可嵌套的 C 风格注释 (/* ... */)。TableGen 还提供了简单的预处理工具

换页符可以在文件中自由使用,以便在打印文件以供审阅时产生分页符。

以下是基本标点符号标记

- + [ ] { } ( ) < > : ; . ... = ? #

1.3.1 字面量

数字字面量采用以下形式之一

TokInteger     ::=  DecimalInteger | HexInteger | BinInteger
DecimalInteger ::=  ["+" | "-"] ("0"..."9")+
HexInteger     ::=  "0x" ("0"..."9" | "a"..."f" | "A"..."F")+
BinInteger     ::=  "0b" ("0" | "1")+

请注意,与大多数语言(其中符号将被视为一元运算符)不同,DecimalInteger 标记包含可选的 +- 符号。

TableGen 有两种字符串字面量

TokString ::=  '"' (non-'"' characters and escapes) '"'
TokCode   ::=  "[{" (text not containing "}]") "}]"

TokCode 只是一个由 [{}] 分隔的多行字符串字面量。它可以跨行断开,并且换行符保留在字符串中。

当前实现接受以下转义序列

\\ \' \" \t \n

1.3.2 标识符

TableGen 具有区分大小写的名称和类似标识符的标记。

ualpha        ::=  "a"..."z" | "A"..."Z" | "_"
TokIdentifier ::=  ("0"..."9")* ualpha (ualpha | "0"..."9")*
TokVarName    ::=  "$" ualpha (ualpha |  "0"..."9")*

请注意,与大多数语言不同,TableGen 允许 TokIdentifier 以整数开头。如果存在歧义,则标记被解释为数字字面量而不是标识符。

TableGen 具有以下保留关键字,这些关键字不能用作标识符

assert     bit           bits          class         code
dag        def           dump          else          false
foreach    defm          defset        defvar        field
if         in            include       int           let
list       multiclass    string        then          true

警告

field 保留字已弃用,除非与 CodeEmitterGen 后端一起使用,在这种情况下,它用于区分普通记录字段和编码字段。

1.3.3 Bang 运算符

TableGen 提供了“bang 运算符”,它们用途广泛

BangOperator ::=  one of
                  !add         !and         !cast        !con         !dag
                  !div         !empty       !eq          !exists      !filter
                  !find        !foldl       !foreach     !ge          !getdagarg
                  !getdagname  !getdagop    !gt          !head        !if
                  !initialized !interleave  !isa         !le          !listconcat
                  !listflatten !listremove  !listsplat   !logtwo      !lt
                  !mul         !ne          !not         !or          !range
                  !repr        !setdagarg   !setdagname  !setdagop    !shl
                  !size        !sra         !srl         !strconcat   !sub
                  !subst       !substr      !tail        !tolower     !toupper
                  !xor

!cond 运算符的语法与其他 bang 运算符略有不同,因此单独定义

CondOperator ::=  !cond

有关每个 bang 运算符的描述,请参阅附录 A:Bang 运算符

1.3.4 包含文件

TableGen 具有包含机制。包含文件的内容在词法上替换 include 指令,然后像最初在主文件中一样进行解析。

IncludeDirective ::=  "include" TokString

主文件和包含文件的部分内容可以使用预处理器指令进行条件化。

PreprocessorDirective ::=  "#define" | "#ifdef" | "#ifndef"

1.4 类型

TableGen 语言是静态类型的,使用简单但完整的类型系统。类型用于检查错误、执行隐式转换以及帮助接口设计者约束允许的输入。每个值都必须具有关联的类型。

TableGen 支持低级类型(例如,bit)和高级类型(例如,dag)的混合。这种灵活性使您可以方便而紧凑地描述各种记录。

Type    ::=  "bit" | "int" | "string" | "dag" | "code"
            | "bits" "<" TokInteger ">"
            | "list" "<" Type ">"
            | ClassID
ClassID ::=  TokIdentifier
bit

bit 是一个布尔值,可以是 0 或 1。

int

int 类型表示一个简单的 64 位整数值,例如 5 或 -42。

string

string 类型表示任意长度的有序字符序列。

code

关键字 codestring 的别名,可用于指示作为代码的字符串值。

bits<n>

bits 类型是任意长度 n 的固定大小整数,被视为单独的位。可以单独访问这些位。此类型的字段对于表示指令操作码、寄存器号或寻址模式/寄存器/位移非常有用。可以单独或作为子字段设置字段的位。例如,在指令地址中,可以单独设置寻址模式、基址寄存器号和位移。

list<type>

此类型表示列表,其元素是尖括号中指定的类型。元素类型是任意的;它甚至可以是另一个列表类型。列表元素的索引从 0 开始。

dag

此类型表示节点的可嵌套有向无环图 (DAG)。每个节点都有一个运算符和零个或多个参数(或操作数)。参数可以是另一个 dag 对象,从而允许任意的节点和边树。例如,DAG 用于表示代码生成器指令选择算法使用的代码模式。有关更多详细信息,请参阅有向无环图 (DAG)

ClassID

在类型上下文中指定类名表示已定义值的类型必须是指定类的子类。这与 list 类型结合使用时非常有用;例如,将列表的元素约束为公共基类(例如,list<Register> 只能包含从 Register 类派生的定义)。ClassID 必须命名先前声明或定义的类。

1.5 值和表达式

在 TableGen 语句中,有很多上下文需要值。一个常见的例子是在记录的定义中,其中每个字段都由名称和可选值指定。TableGen 在构建值表达式时允许合理数量的不同形式。这些形式允许以对于应用程序自然的语法编写 TableGen 文件。

请注意,所有值都具有从一种类型转换为另一种类型的规则。例如,这些规则允许您将像 7 这样的值分配给 bits<4> 类型的实体。

Value         ::=  SimpleValue ValueSuffix*
                  | Value "#" [Value]
ValueSuffix   ::=  "{" RangeList "}"
                  | "[" SliceElements "]"
                  | "." TokIdentifier
RangeList     ::=  RangePiece ("," RangePiece)*
RangePiece    ::=  TokInteger
                  | TokInteger "..." TokInteger
                  | TokInteger "-" TokInteger
                  | TokInteger TokInteger
SliceElements ::=  (SliceElement ",")* SliceElement ","?
SliceElement  ::=  Value
                  | Value "..." Value
                  | Value "-" Value
                  | Value TokInteger

警告

RangePieceSliceElement 的特殊的最后形式是由于 “-” 包含在 TokInteger 中,因此 1-5 被词法分析为两个连续的标记,值分别为 1-5,而不是 “1”、“-” 和 “5”。不推荐使用连字符作为范围标点符号。

1.5.1 简单值

SimpleValue 有多种形式。

SimpleValue  ::=  SimpleValue1
                 | SimpleValue2
                 | SimpleValue3
                 | SimpleValue4
                 | SimpleValue5
                 | SimpleValue6
                 | SimpleValue7
                 | SimpleValue8
                 | SimpleValue9
SimpleValue1 ::=  TokInteger | TokString+ | TokCode

值可以是整数文字、字符串文字或代码文字。多个相邻的字符串文字像在 C/C++ 中一样连接;简单值是字符串的连接。代码文字变成字符串,然后与字符串无法区分。

SimpleValue2 ::=  "true" | "false"

truefalse 文字本质上是整数值 1 和 0 的语法糖。当布尔值用于字段初始化、位序列、if 语句等时,它们可以提高 TableGen 文件的可读性。解析后,这些文字将转换为整数。

注意

尽管 truefalse 是 1 和 0 的文字名称,但我们建议作为一种风格规则,您仅将它们用于布尔值。

SimpleValue3 ::=  "?"

问号表示未初始化的值。

SimpleValue4 ::=  "{" [ValueList] "}"
ValueList    ::=  ValueListNE
ValueListNE  ::=  Value ("," Value)*

此值表示位序列,可用于初始化 bits<n> 字段(注意大括号)。这样做时,这些值必须表示总共 n 位。

SimpleValue5 ::=  "[" ValueList "]" ["<" Type ">"]

此值是列表初始化器(注意方括号)。方括号中的值是列表的元素。可选的 Type 可用于指示特定的元素类型;否则,元素类型从给定的值推断。TableGen 通常可以推断类型,尽管有时当值为空列表 ([]) 时则不能。

SimpleValue6 ::=  "(" DagArg [DagArgList] ")"
DagArgList   ::=  DagArg ("," DagArg)*
DagArg       ::=  Value [":" TokVarName] | TokVarName

这表示 DAG 初始化器(注意圆括号)。第一个 DagArg 称为 DAG 的“运算符”,并且必须是记录。有关更多详细信息,请参阅有向无环图 (DAG)

SimpleValue7 ::=  TokIdentifier

结果值是由标识符命名的实体的值。此处描述了可能的标识符,但在阅读本指南的其余部分后,这些描述将更有意义。

  • class 的模板参数,例如 Bar 在以下用法中的使用

    class Foo <int Bar> {
      int Baz = Bar;
    }
    
  • classmulticlass 定义中的隐式模板参数 NAME(请参阅NAME)。

  • class 的本地字段,例如 Bar 在以下用法中的使用

    class Foo {
      int Bar = 5;
      int Baz = Bar;
    }
    
  • 记录定义的名称,例如 BarFoo 的定义中的使用

    def Bar : SomeClass {
      int X = 5;
    }
    
    def Foo {
      SomeClass Baz = Bar;
    }
    
  • 记录定义的本地字段,例如 Bar 在以下用法中的使用

    def Foo {
      int Bar = 5;
      int Baz = Bar;
    }
    

    可以以相同的方式访问从记录的父类继承的字段。

  • multiclass 的模板参数,例如 Bar 在以下用法中的使用

    multiclass Foo <int Bar> {
      def : SomeClass<Bar>;
    }
    
  • 使用 defvardefset 语句定义的变量。

  • foreach 的迭代变量,例如 i 在以下用法中的使用

    foreach i = 0...5 in
      def Foo#i;
    
SimpleValue8 ::=  ClassID "<" ArgValueList ">"

此形式创建一个新的匿名记录定义(就像由继承自给定类并具有给定模板参数的未命名 def 创建的一样;请参阅def),并且该值是该记录。可以使用后缀获取记录的字段;请参阅带后缀的值

以这种方式调用类可以提供简单的子例程工具。有关更多信息,请参阅将类用作子例程

SimpleValue9 ::=  BangOperator ["<" Type ">"] "(" ValueListNE ")"
                 | CondOperator "(" CondClause ("," CondClause)* ")"
CondClause   ::=  Value ":" Value

bang 运算符提供的功能是其他简单值所不具备的。除了 !cond 的情况外,bang 运算符都接受括在圆括号中的参数列表,并对这些参数执行某些功能,从而为该 bang 运算符生成一个值。!cond 运算符接受由冒号分隔的参数对列表。有关每个 bang 运算符的描述,请参阅附录 A:Bang 运算符

Type 仅被某些 bang 运算符接受,并且不能是 code

1.5.2 带后缀的值

上面描述的 SimpleValue 值可以使用某些后缀来指定。后缀的目的是获取主值的子值。以下是一些主要的可能后缀。

value{17}

最终值是整数的位 17(注意大括号)。

value{8...15}

最终值是整数的位 8–15。可以通过指定 {15...8} 来反转位的顺序。

value[i]

最终值是列表的元素 i(注意方括号)。换句话说,方括号充当列表上的下标运算符。仅当指定单个元素时才是这种情况。

value[i,]

最终值是一个列表,其中包含列表的单个元素 i。简而言之,一个包含单个元素的列表切片。

value[4...7,17,2...3,4]

最终值是一个新列表,它是列表的切片。新列表包含元素 4、5、6、7、17、2、3 和 4。元素可以多次包含且顺序任意。仅当指定多个元素时,才会得到此结果。

value[i,m...n,j,ls]

每个元素都可以是一个表达式(变量、bang 运算符)。mn 的类型应为 intijls 的类型应为 intlist<int>

value.field

最终值是指定记录中指定字段的值。

1.5.3 粘贴运算符

粘贴运算符 (#) 是 TableGen 表达式中唯一可用的中缀运算符。它允许您连接字符串或列表,但有一些不寻常的功能。

DefDefm 语句中指定记录名称时,可以使用粘贴运算符,在这种情况下,它必须构造一个字符串。如果操作数是未定义的名称 (TokIdentifier) 或全局 DefvarDefset 的名称,则它被视为字符的原文字符串。全局名称的值不被使用。

粘贴运算符可以用于所有其他值表达式中,在这种情况下,它可以构造字符串或列表。非常奇怪的是,但与前一种情况一致,如果右侧操作数是未定义的名称或全局名称,则它被视为字符的原文字符串。左侧操作数被正常处理。

值可以带有尾随粘贴运算符,在这种情况下,左侧操作数将连接到空字符串。

附录 B:粘贴运算符示例 介绍了粘贴运算符的行为示例。

1.6 语句

以下语句可以出现在 TableGen 源文件的顶层。

TableGenFile ::=  (Statement | IncludeDirective
                 | PreprocessorDirective)*
Statement    ::=  Assert | Class | Def | Defm | Defset | Deftype
                 | Defvar | Dump  | Foreach | If | Let | MultiClass

以下章节描述了每个顶层语句。

1.6.1 class — 定义抽象记录类

class 语句定义了一个抽象记录类,其他类和记录可以从中继承。

Class           ::=  "class" ClassID [TemplateArgList] RecordBody
TemplateArgList ::=  "<" TemplateArgDecl ("," TemplateArgDecl)* ">"
TemplateArgDecl ::=  Type TokIdentifier ["=" Value]

类可以通过“模板参数”列表进行参数化,模板参数的值可以在类的记录体中使用。这些模板参数在每次类被另一个类或记录继承时指定。

如果模板参数没有使用 = 分配默认值,则它是未初始化的(具有“值” ?),并且在类被继承时必须在模板参数列表中指定(必需参数)。如果参数被分配了默认值,则不需要在参数列表中指定(可选参数)。在声明中,所有必需的模板参数必须在任何可选参数之前。模板参数默认值从左到右进行评估。

RecordBody 在下面定义。它可以包含当前类继承的父类列表,以及字段定义和其他语句。当类 C 从另一个类 D 继承时,D 的字段有效地合并到 C 的字段中。

给定的类只能定义一次。如果以下任何一项为真,则 class 语句被认为定义了该类(RecordBody 元素在下面描述)。

您可以通过指定空的 TemplateArgList 和空的 RecordBody 来声明一个空类。这可以作为前向声明的一种受限形式。请注意,从前向声明的类派生的记录将不会从中继承任何字段,因为这些记录是在解析其声明时构建的,因此在类最终定义之前。

每个类都有一个隐式模板参数,名为 NAME(大写),它绑定到从该类继承的 DefDefm 的名称。如果该类由匿名记录继承,则名称未指定但全局唯一。

有关示例,请参见 示例:类和记录

1.6.1.1 记录体

记录体出现在类和记录定义中。记录体可以包含父类列表,该列表指定当前类或记录从中继承字段的类。这些类称为类或记录的父类。记录体还包括定义的主体,其中包含类或记录的字段规范。

RecordBody            ::=  ParentClassList Body
ParentClassList       ::=  [":" ParentClassListNE]
ParentClassListNE     ::=  ClassRef ("," ClassRef)*
ClassRef              ::=  (ClassID | MultiClassID) ["<" [ArgValueList] ">"]
ArgValueList          ::=  PostionalArgValueList [","] NamedArgValueList
PostionalArgValueList ::=  [Value {"," Value}*]
NamedArgValueList     ::=  [NameValue "=" Value {"," NameValue "=" Value}*]

包含 MultiClassIDParentClassList 仅在 defm 语句的类列表中有效。在这种情况下,ID 必须是 multiclass 的名称。

参数值可以用两种形式指定

  • 位置参数 (value)。该值被分配给相应位置的参数。对于 Foo<a0, a1>a0 将被分配给第一个参数,a1 将被分配给第二个参数。

  • 命名参数 (name=value)。该值被分配给具有指定名称的参数。对于 Foo<a=a0, b=a1>a0 将被分配给名称为 a 的参数,a1 将被分配给名称为 b 的参数。

必需参数也可以指定为命名参数。

请注意,无论以何种方式(命名或位置)指定,参数只能指定一次,并且位置参数应放在命名参数之前。

Body     ::=  ";" | "{" BodyItem* "}"
BodyItem ::=  Type TokIdentifier ["=" Value] ";"
             | "let" TokIdentifier ["{" RangeList "}"] "=" Value ";"
             | "defvar" TokIdentifier "=" Value ";"
             | Assert

主体中的字段定义指定要包含在类或记录中的字段。如果未指定初始值,则字段的值未初始化。必须指定类型;TableGen 不会从值中推断类型。

let 形式用于将字段重置为新值。这可以对直接在主体中定义的字段或从父类继承的字段执行。RangeList 可以指定以重置 bit<n> 字段中的某些位。

defvar 形式定义了一个变量,其值可以在主体内的其他值表达式中使用。该变量不是字段:它不会成为正在定义的类或记录的字段。提供变量是为了在处理主体时保存临时值。有关更多详细信息,请参见 记录体中的 Defvar

当类 C2 从类 C1 继承时,它会获得 C1 的所有字段定义。当这些定义合并到类 C2 中时,C2 传递给 C1 的任何模板参数都将替换到定义中。换句话说,由 C1 定义的抽象记录字段在合并到 C2 之前,会使用模板参数进行扩展。

1.6.2 def — 定义具体记录

def 语句定义了一个新的具体记录。

Def       ::=  "def" [NameValue] RecordBody
NameValue ::=  Value (parsed in a special mode)

名称值是可选的。如果指定,则以特殊模式解析,其中未定义的(无法识别的)标识符被解释为文字字符串。特别是,全局标识符被认为无法识别。这些包括由 defvardefset 定义的全局变量。记录名称可以为空字符串。

如果未给出名称值,则记录是匿名的。匿名记录的最终名称未指定但全局唯一。

如果 def 出现在 multiclass 语句中,则会发生特殊处理。有关详细信息,请参见下面的 multiclass 部分。

记录可以通过在其记录体的开头指定 ParentClassList 子句,从一个或多个类继承。父类中的所有字段都将添加到记录中。如果两个或多个父类提供相同的字段,则记录最终将具有最后一个父类的字段值。

作为一种特殊情况,记录的名称可以作为模板参数传递给该记录的父类。例如

class A <dag d> {
  dag the_dag = d;
}

def rec1 : A<(ops rec1)>;

DAG (ops rec1) 作为模板参数传递给类 A。请注意,DAG 包括正在定义的记录 rec1

创建新记录的步骤有些复杂。请参见 记录是如何构建的

有关示例,请参见 示例:类和记录

1.6.3 示例:类和记录

这是一个简单的 TableGen 文件,其中包含一个类和两个记录定义。

class C {
  bit V = true;
}

def X : C;
def Y : C {
  let V = false;
  string Greeting = "Hello!";
}

首先,定义抽象类 C。它有一个名为 V 的字段,该字段是一个初始化为 true 的位。

接下来,定义了两个记录,它们派生自类 C;也就是说,以 C 作为它们的父类。因此,它们都继承了 V 字段。记录 Y 还定义了另一个字符串字段 Greeting,该字段初始化为 "Hello!"。此外,Y 覆盖了继承的 V 字段,将其设置为 false。

类对于将多个记录的共同特征隔离在一个地方很有用。类可以将公共字段初始化为默认值,但是从该类继承的记录可以覆盖默认值。

TableGen 支持参数化类以及非参数化类的定义。参数化类指定变量声明列表,这些声明可以可选地具有默认值,这些默认值在将类指定为另一个类或记录的父类时绑定。

class FPFormat <bits<3> val> {
  bits<3> Value = val;
}

def NotFP      : FPFormat<0>;
def ZeroArgFP  : FPFormat<1>;
def OneArgFP   : FPFormat<2>;
def OneArgFPRW : FPFormat<3>;
def TwoArgFP   : FPFormat<4>;
def CompareFP  : FPFormat<5>;
def CondMovFP  : FPFormat<6>;
def SpecialFP  : FPFormat<7>;

FPFormat 类的目的是充当一种枚举类型。它提供了一个字段 Value,它保存一个 3 位数字。其模板参数 val 用于设置 Value 字段。八个记录中的每一个都定义了 FPFormat 作为其父类。枚举值在尖括号中作为模板参数传递。每个记录都将继承具有适当枚举值的 Value 字段。

这是一个更复杂的带有模板参数的类示例。首先,我们定义一个类似于上面 FPFormat 类的类。它接受一个模板参数,并使用它来初始化一个名为 Value 的字段。然后,我们定义四个记录,它们继承了具有四个不同整数值的 Value 字段。

class ModRefVal <bits<2> val> {
  bits<2> Value = val;
}

def None   : ModRefVal<0>;
def Mod    : ModRefVal<1>;
def Ref    : ModRefVal<2>;
def ModRef : ModRefVal<3>;

这有点人为设计,但是假设我们想独立检查 Value 字段的两位。我们可以定义一个类,该类接受 ModRefVal 记录作为模板参数,并将其值拆分为两个字段,每个字段一位。然后,我们可以定义从 ModRefBits 继承的记录,从而从中获取两个字段,每个字段对应于作为模板参数传递的 ModRefVal 记录中的一位。

class ModRefBits <ModRefVal mrv> {
  // Break the value up into its bits, which can provide a nice
  // interface to the ModRefVal values.
  bit isMod = mrv.Value{0};
  bit isRef = mrv.Value{1};
}

// Example uses.
def foo   : ModRefBits<Mod>;
def bar   : ModRefBits<Ref>;
def snork : ModRefBits<ModRef>;

这说明了一个类如何被定义为重组另一个类中的字段,从而隐藏另一个类的内部表示。

在示例上运行 llvm-tblgen 会打印以下定义

def bar {      // Value
  bit isMod = 0;
  bit isRef = 1;
}
def foo {      // Value
  bit isMod = 1;
  bit isRef = 0;
}
def snork {      // Value
  bit isMod = 1;
  bit isRef = 1;
}

1.6.4 let — 覆盖类或记录中的字段

let 语句收集一组字段值(有时称为绑定),并将它们应用于 let 作用域内的语句定义的所有类和记录。

Let     ::=   "let" LetList "in" "{" Statement* "}"
            | "let" LetList "in" Statement
LetList ::=  LetItem ("," LetItem)*
LetItem ::=  TokIdentifier ["<" RangeList ">"] "=" Value

let 语句建立一个作用域,该作用域是花括号中的语句序列或没有花括号的单个语句。LetList 中的绑定应用于该作用域中的语句。

LetList 中的字段名称必须命名语句中定义的类和记录继承的类中的字段。字段值在记录从其父类继承所有字段之后应用于类和记录。因此,let 的作用是覆盖继承的字段值。let 无法覆盖模板参数的值。

当需要在多个记录中覆盖一些字段时,顶层 let 语句通常很有用。以下是两个示例。请注意,let 语句可以嵌套。

let isTerminator = true, isReturn = true, isBarrier = true, hasCtrlDep = true in
  def RET : I<0xC3, RawFrm, (outs), (ins), "ret", [(X86retflag 0)]>;

let isCall = true in
  // All calls clobber the non-callee saved registers...
  let Defs = [EAX, ECX, EDX, FP0, FP1, FP2, FP3, FP4, FP5, FP6, ST0,
              MM0, MM1, MM2, MM3, MM4, MM5, MM6, MM7, XMM0, XMM1, XMM2,
              XMM3, XMM4, XMM5, XMM6, XMM7, EFLAGS] in {
    def CALLpcrel32 : Ii32<0xE8, RawFrm, (outs), (ins i32imm:$dst, variable_ops),
                           "call\t${dst:call}", []>;
    def CALL32r     : I<0xFF, MRM2r, (outs), (ins GR32:$dst, variable_ops),
                        "call\t{*}$dst", [(X86call GR32:$dst)]>;
    def CALL32m     : I<0xFF, MRM2m, (outs), (ins i32mem:$dst, variable_ops),
                        "call\t{*}$dst", []>;
  }

请注意,顶层 let 不会覆盖类或记录本身中定义的字段。

1.6.5 multiclass — 定义多个记录

虽然带有模板参数的类是分解多个记录之间共性的好方法,但 multiclass 允许一种方便的方法一次定义多个记录。例如,考虑一个 3 地址指令架构,其指令有两种格式:reg = reg op regreg = reg op imm (例如,SPARC)。我们希望在一个地方指定这两种常见格式存在,然后在另一个地方指定所有操作是什么。multiclassdefm 语句实现了此目标。您可以将 multiclass 视为宏或模板,它扩展为多个记录。

MultiClass          ::=  "multiclass" TokIdentifier [TemplateArgList]
                         ParentClassList
                         "{" MultiClassStatement+ "}"
MultiClassID        ::=  TokIdentifier
MultiClassStatement ::=  Assert | Def | Defm | Defvar | Foreach | If | Let

与常规类一样,multiclass 具有名称,并且可以接受模板参数。multiclass 可以从其他 multiclass 继承,这会导致其他 multiclass 被扩展并贡献于继承 multiclass 中的记录定义。multiclass 的主体包含一系列语句,这些语句使用 DefDefm 定义记录。此外,DefvarForeachLet 语句可以用于分解更多公共元素。IfAssert 语句也可以使用。

同样与常规类一样,multiclass 具有隐式模板参数 NAME(请参见 NAME)。当在 multiclass 中定义命名(非匿名)记录,并且记录的名称不包含模板参数 NAME 的使用时,这种使用会自动前置到名称。也就是说,以下在 multiclass 内部是等效的

def Foo ...
def NAME # Foo ...

在 multiclass 中定义的记录在 multiclass 通过 multiclass 定义之外的 defm 语句“实例化”或“调用”时创建。multiclass 中的每个 def 语句都会生成一个记录。与顶层 def 语句一样,这些定义可以从多个父类继承。

有关示例,请参见 示例:multiclass 和 defm

1.6.6 defm — 调用 multiclass 以定义多个记录

一旦定义了 multiclass,您就可以使用 defm 语句“调用”它们,并处理这些 multiclass 中的多个记录定义。这些记录定义由 multiclass 中的 def 语句指定,并间接由 defm 语句指定。

Defm ::=  "defm" [NameValue] ParentClassList ";"

可选的 NameValue 以与 def 的名称相同的方式形成。ParentClassList 是一个冒号,后跟至少一个 multiclass 和任意数量的常规类的列表。multiclass 必须在常规类之前。请注意,defm 没有主体。

此语句实例化所有指定的 multiclass 中定义的所有记录,直接通过 def 语句或间接通过 defm 语句。这些记录还接收在父类列表中包含的任何常规类中定义的字段。这对于向 defm 创建的所有记录添加一组通用字段非常有用。

名称以与 def 使用的相同特殊模式解析。如果未包含名称,则提供未指定但全局唯一的名称。也就是说,以下示例最终会得到不同的名称

defm    : SomeMultiClass<...>;   // A globally unique name.
defm "" : SomeMultiClass<...>;   // An empty name.

defm 语句可以在 multiclass 主体中使用。当这种情况发生时,第二种变体等效于

defm NAME : SomeMultiClass<...>;

更一般地,当 defm 出现在 multiclass 中,并且其名称不包含隐式模板参数 NAME 的使用时,则 NAME 将自动前置。也就是说,以下在 multiclass 内部是等效的

defm Foo        : SomeMultiClass<...>;
defm NAME # Foo : SomeMultiClass<...>;

有关示例,请参见 示例:multiclass 和 defm

1.6.7 示例:multiclass 和 defm

这是一个使用 multiclassdefm 的简单示例。考虑一个 3 地址指令架构,其指令有两种格式:reg = reg op regreg = reg op imm (立即数)。SPARC 就是这种架构的一个例子。

def ops;
def GPR;
def Imm;
class inst <int opc, string asmstr, dag operandlist>;

multiclass ri_inst <int opc, string asmstr> {
  def _rr : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"),
                   (ops GPR:$dst, GPR:$src1, GPR:$src2)>;
  def _ri : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"),
                   (ops GPR:$dst, GPR:$src1, Imm:$src2)>;
}

// Define records for each instruction in the RR and RI formats.
defm ADD : ri_inst<0b111, "add">;
defm SUB : ri_inst<0b101, "sub">;
defm MUL : ri_inst<0b100, "mul">;

ri_inst multiclass 的每次使用都定义了两个记录,一个带有 _rr 后缀,另一个带有 _ri。回想一下,使用 multiclass 的 defm 的名称被前置到该 multiclass 中定义的记录的名称。因此,生成的定义被命名为

ADD_rr, ADD_ri
SUB_rr, SUB_ri
MUL_rr, MUL_ri

如果没有 multiclass 功能,则指令必须按如下方式定义。

def ops;
def GPR;
def Imm;
class inst <int opc, string asmstr, dag operandlist>;

class rrinst <int opc, string asmstr>
  : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"),
           (ops GPR:$dst, GPR:$src1, GPR:$src2)>;

class riinst <int opc, string asmstr>
  : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"),
           (ops GPR:$dst, GPR:$src1, Imm:$src2)>;

// Define records for each instruction in the RR and RI formats.
def ADD_rr : rrinst<0b111, "add">;
def ADD_ri : riinst<0b111, "add">;
def SUB_rr : rrinst<0b101, "sub">;
def SUB_ri : riinst<0b101, "sub">;
def MUL_rr : rrinst<0b100, "mul">;
def MUL_ri : riinst<0b100, "mul">;

defm 可以在 multiclass 中使用,以“调用”其他 multiclass 并在当前 multiclass 中定义的记录之外创建这些 multiclass 中定义的记录。在以下示例中,basic_sbasic_p multiclass 包含引用 basic_r multiclass 的 defm 语句。basic_r multiclass 仅包含 def 语句。

class Instruction <bits<4> opc, string Name> {
  bits<4> opcode = opc;
  string name = Name;
}

multiclass basic_r <bits<4> opc> {
  def rr : Instruction<opc, "rr">;
  def rm : Instruction<opc, "rm">;
}

multiclass basic_s <bits<4> opc> {
  defm SS : basic_r<opc>;
  defm SD : basic_r<opc>;
  def X : Instruction<opc, "x">;
}

multiclass basic_p <bits<4> opc> {
  defm PS : basic_r<opc>;
  defm PD : basic_r<opc>;
  def Y : Instruction<opc, "y">;
}

defm ADD : basic_s<0xf>, basic_p<0xf>;

最终的 defm 创建了以下记录,五个来自 basic_s multiclass,五个来自 basic_p multiclass

ADDSSrr, ADDSSrm
ADDSDrr, ADDSDrm
ADDX
ADDPSrr, ADDPSrm
ADDPDrr, ADDPDrm
ADDY

defm 语句,无论是在顶层还是在 multiclass 中,都可以从常规类以及 multiclass 继承。规则是常规类必须在 multiclass 之后列出,并且必须至少有一个 multiclass。

class XD {
  bits<4> Prefix = 11;
}
class XS {
  bits<4> Prefix = 12;
}
class I <bits<4> op> {
  bits<4> opcode = op;
}

multiclass R {
  def rr : I<4>;
  def rm : I<2>;
}

multiclass Y {
  defm SS : R, XD;    // First multiclass R, then regular class XD.
  defm SD : R, XS;
}

defm Instr : Y;

此示例将创建四个记录,此处按字母顺序显示,并带有其字段。

def InstrSDrm {
  bits<4> opcode = { 0, 0, 1, 0 };
  bits<4> Prefix = { 1, 1, 0, 0 };
}

def InstrSDrr {
  bits<4> opcode = { 0, 1, 0, 0 };
  bits<4> Prefix = { 1, 1, 0, 0 };
}

def InstrSSrm {
  bits<4> opcode = { 0, 0, 1, 0 };
  bits<4> Prefix = { 1, 0, 1, 1 };
}

def InstrSSrr {
  bits<4> opcode = { 0, 1, 0, 0 };
  bits<4> Prefix = { 1, 0, 1, 1 };
}

也可以在 multiclass 内部使用 let 语句,从而提供另一种从记录中分解共性的方法,尤其是在使用多个级别的 multiclass 实例化时。

multiclass basic_r <bits<4> opc> {
  let Predicates = [HasSSE2] in {
    def rr : Instruction<opc, "rr">;
    def rm : Instruction<opc, "rm">;
  }
  let Predicates = [HasSSE3] in
    def rx : Instruction<opc, "rx">;
}

multiclass basic_ss <bits<4> opc> {
  let IsDouble = false in
    defm SS : basic_r<opc>;

  let IsDouble = true in
    defm SD : basic_r<opc>;
}

defm ADD : basic_ss<0xf>;

1.6.8 defset — 创建定义集

defset 语句用于将一组记录收集到全局记录列表中。

Defset ::=  "defset" Type TokIdentifier "=" "{" Statement* "}"

在花括号内通过 defdefm 定义的所有记录都照常定义,并且它们也收集在给定名称 (TokIdentifier) 的全局列表中。

指定的类型必须是 list<class>,其中 class 是某个记录类。defset 语句为其语句建立一个作用域。在 defset 的作用域内定义不是 class 类型的记录是错误的。

defset 语句可以嵌套。内部 defset 将记录添加到其自己的集合中,并且所有这些记录也添加到外部集合中。

使用 ClassID<...> 语法在初始化表达式中创建的匿名记录不会收集在集合中。

1.6.9 deftype — 定义类型

deftype 语句定义一个类型。该类型可以在后面的语句中通篇使用。

Deftype ::=  "deftype" TokIdentifier "=" Type ";"

= 左侧的标识符被定义为类型名称,其实际类型由 = 右侧的类型表达式给出。

目前,仅支持原始类型和类型别名作为源类型,并且 deftype 语句只能出现在顶层。

1.6.10 defvar — 定义变量

defvar 语句定义一个全局变量。它的值可以在后面的语句中通篇使用。

Defvar ::=  "defvar" TokIdentifier "=" Value ";"

= 左侧的标识符被定义为全局变量,其值由 = 右侧的值表达式给出。变量的类型是自动推断的。

一旦定义了变量,就不能将其设置为另一个值。

在顶层 foreach 中定义的变量在每次循环迭代结束时超出作用域,因此它们在一个迭代中的值在下一个迭代中不可用。以下 defvar 将不起作用

defvar i = !add(i, 1);

变量也可以在记录体中使用 defvar 定义。有关更多详细信息,请参见 记录体中的 Defvar

1.6.11 foreach — 迭代语句序列

foreach 语句迭代一系列语句,使变量在一系列值上变化。

Foreach         ::=  "foreach" ForeachIterator "in" "{" Statement* "}"
                    | "foreach" ForeachIterator "in" Statement
ForeachIterator ::=  TokIdentifier "=" ("{" RangeList "}" | RangePiece | Value)

foreach 的主体是花括号中的语句序列或没有花括号的单个语句。对于范围列表、范围片段或单个值中的每个值,语句都会重新评估一次。在每次迭代中,TokIdentifier 变量设置为该值,并可以在语句中使用。

语句列表建立了一个内部作用域。 foreach 的局部变量在每次循环迭代结束时超出作用域,因此它们的值不会从一次迭代传递到下一次迭代。 Foreach 循环可以嵌套。

foreach i = [0, 1, 2, 3] in {
  def R#i : Register<...>;
  def F#i : Register<...>;
}

此循环定义了名为 R0R1R2R3 的记录,以及 F0F1F2F3

1.6.12 dump — 将消息打印到 stderr

dump 语句将输入字符串打印到标准错误输出。它旨在用于调试目的。

  • 在顶层,消息会立即打印。

  • 在记录/类/多类中,dump 在包含记录的每个实例化点进行评估。

Dump ::=  "dump" Value ";"

Value 是任意字符串表达式。例如,它可以与 !repr 结合使用,以调查传递给多类的值

multiclass MC<dag s> {
  dump "s = " # !repr(s);
}

1.6.13 if — 根据测试选择语句

if 语句允许根据表达式的值选择两个语句组中的一个。

If     ::=  "if" Value "then" IfBody
           | "if" Value "then" IfBody "else" IfBody
IfBody ::=  "{" Statement* "}" | Statement

评估值表达式。如果它评估为 true(与 bang 运算符使用的意义相同),则处理 then 保留字后面的语句。否则,如果存在 else 保留字,则处理 else 后面的语句。如果值为 false 且没有 else 分支,则不处理任何语句。

由于 then 语句周围的大括号是可选的,因此此语法规则通常与“悬空 else”子句存在歧义,并且它以通常的方式解决:在类似 if v1 then if v2 then {...} else {...} 的情况下,else 与内部 if 而不是外部 if 关联。

if 的 then 和 else 分支的 IfBody 建立了一个内部作用域。在主体中定义的任何 defvar 变量在主体完成时超出作用域(有关更多详细信息,请参阅记录主体中的 Defvar)。

if 语句也可以在记录 Body 中使用。

1.6.14 assert — 检查条件是否为真

assert 语句检查布尔条件以确保其为真,如果不是,则打印错误消息。

Assert ::=  "assert" Value "," Value ";"

第一个 Value 是一个布尔条件。如果为真,则语句不执行任何操作。如果条件为假,则打印非致命错误消息。第二个 Value 是一条消息,可以是任意字符串表达式。它作为注释包含在错误消息中。assert 语句的确切行为取决于其位置。

  • 在顶层,断言会立即检查。

  • 在记录定义中,语句被保存,并且在记录完全构建后检查所有断言。

  • 在类定义中,断言被保存并由从该类继承的所有子类和记录继承。然后在记录完全构建时检查断言。

  • 在多类定义中,断言与多类的其他组件一起保存,然后在每次使用 defm 实例化多类时进行检查。

在 TableGen 文件中使用断言可以简化 TableGen 后端中的记录检查。以下是在两个类定义中使用 assert 的示例。

class PersonName<string name> {
  assert !le(!size(name), 32), "person name is too long: " # name;
  string Name = name;
}

class Person<string name, int age> : PersonName<name> {
  assert !and(!ge(age, 1), !le(age, 120)), "person age is invalid: " # age;
  int Age = age;
}

def Rec20 : Person<"Donald Knuth", 60> {
  ...
}

1.7 其他详细信息

1.7.1 有向无环图 (DAG)

可以使用 dag 数据类型在 TableGen 中直接表示有向无环图。 DAG 节点由运算符和零个或多个参数(或操作数)组成。每个参数可以是任何所需类型。通过使用另一个 DAG 节点作为参数,可以构建 DAG 节点的任意图。

dag 实例的语法是

( 运算符 参数1, 参数2,)

运算符必须存在并且必须是记录。可以有零个或多个参数,以逗号分隔。运算符和参数可以有三种格式。

格式

含义

参数值

:名称

参数值和关联名称

名称

具有未设置(未初始化)值的参数名称

可以是任何 TableGen 值。 名称(如果存在)必须是 TokVarName,它以美元符号 ($) 开头。名称的目的是用特定含义标记 DAG 中的运算符或参数,或者将一个 DAG 中的参数与另一个 DAG 中同名的参数关联起来。

以下 bang 运算符对于使用 DAG 非常有用:!con!dag!empty!foreach!getdagarg!getdagname!getdagop!setdagarg!setdagname!setdagop!size

1.7.2 记录主体中的 Defvar

除了定义全局变量外,defvar 语句还可以在类或记录定义的 Body 内部使用,以定义局部变量。 classmulticlass 的模板参数可以在值表达式中使用。变量的作用域从 defvar 语句扩展到主体的末尾。在其作用域内,它不能设置为不同的值。 defvar 语句也可以在 foreach 的语句列表中使用,这会建立一个作用域。

内部作用域中名为 V 的变量会遮蔽(隐藏)外部作用域中的任何变量 V。 特别是,有几种情况

  • 记录主体中的 V 遮蔽全局 V

  • 记录主体中的 V 遮蔽模板参数 V

  • 模板参数中的 V 遮蔽全局 V

  • foreach 语句列表中的 V 遮蔽周围记录或全局作用域中的任何 V

foreach 中定义的变量在每次循环迭代结束时超出作用域,因此它们在一个迭代中的值在下一次迭代中不可用。以下 defvar 将不起作用

defvar i = !add(i, 1)

1.7.3 记录是如何构建的

TableGen 在构建记录时会执行以下步骤。类只是抽象记录,因此也经历相同的步骤。

  1. 构建记录名称 (NameValue) 并创建一个空记录。

  2. 从左到右解析 ParentClassList 中的父类,从上到下访问每个父类的祖先类。

  1. 将父类的字段添加到记录中。

  2. 将模板参数代入这些字段。

  3. 将父类添加到记录的继承类列表中。

  1. 将任何顶层 let 绑定应用于记录。 回想一下,顶层绑定仅适用于继承的字段。

  2. 解析记录的主体。

  • 将任何字段添加到记录中。

  • 根据局部 let 语句修改字段的值。

  • 定义任何 defvar 变量。

  1. 遍历所有字段以解析任何字段间引用。

  2. 将记录添加到最终记录列表中。

由于字段之间的引用在应用 let 绑定(步骤 3)后解析(步骤 5),因此 let 语句具有不同寻常的力量。 例如

class C <int x> {
  int Y = x;
  int Yplus1 = !add(Y, 1);
  int xplus1 = !add(x, 1);
}

let Y = 10 in {
  def rec1 : C<5> {
  }
}

def rec2 : C<5> {
  let Y = 10;
}

在这两种情况下,一种是使用顶层 let 来绑定 Y,另一种是使用局部 let 来做同样的事情,结果都是

def rec1 {      // C
  int Y = 10;
  int Yplus1 = 11;
  int xplus1 = 6;
}
def rec2 {      // C
  int Y = 10;
  int Yplus1 = 11;
  int xplus1 = 6;
}

Yplus1 为 11,因为 let Y!add(Y, 1) 解析之前执行。 请明智地使用这种力量。

1.8 将类用作子程序

简单值中所述,可以在表达式中调用类并传递模板参数。 这会导致 TableGen 创建一个新的匿名记录,该记录从该类继承。 像往常一样,该记录接收类中定义的所有字段。

此功能可以用作简单的子程序工具。 该类可以使用模板参数来定义各种变量和字段,这些变量和字段最终会出现在匿名记录中。 然后可以在调用类的表达式中检索这些字段,如下所示。 假设字段 ret 包含子程序的最终值。

int Result = ... CalcValue<arg>.ret ...;

CalcValue 类使用模板参数 arg 调用。 它计算 ret 字段的值,然后在 Result 字段的初始化中的“调用点”检索该值。 在此示例中创建的匿名记录除了携带结果值外,没有其他用途。

这是一个实际示例。 类 isValidSize 确定指定的字节数是否表示有效的数据大小。 位 ret 设置为适当的值。 字段 ValidSize 通过使用数据大小调用 isValidSize 并从生成的匿名记录中检索 ret 字段来获取其初始值。

class isValidSize<int size> {
  bit ret = !cond(!eq(size,  1): 1,
                  !eq(size,  2): 1,
                  !eq(size,  4): 1,
                  !eq(size,  8): 1,
                  !eq(size, 16): 1,
                  true: 0);
}

def Data1 {
  int Size = ...;
  bit ValidSize = isValidSize<Size>.ret;
}

1.9 预处理功能

TableGen 中嵌入的预处理器仅用于简单的条件编译。 它支持以下指令,这些指令以某种非正式的方式指定。

LineBegin              ::=  beginning of line
LineEnd                ::=  newline | return | EOF
WhiteSpace             ::=  space | tab
CComment               ::=  "/*" ... "*/"
BCPLComment            ::=  "//" ... LineEnd
WhiteSpaceOrCComment   ::=  WhiteSpace | CComment
WhiteSpaceOrAnyComment ::=  WhiteSpace | CComment | BCPLComment
MacroName              ::=  ualpha (ualpha | "0"..."9")*
PreDefine              ::=  LineBegin (WhiteSpaceOrCComment)*
                            "#define" (WhiteSpace)+ MacroName
                            (WhiteSpaceOrAnyComment)* LineEnd
PreIfdef               ::=  LineBegin (WhiteSpaceOrCComment)*
                            ("#ifdef" | "#ifndef") (WhiteSpace)+ MacroName
                            (WhiteSpaceOrAnyComment)* LineEnd
PreElse                ::=  LineBegin (WhiteSpaceOrCComment)*
                            "#else" (WhiteSpaceOrAnyComment)* LineEnd
PreEndif               ::=  LineBegin (WhiteSpaceOrCComment)*
                            "#endif" (WhiteSpaceOrAnyComment)* LineEnd

MacroName 可以在 TableGen 文件中的任何位置定义。 该名称没有值; 只能测试它是否已定义。

宏测试区域以 #ifdef#ifndef 指令开头。 如果宏名称已定义 (#ifdef) 或未定义 (#ifndef),则处理指令和相应的 #else#endif 之间的源代码。 如果测试失败但存在 #else 子句,则处理 #else#endif 之间的源代码。 如果测试失败且没有 #else 子句,则不处理测试区域中的任何源代码。

测试区域可以嵌套,但它们必须正确嵌套。 在文件中启动的区域必须在该文件中结束; 也就是说,必须在同一文件中包含其 #endif

可以使用 *-tblgen 命令行上的 -D 选项在外部定义 MacroName

llvm-tblgen self-reference.td -Dmacro1 -Dmacro3

1.10 附录 A:Bang 运算符

Bang 运算符在值表达式中充当函数。 bang 运算符接受一个或多个参数,对其进行操作,并产生结果。 如果运算符产生布尔结果,则结果值对于 true 为 1,对于 false 为 0。 当运算符测试布尔参数时,它将 0 解释为 false,将非 0 解释为 true。

警告

!getop!setop bang 运算符已被弃用,建议使用 !getdagop!setdagop

!add(a, b, ...)

此运算符将 ab 等相加,并产生总和。

!and(a, b, ...)

此运算符对 ab 等执行按位 AND 运算,并产生结果。 如果所有参数均为 0 或 1,则可以执行逻辑 AND 运算。 当最左边的操作数为 0 时,此运算符短路为 0。

!cast<type>(a)

此运算符对 a 执行强制转换并产生结果。 如果 a 不是字符串,则执行直接强制转换,例如在 intbit 之间,或在记录类型之间。 这允许将记录强制转换为类。 如果将记录强制转换为 string,则生成记录的名称。

如果 a 是字符串,则将其视为记录名称并在所有已定义记录的列表中查找。 生成的记录应为指定的 type

例如,如果 !cast<type>(name) 出现在多类定义中,或出现在多类定义内部实例化的类中,并且 name 不引用多类的任何模板参数,则该名称的记录必须已在源文件中较早实例化。 如果 name 确实引用了模板参数,则查找将延迟到实例化多类的 defm 语句(或者稍后,如果 defm 出现在另一个多类中,并且 name 引用的内部多类的模板参数被本身包含对外部多类的模板参数的引用的值替换)。

如果 a 的类型与 type 不匹配,则 TableGen 会引发错误。

!con(a, b, ...)

此运算符连接 DAG 节点 ab 等。 它们的操作必须相等。

!con((op a1:$name1, a2:$name2), (op b1:$name3))

产生 DAG 节点 (op a1:$name1, a2:$name2, b1:$name3)

!cond(cond1 : val1, cond2 : val2, ..., condn : valn)

此运算符测试 cond1,如果结果为 true,则返回 val1。 如果为 false,则运算符测试 cond2,如果结果为 true,则返回 val2。 依此类推。 如果没有条件为 true,则报告错误。

此示例生成整数的符号字

!cond(!lt(x, 0) : "negative", !eq(x, 0) : "zero", true : "positive")
!dag(op, arguments, names)

此运算符使用给定的运算符和参数创建 DAG 节点。 argumentsnames 参数必须是长度相等的列表或未初始化 (?)。 names 参数的类型必须为 list<string>

由于类型系统的限制,arguments 必须是具有共同类型的项目列表。 在实践中,这意味着它们应具有相同的类型或为具有共同父类的记录。 混合 dag 和非 dag 项是不可能的。 但是,可以使用 ?

示例: !dag(op, [a1, a2, ?], ["name1", "name2", "name3"]) 产生 (op a1-value:$name1, a2-value:$name2, ?:$name3)

!div(a, b)

此运算符执行 a 除以 b 的有符号除法,并产生商。 除以 0 会产生错误。 INT64_MIN 除以 -1 会产生错误。

!empty(a)

如果字符串、列表或 DAG a 为空,则此运算符产生 1; 否则为 0。 如果 DAG 没有参数,则 DAG 为空; 运算符不计数。

!eq( a, b)

如果 a 等于 b,则此运算符产生 1; 否则为 0。 参数必须是 bitbitsintstring 或记录值。 使用 !cast<string> 比较其他类型的对象。

!exists<type>(name)

如果存在名称为 name 的给定 type 的记录,则此运算符产生 1; 否则为 0。 name 的类型应为 string

!filter(var, list, predicate)

此运算符通过过滤 list 中的元素来创建新的 list。 为了执行过滤,TableGen 将变量 var 绑定到每个元素,然后评估 predicate 表达式,该表达式大概引用了 var。 谓词必须产生布尔值(bitbitsint)。 该值与 !if 一样解释:如果值为 0,则该元素不包含在新列表中。 如果该值为任何其他值,则该元素包含在内。

!find(string1, string2[, start])

此运算符在 string1 中搜索 string2 并产生其位置。 搜索的起始位置可以由 start 指定,start 的范围可以在 0 到 string1 的长度之间; 默认为 0。 如果未找到字符串,则结果为 -1。

!foldl(init, list, acc, var, expr)

此运算符对 list 中的项目执行左折叠。 变量 acc 充当累加器,并初始化为 init。 变量 var 绑定到 list 中的每个元素。 表达式针对每个元素进行评估,大概使用 accvar 来计算累积值,!foldl 将累积值存储回 acc 中。 acc 的类型与 init 相同; var 的类型与 list 的元素相同; expr 必须与 init 具有相同的类型。

以下示例计算 RecList 中记录列表中的 Number 字段的总和

int x = !foldl(0, RecList, total, rec, !add(total, rec.Number));

如果您的目标是过滤列表并生成一个新列表,其中仅包含某些元素,请参阅 !filter

!foreach(var, sequence, expr)

此运算符创建一个新的 list/dag,其中每个元素都是 sequence list/dag 中相应元素的函数。 为了执行该函数,TableGen 将变量 var 绑定到一个元素,然后评估表达式。 该表达式大概引用了变量 var 并计算结果值。

如果您只是想创建一个包含相同值重复多次的特定长度的列表,请参阅 !listsplat

!ge(a, b)

此运算符在 a 大于或等于 b 时生成 1;否则生成 0。参数必须是 bitbitsintstring 值。

!getdagarg<type>(dag,key)

此运算符通过指定的 key 从给定的 dag 节点检索参数,key 可以是整数索引或字符串名称。如果该参数无法转换为指定的 type,则返回 ?

!getdagname(dag,index)

此运算符通过指定的 index 从给定的 dag 节点检索参数名称。如果该参数没有关联的名称,则返回 ?

!getdagop(dag) –或– !getdagop<type>(dag)

此运算符生成给定 dag 节点的运算符。示例:!getdagop((foo 1, 2)) 的结果为 foo。回想一下,DAG 运算符始终是记录。

!getdagop 的结果可以直接在任何记录类都可接受的上下文中使用(通常将其放入另一个 dag 值中)。但在其他上下文中,它必须显式转换为特定的类。<type> 语法是为了简化此操作而提供的。

例如,要将结果分配给 BaseClass 类型的值,您可以编写以下任一代码

BaseClass b = !getdagop<BaseClass>(someDag);
BaseClass b = !cast<BaseClass>(!getdagop(someDag));

但是,要创建一个新的 DAG 节点来重用另一个节点的运算符,则无需强制转换

dag d = !dag(!getdagop(someDag), args, names);
!gt(a, b)

此运算符在 a 大于 b 时生成 1;否则生成 0。参数必须是 bitbitsintstring 值。

!head(a)

此运算符生成列表 a 的第零个元素。(另请参阅 !tail。)

!if(test, then, else)

此运算符评估 testtest 必须生成 bitint。如果结果不为 0,则生成 then 表达式;否则生成 else 表达式。

!initialized(a)

此运算符在 a 不是未初始化值 (?) 时生成 1,否则生成 0。

!interleave(list, delim)

此运算符连接 list 中的项,在每对项之间交错 delim 字符串,并生成结果字符串。列表可以是字符串、int、bits 或 bit 的列表。空列表将生成空字符串。分隔符可以是空字符串。

!isa<type>(a)

此运算符在 a 的类型是给定 type 的子类型时生成 1;否则生成 0。

!le(a, b)

此运算符在 a 小于或等于 b 时生成 1;否则生成 0。参数必须是 bitbitsintstring 值。

!listconcat(list1, list2, ...)

此运算符连接列表参数 list1list2 等,并生成结果列表。列表必须具有相同的元素类型。

!listflatten(list)

此运算符展平列表的列表 list,并生成一个包含所有组成列表的元素连接的列表。如果 list 的类型为 list<list<X>>,则结果列表的类型为 list<X>。如果 list 的元素类型不是列表,则结果为 list 本身。

!listremove(list1, list2)

此运算符返回 list1 的副本,其中删除了 list2 中也出现的所有元素。列表必须具有相同的元素类型。

!listsplat(value, count)

此运算符生成一个长度为 count 的列表,其元素都等于 value。例如,!listsplat(42, 3) 的结果为 [42, 42, 42]

!logtwo(a)

此运算符生成 a 的以 2 为底的对数,并生成整数结果。0 或负数的对数会产生错误。这是一个向下取整运算。

!lt(a, b)

此运算符在 a 小于 b 时生成 1;否则生成 0。参数必须是 bitbitsintstring 值。

!mul(a, b, ...)

此运算符将 ab 等相乘,并生成乘积。

!ne(a, b)

此运算符在 a 不等于 b 时生成 1;否则生成 0。参数必须是 bitbitsintstring 或记录值。使用 !cast<string> 来比较其他类型的对象。

!not(a)

此运算符对 a 执行逻辑非运算,a 必须是整数。参数 0 的结果为 1(真);任何其他参数的结果为 0(假)。

!or(a, b, ...)

此运算符对 ab 等执行按位或运算,并生成结果。如果所有参数均为 0 或 1,则可以执行逻辑或运算。如果最左边的操作数为 -1,则此运算符会短路到 -1(全为 1)。

!range([start,] end[,step])

此运算符生成半开区间序列 [start : end : step) 作为 list<int>start 默认为 0step 默认为 1step 可以为负数,但不能为 0。如果 start < endstep 为负数,或者 start > endstep 为正数,则结果为空列表 []<list<int>>

例如

  • !range(4) 等效于 !range(0, 4, 1),结果为 [0, 1, 2, 3]

  • !range(1, 4) 等效于 !range(1, 4, 1),结果为 [1, 2, 3]

  • !range(0, 4, 2) 的结果为 [0, 2]

  • !range(0, 4, -1)!range(4, 0, 1) 的结果为空。

!range(list)

等效于 !range(0, !size(list))

!repr(value)

value 表示为字符串。不保证值的字符串格式稳定。仅用于调试目的。

!setdagarg(dag,key,arg)

此运算符生成一个 DAG 节点,该节点具有与 dag 相同的运算符和参数,但将由 key 指定的参数的值替换为 arg。该 key 可以是整数索引或字符串名称。

!setdagname(dag,key,name)

此运算符生成一个 DAG 节点,该节点具有与 dag 相同的运算符和参数,但将由 key 指定的参数的名称替换为 name。该 key 可以是整数索引或字符串名称。

!setdagop(dag, op)

此运算符生成一个 DAG 节点,该节点具有与 dag 相同的参数,但其运算符已替换为 op

示例:!setdagop((foo 1, 2), bar) 的结果为 (bar 1, 2)

!shl(a, count)

此运算符将 a 逻辑左移 count 位,并生成结果值。该操作在 64 位整数上执行;对于 0…63 范围之外的移位计数,结果未定义。

!size(a)

此运算符生成字符串、列表或 dag a 的大小。DAG 的大小是参数的数量;运算符不计数。

!sra(a, count)

此运算符将 a 算术右移 count 位,并生成结果值。该操作在 64 位整数上执行;对于 0…63 范围之外的移位计数,结果未定义。

!srl(a, count)

此运算符将 a 逻辑右移 count 位,并生成结果值。该操作在 64 位整数上执行;对于 0…63 范围之外的移位计数,结果未定义。

!strconcat(str1, str2, ...)

此运算符连接字符串参数 str1str2 等,并生成结果字符串。

!sub(a, b)

此运算符从 a 中减去 b,并生成算术差。

!subst(target, repl, value)

此运算符将 value 中所有出现的 target 替换为 repl,并生成结果值。value 可以是字符串,在这种情况下,将执行子字符串替换。

value 可以是记录名称,在这种情况下,如果 target 记录名称等于 value 记录名称,则运算符生成 repl 记录;否则,它生成 value

!substr(string, start[, length])

此运算符提取给定 string 的子字符串。子字符串的起始位置由 start 指定,start 的范围可以在 0 到字符串的长度之间。子字符串的长度由 length 指定;如果未指定,则提取字符串的其余部分。startlength 参数必须是整数。

!tail(a)

此运算符生成一个新列表,其中包含列表 a 的所有元素,但第零个元素除外。(另请参阅 !head。)

!tolower(a)

此运算符将字符串输入 a 转换为小写。

!toupper(a)

此运算符将字符串输入 a 转换为大写。

!xor(a, b, ...)

此运算符对 ab 等执行按位异或运算,并生成结果。如果所有参数均为 0 或 1,则可以执行逻辑异或运算。

1.11 附录 B:粘贴运算符示例

这是一个示例,说明如何在记录名称中使用粘贴运算符。

defvar suffix = "_suffstring";
defvar some_ints = [0, 1, 2, 3];

def name # suffix {
}

foreach i = [1, 2] in {
def rec # i {
}
}

第一个 def 不使用 suffix 变量的值。第二个 def 使用 i 迭代器变量的值,因为它不是全局名称。生成以下记录。

def namesuffix {
}
def rec1 {
}
def rec2 {
}

这是第二个示例,说明如何在字段值表达式中使用粘贴运算符。

def test {
  string strings = suffix # suffix;
  list<int> integers = some_ints # [4, 5, 6];
}

strings 字段表达式在粘贴运算符的两侧都使用 suffix。它在左侧正常评估,但在右侧按字面意思理解。integers 字段表达式使用 some_ints 变量的值和一个字面量列表。生成以下记录。

def test {
  string strings = "_suffstringsuffix";
  list<int> ints = [0, 1, 2, 3, 4, 5, 6];
}

1.12 附录 C:示例记录

LLVM 支持的一个目标机器是 Intel x86。以下 TableGen 的输出显示了为表示 32 位寄存器到寄存器 ADD 指令而创建的记录。

def ADD32rr { // InstructionEncoding Instruction X86Inst I ITy Sched BinOpRR BinOpRR_RF
  int Size = 0;
  string DecoderNamespace = "";
  list<Predicate> Predicates = [];
  string DecoderMethod = "";
  bit hasCompleteDecoder = 1;
  string Namespace = "X86";
  dag OutOperandList = (outs GR32:$dst);
  dag InOperandList = (ins GR32:$src1, GR32:$src2);
  string AsmString = "add{l}  {$src2, $src1|$src1, $src2}";
  EncodingByHwMode EncodingInfos = ?;
  list<dag> Pattern = [(set GR32:$dst, EFLAGS, (X86add_flag GR32:$src1, GR32:$src2))];
  list<Register> Uses = [];
  list<Register> Defs = [EFLAGS];
  int CodeSize = 3;
  int AddedComplexity = 0;
  bit isPreISelOpcode = 0;
  bit isReturn = 0;
  bit isBranch = 0;
  bit isEHScopeReturn = 0;
  bit isIndirectBranch = 0;
  bit isCompare = 0;
  bit isMoveImm = 0;
  bit isMoveReg = 0;
  bit isBitcast = 0;
  bit isSelect = 0;
  bit isBarrier = 0;
  bit isCall = 0;
  bit isAdd = 0;
  bit isTrap = 0;
  bit canFoldAsLoad = 0;
  bit mayLoad = ?;
  bit mayStore = ?;
  bit mayRaiseFPException = 0;
  bit isConvertibleToThreeAddress = 1;
  bit isCommutable = 1;
  bit isTerminator = 0;
  bit isReMaterializable = 0;
  bit isPredicable = 0;
  bit isUnpredicable = 0;
  bit hasDelaySlot = 0;
  bit usesCustomInserter = 0;
  bit hasPostISelHook = 0;
  bit hasCtrlDep = 0;
  bit isNotDuplicable = 0;
  bit isConvergent = 0;
  bit isAuthenticated = 0;
  bit isAsCheapAsAMove = 0;
  bit hasExtraSrcRegAllocReq = 0;
  bit hasExtraDefRegAllocReq = 0;
  bit isRegSequence = 0;
  bit isPseudo = 0;
  bit isExtractSubreg = 0;
  bit isInsertSubreg = 0;
  bit variadicOpsAreDefs = 0;
  bit hasSideEffects = ?;
  bit isCodeGenOnly = 0;
  bit isAsmParserOnly = 0;
  bit hasNoSchedulingInfo = 0;
  InstrItinClass Itinerary = NoItinerary;
  list<SchedReadWrite> SchedRW = [WriteALU];
  string Constraints = "$src1 = $dst";
  string DisableEncoding = "";
  string PostEncoderMethod = "";
  bits<64> TSFlags = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0 };
  string AsmMatchConverter = "";
  string TwoOperandAliasConstraint = "";
  string AsmVariantName = "";
  bit UseNamedOperandTable = 0;
  bit FastISelShouldIgnore = 0;
  bits<8> Opcode = { 0, 0, 0, 0, 0, 0, 0, 1 };
  Format Form = MRMDestReg;
  bits<7> FormBits = { 0, 1, 0, 1, 0, 0, 0 };
  ImmType ImmT = NoImm;
  bit ForceDisassemble = 0;
  OperandSize OpSize = OpSize32;
  bits<2> OpSizeBits = { 1, 0 };
  AddressSize AdSize = AdSizeX;
  bits<2> AdSizeBits = { 0, 0 };
  Prefix OpPrefix = NoPrfx;
  bits<3> OpPrefixBits = { 0, 0, 0 };
  Map OpMap = OB;
  bits<3> OpMapBits = { 0, 0, 0 };
  bit hasREX_WPrefix = 0;
  FPFormat FPForm = NotFP;
  bit hasLockPrefix = 0;
  Domain ExeDomain = GenericDomain;
  bit hasREPPrefix = 0;
  Encoding OpEnc = EncNormal;
  bits<2> OpEncBits = { 0, 0 };
  bit HasVEX_W = 0;
  bit IgnoresVEX_W = 0;
  bit EVEX_W1_VEX_W0 = 0;
  bit hasVEX_4V = 0;
  bit hasVEX_L = 0;
  bit ignoresVEX_L = 0;
  bit hasEVEX_K = 0;
  bit hasEVEX_Z = 0;
  bit hasEVEX_L2 = 0;
  bit hasEVEX_B = 0;
  bits<3> CD8_Form = { 0, 0, 0 };
  int CD8_EltSize = 0;
  bit hasEVEX_RC = 0;
  bit hasNoTrackPrefix = 0;
  bits<7> VectSize = { 0, 0, 1, 0, 0, 0, 0 };
  bits<7> CD8_Scale = { 0, 0, 0, 0, 0, 0, 0 };
  string FoldGenRegForm = ?;
  string EVEX2VEXOverride = ?;
  bit isMemoryFoldable = 1;
  bit notEVEX2VEXConvertible = 0;
}

在记录的第一行,您可以看到 ADD32rr 记录继承自八个类。虽然继承层次结构很复杂,但使用父类比为每个指令指定 109 个单独的字段要简单得多。

以下是用于定义 ADD32rr 和多个其他 ADD 指令的代码片段

defm ADD : ArithBinOp_RF<0x00, 0x02, 0x04, "add", MRM0r, MRM0m,
                         X86add_flag, add, 1, 1, 1>;

defm 语句告诉 TableGen,ArithBinOp_RF 是一个多类,其中包含从 BinOpRR_RF 继承的多个具体记录定义。该类又从 BinOpRR 继承,而 BinOpRR 又从 ITySched 等继承。字段从所有父类继承;例如,IsIndirectBranchInstruction 类继承。