1 TableGen 程序员参考手册¶
1.1 简介¶
TableGen 的目的是根据来自源文件的信息生成复杂的输出文件,这些源文件比输出文件更容易编写,并且更容易随着时间的推移进行维护和修改。信息以声明式风格编码,涉及类和记录,然后由 TableGen 处理。内部化的记录传递给各种后端,后端从记录的子集中提取信息并生成一个或多个输出文件。这些输出文件通常是 C++ 的.inc
文件,但也可以是后端开发人员需要的任何类型的文件。
本文档详细描述了 LLVM TableGen 功能。它面向使用 TableGen 为项目生成代码的程序员。如果您正在寻找简单的概述,请查看TableGen 概述。用于调用 TableGen 的各种*-tblgen
命令在tblgen 系列 - 从 C++ 代码到描述中进行了描述。
后端的一个示例是RegisterInfo
,它为特定目标机器生成寄存器文件信息,供 LLVM 目标无关代码生成器使用。有关 LLVM TableGen 后端的描述,请参见TableGen 后端,有关编写新后端的指南,请参见TableGen 后端开发人员指南。
以下是后端可以执行的一些操作。
为特定目标机器生成寄存器文件信息。
为目标生成指令定义。
生成代码生成器用于将指令与中间表示 (IR) 节点匹配的模式。
为 Clang 生成语义属性标识符。
为 Clang 生成抽象语法树 (AST) 声明节点定义。
为 Clang 生成 AST 语句节点定义。
1.1.1 概念¶
TableGen 源文件包含两个主要项目:抽象记录和具体记录。在本以及其他 TableGen 文档中,抽象记录称为类。(这些类与 C++ 类不同,并且不会映射到它们。)此外,具体记录通常称为记录,尽管有时术语记录同时指代类和具体记录。在上下文中,区别应该很清楚。
类和具体记录都有一个唯一的名称,由程序员选择或由 TableGen 生成。与该名称相关联的是具有值的字段列表和可选的父类列表(有时称为基类或超类)。字段是后端将处理的主要数据。请注意,TableGen 不会为字段分配任何含义;含义完全取决于后端和包含这些后端输出的程序。
注意
术语“父类”可以指另一个类的父类,也可以指具体记录从中继承的类。这种非标准的术语用法是由于 TableGen 对类和具体记录的处理方式相似。
后端处理 TableGen 解析器构建的一些具体记录子集并发出输出文件。这些文件通常是 C++ 的.inc
文件,这些文件包含在需要这些记录中数据的程序中。但是,后端可以生成任何类型的输出文件。例如,它可以生成一个数据文件,其中包含带有标识符和替换参数的标记消息。在 LLVM 代码生成器等复杂用例中,可能存在许多具体记录,其中一些记录可能具有意外的大量字段,从而导致大型输出文件。
为了降低 TableGen 文件的复杂性,类用于抽象记录字段组。例如,一些类可以抽象机器寄存器文件的概念,而其他类可以抽象指令格式,还有一些类可以抽象单个指令。TableGen 允许任意层次结构的类,以便两个概念的抽象类可以共享第三个超类,该超类从这两个原始概念中抽象出共同的“子概念”。
为了使类更有用,具体记录(或另一个类)可以请求一个类作为父类并向其传递模板参数。这些模板参数可以在父类的字段中使用,以自定义方式初始化它们。也就是说,记录或类A
可以请求父类S
并传递一组模板参数,而记录或类B
可以请求S
并传递另一组参数。如果没有模板参数,则需要更多类,每个模板参数组合一个。
类和具体记录都可以包含未初始化的字段。未初始化的“值”由问号 (?
) 表示。类通常具有未初始化的字段,这些字段预计在这些类被具体记录继承时会被填充。即便如此,具体记录的一些字段也可能保持未初始化状态。
TableGen 提供多类以在一个地方收集一组记录定义。多类是一种宏,可以“调用”它来一次定义多个具体记录。多类可以从其他多类继承,这意味着多类继承其父多类中的所有定义。
附录 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 感叹号运算符¶
TableGen 提供了“感叹号运算符”,它们具有多种用途
BangOperator ::= one of !add !and !cast !con !dag !div !empty !eq !exists !filter !find !foldl !foreach !ge !getdagarg !getdagname !getdagop !gt !head !if !interleave !isa !le !listconcat !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
运算符与其他感叹号运算符相比,语法略有不同,因此单独定义
CondOperator ::= !cond
有关每个感叹号运算符的说明,请参见附录 A:感叹号运算符。
1.3.4 包含文件¶
TableGen 具有包含机制。包含文件的内容在词法上替换 include
指令,然后将其解析为它最初在主文件中一样。
IncludeDirective ::= "include" TokString
可以使用预处理器指令对主文件和包含文件的部分进行条件化。
PreprocessorDirective ::= "#define" | "#ifdef" | "#ifndef"
1.4 类型¶
TableGen 语言是静态类型的,使用一个简单但完整的类型系统。类型用于检查错误、执行隐式转换以及帮助接口设计人员约束允许的输入。每个值都需要具有关联的类型。
TableGen 支持低级类型(例如,bit
)和高级类型(例如,dag
)的混合。这种灵活性允许您方便快捷地描述各种记录。
Type ::= "bit" | "int" | "string" | "dag" | "bits" "<"TokInteger
">" | "list" "<"Type
">" |ClassID
ClassID ::=TokIdentifier
bit
一个
bit
是一个布尔值,可以是 0 或 1。int
int
类型表示一个简单的 64 位整数,例如 5 或 -42。string
string
类型表示任意长度的字符的有序序列。bits<
n>
bits
类型是任意长度 n 的固定大小整数,被视为单独的位。可以单独访问这些位。此类型的字段对于表示指令操作码、寄存器编号或寻址模式/寄存器/位移很有用。可以单独或作为子字段设置字段的位。例如,在指令地址中,可以分别设置寻址模式、基址寄存器编号和位移。list<
type>
此类型表示一个列表,其元素是尖括号中指定的 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
警告
RangePiece
和 SliceElement
的特殊最后一种形式是由于“-
”包含在 TokInteger
中,因此 1-5
被词法分析为两个连续的标记,值分别为 1
和 -5
,而不是“1”、“-”和“5”。使用连字符作为范围标点符号已弃用。
1.5.1 简单值¶
SimpleValue
有多种形式。
SimpleValue ::=TokInteger
|TokString
+ |TokCode
值可以是整数字面量、字符串字面量或代码字面量。多个相邻的字符串字面量像在 C/C++ 中一样连接;简单值是字符串的连接。代码字面量变成字符串,然后与它们无法区分。
SimpleValue2 ::= "true" | "false"
true
和 false
字面量本质上是整数 1 和 0 的语法糖。当布尔值用于字段初始化、位序列、if
语句等时,它们可以提高 TableGen 文件的可读性。解析时,这些字面量会被转换为整数。
注意
尽管 true
和 false
是 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; }
class
或multiclass
定义中的隐式模板参数NAME
(请参见NAME)。class
中的局部字段,例如在以下代码中使用Bar
class Foo { int Bar = 5; int Baz = Bar; }
记录定义的名称,例如在
Foo
的定义中使用Bar
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>; }
用
defvar
或defset
语句定义的变量。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
感叹号运算符提供其他简单值不可用的函数。除了!cond
的情况外,感叹号运算符接受括号括起来的参数列表,并对这些参数执行某些函数,为该感叹号运算符生成一个值。!cond
运算符接受由冒号分隔的参数对列表。有关每个感叹号运算符的描述,请参见附录 A:感叹号运算符。
1.5.2 带后缀的值¶
上面描述的SimpleValue
值可以使用某些后缀指定。后缀的目的是获取主值的子值。以下是一些主值的可能后缀。
- 值
{17}
最终值为整数值的第 17 位(注意花括号)。
- 值
{8...15}
最终值为整数值的第 8-15 位。可以通过指定
{15...8}
来反转位的顺序。- 值
[i]
最终值为列表值的第i个元素(注意方括号)。换句话说,方括号充当列表的下标运算符。仅当指定单个元素时,才会出现这种情况。
- 值
[i,]
最终值为包含列表单个元素i的列表。简而言之,包含单个元素的列表切片。
- 值
[4...7,17,2...3,4]
最终值为一个新的列表,它是列表值的切片。新列表包含元素 4、5、6、7、17、2、3 和 4。元素可以多次包含,并且可以按任意顺序包含。仅当指定多个元素时,才会出现此结果。
- 值
[i,m...n,j,ls]
每个元素都可以是表达式(变量、感叹号运算符)。m和n的类型应为int。i、j和ls的类型应为int或list<int>。
- 值
- 值
.
字段 最终值为指定记录值中指定字段的值。
1.5.3 粘贴运算符¶
粘贴运算符(#
)是 TableGen 表达式中唯一可用的中缀运算符。它允许您连接字符串或列表,但具有一些不寻常的功能。
在Def
或Defm
语句中指定记录名称时,可以使用粘贴运算符,在这种情况下,它必须构造一个字符串。如果操作数是未定义的名称(TokIdentifier
)或全局Defvar
或Defset
的名称,则将其视为字符的逐字字符串。不会使用全局名称的值。
粘贴运算符可用于所有其他值表达式,在这种情况下,它可以构造字符串或列表。奇怪的是,但与之前的情况一致,如果右侧操作数是未定义的名称或全局名称,则将其视为字符的逐字字符串。左侧操作数将按正常方式处理。
值可以具有尾随粘贴运算符,在这种情况下,左侧操作数将连接到空字符串。
附录 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
中的ParentClassList
,或者存在
RecordBody
中的Body
并且不为空。
您可以通过指定一个空的TemplateArgList
和一个空的RecordBody
来声明一个空类。这可以用作前向声明的受限形式。请注意,从前向声明的类派生的记录不会继承它的任何字段,因为这些记录是在解析其声明时构建的,因此在最终定义该类之前。
每个类都有一个名为NAME
(大写)的隐式模板参数,它绑定到继承自该类的Def
或Defm
的名称。如果类被匿名记录继承,则名称未指定但全局唯一。
有关示例,请参见示例:类和记录。
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
}*]
包含MultiClassID
的ParentClassList
仅在defm
语句的类列表中有效。在这种情况下,ID 必须是多类的名称。
参数值可以用两种形式指定
位置参数(
value
)。该值将分配给对应位置的参数。对于Foo<a0, a1>
,a0
将分配给第一个参数,a1
将分配给第二个参数。命名参数(
name=value
)。该值将分配给具有指定名称的参数。对于Foo<a=a0, b=a1>
,a0
将分配给名称为a
的参数,a1
将分配给名称为b
的参数。
必需参数也可以指定为命名参数。
请注意,无论指定方式(命名或位置)如何,参数只能指定一次,并且位置参数应放在命名参数之前。
Body ::= ";" | "{"BodyItem
* "}" BodyItem ::= (Type
| "code")TokIdentifier
["="Value
] ";" | "let"TokIdentifier
["{"RangeList
"}"] "="Value
";" | "defvar"TokIdentifier
"="Value
";" |Assert
主体中的字段定义指定要包含在类或记录中的字段。如果未指定初始值,则字段的值未初始化。必须指定类型;TableGen 不会从值中推断类型。关键字code
可用于强调该字段具有作为代码的字符串值。
使用let
形式将字段重置为新值。这可以对直接在主体中定义的字段或从父类继承的字段进行。可以指定RangeList
以重置bit<n>
字段中的某些位。
使用defvar
形式定义一个变量,其值可以在主体内的其他值表达式中使用。该变量不是字段:它不会成为正在定义的类或记录的字段。在处理主体时,提供变量来保存临时值。有关更多详细信息,请参见记录主体中的 Defvar。
当类C2
从类C1
继承时,它获取C1
的所有字段定义。当这些定义合并到类C2
中时,传递给C1
的任何模板参数C2
都被替换到定义中。换句话说,在合并到C2
之前,C1
定义的抽象记录字段会使用模板参数进行扩展。
1.6.2 def
— 定义具体记录¶
一个def
语句定义一个新的具体记录。
Def ::= "def" [NameValue
]RecordBody
NameValue ::=Value
(parsed in a special mode)
name 值是可选的。如果指定,则在一种特殊模式下解析它,其中未定义(无法识别)的标识符被解释为文字字符串。特别是,全局标识符被视为无法识别。其中包括由defvar
和defset
定义的全局变量。记录名称可以是空字符串。
如果未给出 name 值,则该记录为匿名记录。匿名记录的最终名称未指定,但全局唯一。
如果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
— 定义多个记录¶
虽然带有模板参数的类是提取多个记录之间共性的好方法,但多类允许一种方便的方法来一次定义多个记录。例如,考虑一个三地址指令架构,其指令采用两种格式:reg = reg op reg
和reg = reg op imm
(例如,SPARC)。我们希望在一个地方指定这两种常见格式的存在,然后在另一个地方指定所有操作是什么。multiclass
和defm
语句实现了这个目标。您可以将多类视为扩展为多个记录的宏或模板。
MultiClass ::= "multiclass"TokIdentifier
[TemplateArgList
]ParentClassList
"{"MultiClassStatement
+ "}" MultiClassID ::=TokIdentifier
MultiClassStatement ::=Assert
|Def
|Defm
|Defvar
|Foreach
|If
|Let
与常规类一样,多类具有名称并且可以接受模板参数。多类可以从其他多类继承,这会导致其他多类被扩展并为继承多类中的记录定义做出贡献。多类的主体包含一系列定义记录的语句,使用Def
和Defm
。此外,Defvar
、Foreach
和Let
语句可用于提取更多公共元素。If
和Assert
语句也可以使用。
与普通类一样,多类具有隐式模板参数NAME
(参见NAME)。当在多类中定义一个命名(非匿名)记录,并且记录的名称不包含对模板参数NAME
的使用时,此类使用会自动前置到名称中。也就是说,以下在多类内部是等价的
def Foo ...
def NAME # Foo ...
在多类中定义的记录是在多类被外部的defm
语句“实例化”或“调用”时创建的。多类中的每个def
语句都会生成一个记录。与顶级def
语句一样,这些定义可以从多个父类继承。
有关示例,请参见示例:多类和 defm。
1.6.6 defm
— 调用多类以定义多个记录¶
定义多类后,可以使用defm
语句“调用”它们并处理这些多类中的多个记录定义。这些记录定义由多类中的def
语句指定,并由defm
语句间接指定。
Defm ::= "defm" [NameValue
]ParentClassList
";"
可选的NameValue
的形成方式与def
的名称相同。ParentClassList
是一个冒号,后跟至少一个多类和任意数量的普通类的列表。多类必须位于普通类之前。请注意,defm
没有主体。
此语句实例化所有指定多类中定义的所有记录,这些记录可以通过def
语句直接定义,也可以通过defm
语句间接定义。这些记录还接收父类列表中包含的任何普通类中定义的字段。这对于向defm
创建的所有记录添加一组公共字段很有用。
名称以def
使用的相同特殊模式进行解析。如果未包含名称,则会提供一个未指定的但全局唯一的名称。也就是说,以下示例最终将具有不同的名称
defm : SomeMultiClass<...>; // A globally unique name.
defm "" : SomeMultiClass<...>; // An empty name.
defm
语句可以在多类主体中使用。当发生这种情况时,第二个变体等价于
defm NAME : SomeMultiClass<...>;
更一般地,当defm
出现在多类中并且其名称不包含对隐式模板参数NAME
的使用时,则会自动前置NAME
。也就是说,以下在多类内部是等价的
defm Foo : SomeMultiClass<...>;
defm NAME # Foo : SomeMultiClass<...>;
有关示例,请参见示例:多类和 defm。
1.6.7 示例:多类和 defm¶
这是一个使用multiclass
和defm
的简单示例。考虑一个三地址指令架构,其指令有两种格式:reg = reg op reg
和reg = 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
多类都会定义两个记录,一个带有_rr
后缀,另一个带有_ri
。回想一下,使用多类的defm
的名称会前置到该多类中定义的记录的名称。因此,生成的定义的名称为
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
可用于多类中以“调用”其他多类并在这些多类中定义的记录之外创建这些多类中定义的记录。在以下示例中,basic_s
和basic_p
多类包含引用basic_r
多类的defm
语句。basic_r
多类仅包含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
多类的五个和来自basic_p
多类的五个
ADDSSrr, ADDSSrm
ADDSDrr, ADDSDrm
ADDX
ADDPSrr, ADDPSrm
ADDPDrr, ADDPDrm
ADDY
无论是在顶级还是在多类中,defm
语句都可以从普通类和多类继承。规则是普通类必须列在多类之后,并且必须至少有一个多类。
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 };
}
还可以使用多类内部的let
语句,提供另一种方法从记录中提取共性,尤其是在使用多级多类实例化时。
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
* "}"
通过def
和defm
在大括号内定义的所有记录都像往常一样定义,并且它们也收集在给定名称(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<...>;
}
此循环定义了名为R0
、R1
、R2
和R3
的记录,以及F0
、F1
、F2
和F3
。
1.6.12 dump
— 将消息打印到标准错误输出¶
dump
语句将输入字符串打印到标准错误输出。它用于调试目的。
在顶层,消息会立即打印。
在记录/类/多类中,dump在包含记录的每个实例化点进行评估。
Dump ::= "dump" string
";"
例如,它可以与!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
表达式值会被评估。如果它评估为真(与感叹号运算符使用的意义相同),则处理then
保留字之后的语句。否则,如果存在else
保留字,则处理else
之后的语句。如果值为假且不存在else
分支,则不处理任何语句。
由于then
语句周围的花括号是可选的,因此此语法规则与“悬空else”子句具有通常的歧义,并且以通常的方式解决:在类似if v1 then if v2 then {...} else {...}
的情况下,else
与内部if
关联,而不是外部if
。
then 和 else 分支的 IfBody
建立了一个内部作用域。在主体中定义的任何defvar
变量在主体完成后都会超出作用域(有关更多详细信息,请参阅记录主体中的 Defvar)。
if
语句也可以在记录Body
中使用。
1.6.14 assert
— 检查条件是否为真¶
assert
语句检查布尔条件是否为真,如果不是则打印错误消息。
Assert ::= "assert"condition
","message
";"
如果布尔条件为真,则语句不执行任何操作。如果条件为假,则会打印一条非致命错误消息。消息(可以是任意字符串表达式)作为注释包含在错误消息中。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 中的同名参数关联。
以下感叹号运算符可用于处理 DAG:!con
、!dag
、!empty
、!foreach
、!getdagarg
、!getdagname
、!getdagop
、!setdagarg
、!setdagname
、!setdagop
、!size
。
1.7.2 记录主体中的 Defvar¶
除了定义全局变量之外,defvar
语句还可以用于类或记录定义的Body
内部以定义局部变量。class
或multiclass
的模板参数可以在值表达式中使用。变量的作用域从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 会执行以下步骤。类只是抽象记录,因此会经历相同的步骤。
构建记录名称 (
NameValue
) 并创建一个空记录。从左到右解析
ParentClassList
中的父类,从上到下访问每个父类的祖先类。
将父类中的字段添加到记录中。
将模板参数替换到这些字段中。
将父类添加到记录的继承类列表中。
将任何顶层
let
绑定应用于记录。请记住,顶层绑定仅适用于继承的字段。解析记录的主体。
将任何字段添加到记录中。
根据局部
let
语句修改字段的值。定义任何
defvar
变量。
对所有字段进行一次遍历,以解决任何字段间引用。
将记录添加到最终记录列表中。
由于字段之间的引用是在应用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,因为在解析!add(Y, 1)
之前执行了let Y
。明智地使用此功能。
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
可以在 TableGen 文件中的任何位置定义MacroName
。该名称没有值;只能测试它是否已定义。
宏测试区域以 #ifdef
或 #ifndef
指令开头。如果宏名称已定义 (#ifdef
) 或未定义 (#ifndef
),则处理指令和相应的 #else
或 #endif
之间的源代码。如果测试失败但存在 #else
子句,则处理 #else
和 #endif
之间的源代码。如果测试失败且不存在 #else
子句,则不处理测试区域中的任何源代码。
测试区域可以嵌套,但必须正确嵌套。在一个文件中开始的区域必须在该文件中结束;也就是说,必须在其 #endif
在同一文件中。
可以使用 *-tblgen
命令行上的 -D
选项在外部定义MacroName
llvm-tblgen self-reference.td -Dmacro1 -Dmacro3
1.10 附录 A:叹号运算符¶
叹号运算符在值表达式中充当函数。叹号运算符接受一个或多个参数,对它们进行运算并产生结果。如果运算符产生布尔结果,则结果值为真时为 1,假时为 0。当运算符测试布尔参数时,它将 0 解释为假,非 0 解释为真。
警告
!getop
和 !setop
叹号运算符已弃用,建议使用 !getdagop
和 !setdagop
。
!add(
a,
b, ...)
此运算符将 a、b 等相加,并产生总和。
!and(
a,
b, ...)
此运算符对 a、b 等进行按位与运算,并产生结果。如果所有参数均为 0 或 1,则可以执行逻辑与运算。
!cast<
type>(
a)
此运算符对 a 执行强制类型转换并产生结果。如果 a 不是字符串,则执行简单的强制类型转换,例如在
int
和bit
之间,或在记录类型之间。这允许将记录强制转换为类。如果将记录强制转换为string
,则会生成记录的名称。如果 a 是字符串,则将其视为记录名称并在所有已定义记录的列表中查找。预期结果记录为指定的 type。
例如,如果
!cast<
type>(
name)
出现在多类定义中,或出现在多类定义内部实例化的类中,并且 name 未引用多类的任何模板参数,则必须在源文件中的前面已实例化一个具有该名称的记录。如果 name 确实引用了模板参数,则查找将延迟到实例化多类的defm
语句(或更晚,如果 defm 出现在另一个多类中,并且 name 引用的内部多类的模板参数被替换为本身包含对外部多类的模板参数的引用的值)。如果 a 的类型与 type 不匹配,则 TableGen 会引发错误。
!con(
a,
b, ...)
此运算符连接 DAG 节点 a、b 等。它们的运算必须相等。
!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,如果结果为真则返回 val1。如果为假,则运算符测试 cond2,如果结果为真则返回 val2。依此类推。如果没有条件为真,则会报告错误。
此示例生成整数的符号字
!cond(!lt(x, 0) : "negative", !eq(x, 0) : "zero", true : "positive")
!dag(
op,
arguments,
names)
此运算符使用给定的运算符和参数创建 DAG 节点。arguments 和 names 参数必须是长度相等或未初始化 (
?
) 的列表。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 没有参数,则为空;运算符不计数。
!eq(
a, b)
如果 a 等于 b,则此运算符生成 1;否则生成 0。参数必须为
bit
、bits
、int
、string
或记录值。使用!cast<string>
来比较其他类型的对象。!exists<
type>(
name)
如果存在名称为 name 且类型为 type 的记录,则此运算符生成 1;否则生成 0。name 应为 string 类型。
!filter(
var,
list,
predicate)
此操作符通过过滤列表list中的元素创建一个新的
list
。为了执行过滤,TableGen将变量var绑定到每个元素,然后计算predicate表达式,该表达式可能引用var。谓词必须产生一个布尔值(bit
、bits
或int
)。该值按!if
的方式解释:如果值为0,则该元素不包含在新列表中。如果值为其他任何值,则包含该元素。
!find(
string1,
string2[,
start])
此操作符在string1中搜索string2并生成其位置。搜索的起始位置可以通过start指定,其范围可以是0到string1的长度;默认为0。如果未找到字符串,则结果为-1。
!foldl(
init,
list,
acc,
var,
expr)
此操作符对list中的项目执行左折叠。变量acc充当累加器,并初始化为init。变量var绑定到list中的每个元素。表达式针对每个元素进行计算,并且可能使用acc和var来计算累积值,
!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
,其中每个元素都是sequencelist
/dag
中相应元素的函数。为了执行该函数,TableGen将变量var绑定到一个元素,然后计算表达式。该表达式可能引用变量var并计算结果值。如果您只想创建一个包含相同值重复多次的特定长度的列表,请参见
!listsplat
。!ge(
a, b)
如果a大于或等于b,则此操作符生成1;否则生成0。参数必须是
bit
、bits
、int
或string
值。!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。参数必须是
bit
、bits
、int
或string
值。!head(
a)
此操作符生成列表a的第零个元素。(另请参见
!tail
)。!if(
test,
then,
else)
此操作符计算test,它必须生成一个
bit
或int
。如果结果不为0,则生成then表达式;否则生成else表达式。!interleave(
list,
delim)
此操作符连接list中的项目,在每对之间插入delim字符串,并生成结果字符串。列表可以是字符串、整数、位或位的列表。空列表导致空字符串。分隔符可以是空字符串。
!isa<
type>(
a)
如果a的类型是给定type的子类型,则此操作符生成1;否则生成0。
!le(
a,
b)
如果a小于或等于b,则此操作符生成1;否则生成0。参数必须是
bit
、bits
、int
或string
值。!listconcat(
list1,
list2, ...)
此操作符连接列表参数list1、list2等,并生成结果列表。列表必须具有相同的元素类型。
!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。参数必须是
bit
、bits
、int
或string
值。!mul(
a,
b, ...)
此操作符将a、b等相乘,并生成乘积。
!ne(
a, b)
如果a不等于b,则此操作符生成1;否则生成0。参数必须是
bit
、bits
、int
、string
或记录值。使用!cast<string>
比较其他类型的对象。!not(
a)
此操作符对a执行逻辑非运算,a必须是整数。参数0导致1(真);任何其他参数导致0(假)。
!or(
a,
b, ...)
此运算符对a、b等进行按位或运算,并产生结果。如果所有参数均为0或1,则可以执行逻辑或运算。
!range([
start,]
end[,
step])
此运算符生成半开范围序列
[start : end : step)
作为list<int>
。start默认为0
,step默认为1
。step可以为负数,但不能为0。如果start<
end且step为负数,或start>
end且step为正数,则结果为空列表[]<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节点,但用arg替换由key指定的参数的值。该key可以是整数索引或字符串名称。
!setdagname(
dag,
key,
name)
此运算符生成一个与dag具有相同运算符和参数的DAG节点,但用name替换由key指定的参数的名称。该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, ...)
此运算符连接字符串参数str1、str2等,并产生结果字符串。
!sub(
a,
b)
此运算符从a中减去b,并产生算术差。
!subst(
target,
repl,
value)
此运算符用repl替换value中target的所有出现,并产生结果值。value可以是字符串,在这种情况下执行子字符串替换。
value可以是记录名称,在这种情况下,如果target记录名称等于value记录名称,则运算符生成repl记录;否则生成value。
!substr(
string,
start[,
length])
此运算符提取给定string的子字符串。子字符串的起始位置由start指定,其范围可以从0到字符串的长度。子字符串的长度由length指定;如果未指定,则提取字符串的其余部分。start和length参数必须为整数。
!tail(
a)
此运算符生成一个新列表,其中包含列表a的所有元素,除了第零个元素。(另请参见
!head
。)!tolower(
a)
此运算符将字符串输入a转换为小写。
!toupper(
a)
此运算符将字符串输入a转换为大写。
!xor(
a,
b, ...)
此运算符对a、b等进行按位异或运算,并产生结果。如果所有参数均为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
继承,后者又从ITy
和Sched
继承,依此类推。字段从所有父类继承;例如,IsIndirectBranch
从Instruction
类继承。