TableGen程序员参考

本文翻译自TableGen Programmer’s Reference — LLVM 15.0.0git documentation

1.1 介绍

TableGen使用输入文件中的信息生成复杂的输出文件,输入源文件比输出文件更容易编码,也更容易维护和修改。输入文件中TableGen处理的信息以声明式风格编码,信息包括类和记录。内部化的记录被传递到各种后端,后端从记录的子集中提取信息,并生成一个或多个输出文件。这些输出文件通常是C++的.inc文件,但也可以是后端开发人员需要的任何其他类型的文件。

本文档将详细介绍LLVM TableGen工具。它适用于需要使用TableGen为项目生成代码的程序员。如果您想简单了解TableGen,请查看TableGen概述。TableGen的各种*-tblgen命令在 tblgen Family - Description to C++ Code中有介绍。 

RegisterInfo是一个后端的例子,它为特定的目标机器生成寄存器文件信息,供LLVM目标无关代码生成器使用。LLVM TableGen后端介绍参见TableGen Backends,如果要编写一个新的后端,请参考 TableGen Backend Developer’s Guide。 

下面是后端可以做的一些事情。

  • 为特定目标机器生成寄存器文件信息;
  • 为目标生成指令定义;
  • 生成代码生成器匹配指令与中间表示(IR)节点的模式;
  • 生成Clang语义属性标识符;
  • 生成Clang抽象语法树(AST)声明节点定义;
  • 生成ClangAST语句节点定义;

 1.1.1 概念

TableGen源文件主要包含两种信息:抽象记录和具体记录。在本文档和其他TableGen文档中,抽象记录被称为类(这里的类与C++的类不同,不要把他们对应起来)。此外,具体的记录通常也被称为记录,尽管有时记录一词既指类又指具体的记录。这一区别应在上下文中明确。

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

注意:术语“父类”可以指的是另一个类的父类,也可以指一个具体记录从其继承的类。这个术语之所以出现这种不标准的用法,是因为TableGen以类似的方式处理类和具体记录。

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

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

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

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

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

Appendix C: Sample Record 展示了Intel X86目标中的一个复杂记录,以及定义它的简单方法。

1.2 源文件 

 TableGen源文件是纯ASCII文本文件。文件可以包含语句、注释和空行(参见 Lexical Analysis)。TableGen文件的标准文件扩展名是.td。

TableGen文件可能会变得非常大,因此存在一种包含机制,允许一个文件包含另一个文件的内容(参见 Include Files)。这样可以将大文件分解为小文件,同时还提供了一种简单的库机制,允许多个源文件可以包含相同的库文件。 

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

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   ::=  "[{" (shortest 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           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
                  !empty      !eq          !filter      !find        !foldl
                  !foreach    !ge          !getdagop    !gt          !head
                  !if         !interleave  !isa         !le          !listconcat
                  !listsplat  !lt          !mul         !ne          !not
                  !or         !setdagop    !shl         !size        !sra
                  !srl        !strconcat   !sub         !subst       !substr
                  !tail       !xor

!cond与其他bang操作符语法略有不同,因此它是单独定义的:

CondOperator ::=  !cond

各bang操作符的信息请参阅 Appendix A: Bang Operators

 1.3.4 include文件

TableGen支持文件包含。包含的文件在包含的位置直接展开,然后被解析。

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的整数,n可以为任意值。这n个bit位可以单独访问。这种类型的字段用于表示指令操作代码、寄存器号或地址模式/寄存器/位移。字段的位可以单独设置,也可以作为子字段设置。例如,在指令地址中,寻址模式、基址寄存器号和位移可以分别设置。

list<type>

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

dag

这种类型表示节点组成的嵌套的有向无环图(DAG)。每个节点有一个操作符和零个或多个参数(或操作数)。参数可以是另一个dag对象,允许任意的节点和边树。例如,DAG用于表示代码生成器指令选择算法使用的代码模式。详见有向无环图(dag)

ClassID

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

1.5 值和表达式 

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

注意,所有值都有将它们从一种类型转换为另一种类型的规则。例如,这些规则允许将值7赋给bits<4>类型的实体。

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

 警告:RangePiece特有的一种形式是“-”包含在TokInteger中,因此1-5作为两个连续的标记,值为1和-5,而不是“1”,“-”和“5”。不建议使用连字符表示范围。

 1.5.1 简单值

SimpleValue有很多形式:

SimpleValue ::=  TokInteger | TokString+ | TokCode

 值可以是整数字面量、字符串字面量或编码字面量。在C/C++中,多个相邻的字符串字面量是连接在一起的;简单值是字符串的拼接。编码字面量变成了字符串,然后它们就无法区分了。

SimpleValue2 ::=  "true" | "false"

 true和false字面量本质上是整数1和0的语法糖。当在字段初始化、bit序列、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

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

  • 类模板参数,例如下面Bar的使用:
class Foo <int Bar> {
  int Baz = Bar;
}
  • 类或多类定义中的隐式模板参数NAME(参见NAME)
  • 类的成员字段,如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;
}

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

  • 一个多类的模板参数,例如Bar:
multiclass Foo <int Bar> {
  def : SomeClass<Bar>;
}
  • 用defvar或defset语句定义的变量
  • foreach的迭代变量,如下i的使用: 
foreach i = 0...5 in
  def Foo#i;
SimpleValue8 ::=  ClassID "<" ValueListNE ">"

这个形式创建了一个新的匿名记录定义(就像从给定的类继承给定模板参数的未命名def所创建的那样;参见def),值就是那条记录。记录的一个字段可以使用后缀获得;参见Suffixed Values

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

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

 bang运算符提供其他简单值不可用的函数。除了!cond的情况,bang操作符接受圆括号中包含的参数列表,并使用这些参数执行一些函数,为bang操作符生成一个值。cond操作符接受以冒号分隔的参数对列表。参见附录A:bang操作符以了解每个bang操作符的描述。

1.5.2 作为后缀的值 

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

value{17}

表示整数value的第17bit位(注意括号)的值。

value{8...15}

表示整数value的第8到第15bit位的值,也可以用按{15...8}反序指定

value[4]

表示列表value的第4个元素。换句话说,方括号充当列表的下标操作符。只有在指定单个元素时才会出现这种情况。

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

最后得到的是一个新列表,它是列表value的一个切片。新列表包含元素4、5、6、7、17、2、3和4。元素可以以任意顺序被包含多次。这是指定多个元素时的结果。

value.field

指定记录value中指定字段field的值。

1.5.3 粘贴操作符 

粘贴操作符(#)是TableGen表达式中唯一可用的中缀操作符。它允许连接字符串或列表,但有一些特别的特性

粘贴操作符可以在Def或Defm语句中指定记录名称时使用,在该场景下,它必须构造一个字符串。如果操作数是未定义的名称(TokIdentifier)或全局Defvar或Defset的名称,则将其视为逐字字符串。全局名称的值没有被使用。

粘贴操作符可以在所有其他值表达式中使用,在这些情况下,它可以构造字符串或列表。很奇怪,但与前面的情况一致,如果右边的操作数是一个未定义的名称或全局名称,则将其视为逐字字符串。左边的操作数被正常处理。

值可以有一个尾随的粘贴操作符,在这种情况下,左边的操作数连接到一个空字符串。

Appendix B: Paste Operator Examples 展示了粘贴操作符行为的例子

1.6 语句

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

TableGenFile ::=  (Statement | IncludeDirective
                 | PreprocessorDirective)*
Statement    ::=  Assert | Class | Def | Defm | Defset | Defvar
                 | 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的字段中。

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

  • TemplateArgList已经存在,或者
  • RecordBody中的ParentClassList存在,或者
  • RecordBody中的主体存在并且不是空的。

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

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

示例参见 Examples: classes and records  

1.6.1.1 记录体

类和记录定义中都包含记录体。记录体可以包括父类列表,该列表指定当前类或记录从哪些类继承字段。这样的类被称为类或记录的父类。记录主体还包括定义的主体,其中包含类或记录的字段的说明。

RecordBody        ::=  ParentClassList Body
ParentClassList   ::=  [":" ParentClassListNE]
ParentClassListNE ::=  ClassRef ("," ClassRef)*
ClassRef          ::=  (ClassID | MultiClassID) ["<" [ValueList] ">"]

 包含MultiClassID的ParentClassList只在defm语句的类列表中才有效。在这种情况下,ID必须是一个多类的名称。

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

主体中的字段定义指定了包含在类或记录中的字段。如果没有指定初始值,则该字段的值是未初始化的。字段类型必须指定;TableGen不会从值推断类型。关键字code可以用来强调字段有一个字符串类型的值,这个字符串值就是编码(code)。

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)

NameValue是可选的。如果指定,将以特殊模式解析它,其中未定义(无法识别)标识符将被解释为文字字符串。特别是,全局标识符被认为是不可识别的。其中包括defvar和defset定义的全局变量。记录名称可以是空字符串。

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

如果def出现在多类语句中,就会做特殊处理。有关详细信息,请参阅下面的多类部分。

通过在记录主体的开头指定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。它有一个被初始化为true字段V

接下来,定义了两个记录,它们派生自类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字段。这8条记录以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字段的两个bit位。我们可以定义一个类,它接受ModRefVal记录作为模板参数,并将其值分成两个字段,每个字段一个bit位。然后,我们可以定义继承自ModRefBits的记录,从而从它获得两个字段,用于作为模板参数传递的ModRefVal记录中的每个bit位。

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--定义多个记录 

虽然带有模板参数的类是提取多个记录之间的共性的好方法,但多类可以方便的一次定义多个记录。例如,考虑一个3地址指令体系结构,它的指令有两种格式: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

与常规类一样,multiclass有一个名称,并且可以接受模板参数。一个多类可以从其他多类继承,这导致其他多类被扩展,并在继承的多类中贡献记录定义。多类的主体包含一系列使用Def和Defm定义记录的语句。此外,Defvar、Foreach和Let语句可以用来分解出更多的公共元素。还可以使用If和Assert语句。

与常规类一样,多类也有隐式模板参数NAME(参见NAME)。当一个命名(非匿名)记录在一个多类中定义,并且记录的名称不包含模板参数NAME的使用时,这样的使用会自动添加到名称的前面。也就是说,以下代码在一个多类中是等价的: 

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

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

示例请查看Examples: multiclasses and defms 

1.6.6 defm--调用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<...>;

示例请查看 Examples: multiclasses and defms

1.6.7 示例:多类和defm

下面是一个使用multiclass和defm的简单示例。考虑一个3地址指令体系结构,它的指令有两种格式:reg = reg op reg和reg = reg op imm (immediate)。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

如果没有多类特性,指令就必须定义如下:

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创建以下记录,5条来自basic_s多类,5条来自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范围内定义非类类型的记录是错误的。

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

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

1.6.9 defvar--定义一个变量 

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

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

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

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

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

defvar i = !add(i, 1);

变量也可以在记录体中用defvar定义。详情请参阅Defvar in a Record Body

1.6.10 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.11 if--基于一个测试的选择语句 

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

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

对值表达式进行计算。如果它的计算结果为true(与bang操作符使用的意义相同),则处理then保留字后面的语句。否则,如果有else保留字,则处理else后面的语句。如果值为false且没有其他分支,则不处理任何语句。

因为then语句周围的大括号是可选的,该语法规则与“悬空else”子句具有歧义性,并且它以通常的方式解决:在类似于if v1 then if v2 then{…else}{…}, else与内部的if相关联,而不是外部的if。

if的then和else分支的IfBody建立一个内部作用域。当主体完成时,任何在主体中定义的defvar变量都将超出作用域(更多细节请参见记录主体中的defvar)。

if语句也可以用于记录体中。

1.6.12 assert--检查一个条件为true 

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

Assert ::=  "assert" condition "," message ";"

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

  • 在顶层,断言被立即检查。
  • 在记录定义中,将保存语句,在记录完全构建后检查所有断言。
  • 在类定义中,断言由继承自该类的所有子类和记录保存和继承。然后在记录完全被构建时检查断言。
  • 在多类定义中,断言与多类的其他组件一起保存,然后在每次用defm实例化多类时检查断言。

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

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) 

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

dag实例的语法如下:

                              (operator argument1, argument2,…)

操作符必须呈现并且必须有记录。可以有零个或多个参数,用逗号分隔。操作符和参数可以有三种格式。

格式说明
value参数值
value:name参数值和对应的名称
name未初始化的参数的名称

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

下面的bang操作符对于处理DAG非常有用:!con, !dag, !empty, !foreach, !getdagop, !setdagop, !size。

1.7.2 记录体中的DefVar 

除了定义全局变量之外,defvar语句还可以在类或记录定义的Body中定义局部变量。变量的作用域从defvar语句一直延伸到记录体的末尾。不能在其作用域内将其设置为不同的值。defvar语句还可以在foreach的语句列表中使用,根据foreach确定作用域。

一个名为V的变量在内部作用域中隐藏任何外部作用域的变量V。特别是,记录体中的V会对全局V进行隐藏处理,而foreach语句列表中的V会对周围记录或全局作用域中的任何V进行隐藏处理。

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

defvar i = !add(i, 1)

 1.7.3 记录如何构建

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

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

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

  • 将父类中的字段添加到记录中。
  • 将模板参数替换到这些字段中。
  • 将父类添加到记录的继承类列表中。

3. 对记录应用所有的顶层let绑定。回想一下,顶层绑定只应用于继承的字段。

4. 解析记录主体。

  • 向记录中添加所有的字段。
  • 根据本地let语句修改字段的值。
  • 定义所有的defvar变量。

5. 遍历所有字段以解析所有的字段间引用。

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

因为在应用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

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运算符接受一个或多个参数,对它们进行操作,并产生一个结果。如果操作符产生一个布尔值,结果值为1表示真,0表示假。当bang操作符测试布尔参数时,它将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之间,或者在记录类型之间。 这允许将一条记录强制转换为一个类。 如果将记录转换为字符串,则生成记录的名称。  

如果a是一个字符串,那么它将被视为一个记录名,并在所有已定义记录的列表中查找。 结果记录应该是指定类型的。  

例如,如果!cast<type>(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)

!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节点。参数和名称参数必须是长度相等或未初始化的列表(?)。names参数必须是 list<string>类型。

由于类型系统的限制,参数必须是一个公共类型的项的列表。在实践中,这意味着它们要么具有相同的类型,要么是具有公共父类的记录。不可能将dag和非dag混合。然而,未初始化的?可以使用。

例如:!dag(op, [a1, a2, ?], ["name1", "name2", "name3"])结果为(op a1-value:$name1, a2-value:$name2, ?:$name3)。

!empty(a)

参数a可以是字符串、列表或DAG,如果a为空,则该bang操作符结果未1;否则为0。如果dag没有参数,则认为该dag为空。

!eq( ab) 

如果a = b,计算结果为1, 否则为0。参数必须是bit、bits、int、string或record值。如果是其他类型,使用!cast<string>做类型强制转换后再比较。

!filter(var, list, predicate)

这个操作符通过过滤list中的元素来创建一个新列表。为了执行过滤,TableGen将变量var绑定到每个元素,然后计算predicate表达式,该表达式可能引用var。 predicate必须产生一个布尔值(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被绑定到列表中的每个元素。对每个元素计算表达式,并假定使用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,其中每个元素都是序列list/dag中相应元素的函数。为了执行该函数,TableGen将变量var绑定到一个元素,然后计算表达式。该表达式可能指的是变量var,并计算结果值。

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

!ge(ab)

如果a大于等于b,计算结果为1, 否则为0。参数必须是bit、bits、int或者string类型的值。 

!getdagop(dag) –or– !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(ab)

如果a大于b返回1,否则返回0。参数必须是 bit、bits、nt或string 值。

!head(a)

这个操作符生成列表a的第0个元素。

!if(test, then, else)

操作符先计算test,结果必须是bit或int类型值。如果结果不为0,则生成then表达式;否则生成else表达式。

!interleave(list, delim)

该操作符连接列表list中的项,在每对之间交叉使用delim字符串,并生成结果字符串。列表元素可以是字符串、int、bits或bit类型。空列表将导致空字符串。分隔符可以是空字符串。

!isa<type>(a)

如果a的类型是给定类型的子类型,则返回1,否则返回0。

!exists<type>(name)

如果类型为type,名字为name的记录存在,则返回1,否则返回0。name应该是string类型的。

!le(a, b)

如果a小于等于b,操作符返回1,否则返回0。参数必须是bit、bits、int或者string值。

!listconcat(list1, list2, ...)

该操作符连接列表参数list1、list2等,并生成结果列表。列表必须具有相同的元素类型。

!listsplat(value, count) 

该操作符产生一个长度为count的列表,其元素都等于value。例如,!listsplat(42,3)的结果是[42,42,42]。 

!lt(ab) 

如果a小于b,返回1,否则返回0。参数必须是bit、bits、int或string值。

!mul(a, b, ...)

这个算子将a、b等相乘,得到乘积。

!ne(ab)

 如果a不等于b,这个算子返回1,否则返回0。参数必须是bit、bits、int、string或record值。如果是其他类型,使用!cast<string>强转类型后再比较。

!not(a)

该操作符对a执行逻辑NOT, a必须为整数。参数0的结果是1 (true);其他参数的结果是0 (false)。

!or(a, b, ...)

该操作符对a、b等进行按位或运算。如果所有参数都是0或1,则可以执行逻辑OR。

!setdagop(dag, op)

该操作符使用与dag相同的参数生成一个DAG节点,但dag的操作符被op替换。

例如:!setdagop((foo 1, 2), bar) 结果为 (bar 1, 2).

!shl(a, count)

该运算符对a逻辑左移count位。该操作在64位整数上执行;移位数在0…63以外的结果未定义。

!size(a)

这个操作符返回a的大小。a是字符串、列表或dag。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中除第0个元素外的所有元素。(参见!head。)

!xor(a, b, ...)

该操作符对a、b等进行位EXCLUSIVE OR运算,并产生结果。如果所有参数都是0或1,则可以执行逻辑异或。

1.11 附录B:paste操作符示例

 下面是一个演示在记录名称中使用粘贴操作符的示例。

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_int变量的值和一个文字列表。产生了以下记录。

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记录继承自8个类。尽管继承层次结构很复杂,但是使用父类要比为每个指令指定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又继承了ITy和Sched等等。字段继承自所有父类;例如,IsIndirectBranch继承自指令类。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Intel 80386 程序员参考手册是一本非常重要的技术文档,主要为程序员提供有关 Intel 80386 微处理器的详细信息和指令集。 Intel 80386 是一款32位的x86微处理器,于1985年推出,是当时电脑领域的一项重大突破,对于现代计算机技术的发展起到了重要的推动作用。 这本手册主要包含了多个部分,包括处理器的架构、寄存器和指令集等方面的详细介绍。它为程序员提供了关于寄存器模型、内存管理、中断和异常处理、保护模式等方面的深入了解,使得程序员能够更好地理解处理器的工作原理和操作方式。 不仅如此,该手册还提供了大量的指令集参考,包括算术指令、逻辑指令、控制指令等等,使得程序员能够充分利用处理器的功能和特性,编写出更高效、更优化的代码。 Intel 80386 程序员参考手册不仅适用于初学者,也是有经验的程序员的宝贵资源。它的详细内容和专业性能够帮助程序员深入了解处理器的内部工作原理,提供了丰富的资料和指导,使得程序员能够编写出更高性能、更稳定的应用程序。 总而言之,Intel 80386 程序员参考手册是一本对于程序员来说不可或缺的重要文献,它帮助程序员全面了解 Intel 80386 微处理器的特性和指令集,对于编写高效、优化的代码至关重要。无论是初学者还是有经验的程序员,都应该将这本手册作为必备的参考工具,并能够灵活运用其中的知识和指导。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值