有关于TableGen语言语法的文章,LLVM官方发布有两篇,第一篇是:TableGen Language Introduction,第二篇是:TableGen Language Reference(version llvm 10.0.0)。文章开头声明说,第一篇不是规范的参考文档,第二篇是规范的参考文档,并且两篇都有点年久失修。我把两篇都看了一下,确实感觉第二篇更规范一些,尤其是语法描述的章节,特别严谨。但是,我这里还是选择以第一篇的内容作为参考文档,主要是因为从易读性的角度来说,第一篇更容易理解(读者友好型),当然,第二篇作为参考查阅更便捷(活学活用Ctrl-f),关键在于能通过学习,掌握TableGen语言语法。
需要说明:本文中提到的
记录
是指官方文章中的record
,即通过def
或defm
等语法定义的实例化的数据对象。
文章目录
-
- TableGen语法的特点
- TableGen语法介绍
-
- 注释
- 数据类型
- 值和表达式
- 类和定义
-
- 值的定义
- Let表达式
- 类模版参数
- `multiclass`定义和实例化
- 文件相关
-
- 文件包含
- Let表达式
- 循环
TableGen语法的特点
在我看来,TableGen的语法使用起来特别的灵活,但它还确实是一个强类型的语法,由于TableGen的作用只是用来描述信息,所以几乎没有控制流的语法规范,它也不关心语言本身的意义是否正确(由TableGen后端关心),它只关心语言本身的语法是否合法。它具有一些不同数据类型的定义,也支持一定程度的类型转换,数据类型范围特别宽泛。
以下介绍一下主要的语法。
TableGen语法介绍
注释
使用C++的注释风格://
,不过也支持C风格的嵌套注释:/* */
。
数据类型
由于它是一个强类型语言,所以我们需要在编写td文件时考虑到数据格式规范。另外,它的覆盖范围特别广,小到位类型,大到dag类型,还支持类参数类型的扩展和列表的扩展,这使得TableGen非常的灵活和易于使用。
主要类型有:
bit
:一位就是表示一个布尔值,比如0或者1;int
:表示一个32位的整形值,比如5;string
:表示一个有序的固定长度的字符序列,比如“add”;code
:表示一个代码片段,可以是单行或多行,不过其实和string
本质一样,只是展示的意义不同而已;bits<n>
:bit
的扩展,可以指定n个位同时赋值,比如当n=3,可以是010,指定位模式时特别常用;list<ty>
:很灵活的一个类型,可以保存指定ty类型的数据的列表,ty类型也可以是另一个list<ty>
,可以理解是C++中的List模板类;class
:指定一些类型数据的集合表示,必须用def或defm来使用这个类定义记录之后,内部数据才被分配。用来声明多个记录的共有信息,支持继承和重载等特性;dag
:表示可嵌套的有向无环图元素;
原文中指出:当前这些数据类型已经足够使用,如果日后还添加其他数据类型,再另行发布(我也不知道会不会更新这篇文章,以官方为准)
值和表达式
也非常灵活,有些时候,def的参数列表搞的非常长,就是因为这一部分,阅读代码会比较累,也没办法。
-
?:未定义;
-
0b1001001:位值,注意它的长度是固定的,它不会自动扩展或截断;
-
7:十进制数;
-
0x7F:十六进制数;
-
“foo”:单行的字符串值,可以直接赋给
string
或code
; -
[{ … }]:代码片段,通常用于赋给
code
,但其实就是多行字符串值; -
[ X, Y, Z ]<type>:列表,type是指定列表中元素类型,一般情况下可省略,TableGen前端能够推测类型,极少数特殊情况下需要明确指定;
-
{ a, b, 0b10 }:初始化
bits<n>
这样的类型,第一位是a变量的值,第二位是b变量的值,第三位和第四位是’0b1’ 和 ‘0b0’; -
value:值的引用,比如上边出现的
X
,Y
,Z
,a
,b
; -
value{17}:值的引用并截取一位;
-
value{15-17}:值的引用并截取一部分位,
-
两边必须要连续 -
DEF:记录的引用;
-
CLASS<val list>:匿名定义的引用,
<val list>
是模版参数,这里是这个意思,对于一个带模版参数的类,通过指定模版参数,可以直接定义一个匿名的记录,这里就是引用这个匿名记录; -
X.Y:引用一个值的子域,常用在记录上;
-
list[4-7,17,2-3]:列表片段,比如这个例子中,引用了列表list的第4、5、6、7、17、2、3这几位;
-
foreach <var> = [ <list> ] in { <body> }:一种循环结构,依次将list中的值赋给var,并执行body体,类似于C++11中的foreach,body中仅可以包含def和defm;
-
foreach <var> = [ <list> ] in <def>:同上,不同是循环体只有一条语句,不需要用
{}
; -
foreach <var> = 0-15 in …:同上,不同的是循环的是整数;
-
foreach <var> = {0-15,32-47} in …:同上,不同的是循环的是几个整数片段;
-
(DEF a, b, …):这是dag类型的表示,第一个参数
DEF
是一个记录的定义,剩下的参数可以是其他类型值,当然也包括嵌套的dag类型值,除第一个参数外,其他参数可省略; -
!con(a, b, …):将两个或多个dag类型节点连接,它们的操作码必须相同;
例子:
!con((op a1:$name1, a2:$name2), (op b1:$name3))
,等效于(op a1:$name1, a2:$name2, b1:$name3)
。其中 a1,a2,b1 均表示接后边值的类型。 -
!dag(op, children, names):生成一个dag节点,children和names必须有相同的长度的列表或者是
?
,names必须是list<string>
类型,children必须是常见类型的列表,children列表中的类型必须是相同的或者它们的祖先类是相同的,不支持混合的dag和non-dag;例子:
!dag(op, [a1, a2, ?], ["name1", "name2", "name3"])
,等效于(op a1:$name1, a2:$name2, ?:name3)
。 -
!listconcat(a, b, …):将两个或多个列表合并成一个列表,这些子列表必须具有相同的子项类型;
-
!listsplat(a, size):将指定a列表中的子项重复包含size次;
例子:
!listsplat(0, 2)
,等效于[0, 0]
。 -
!strconcat(a, b, …):将两个或多个字符串合并成一个字符串;
-
!str1#str2:将两个字符串合并成一个字符串,是
!strconcat(a, b)
的简化用法,如果其中有不是字符串的类型,TableGen前端会隐式调用!cast<string>
操作转换为字符串类型。 -
!cast<type>(a):强制类型转换。(注:这段我方了,没有完全理解,后边解释不用看,感兴趣的同学回去翻原文吧)
如果a是字符串,而type是记录类型,那么将会通过完全检查a和所有记录之间的匹配,并确保匹配记录已经完整声明。这个完整声明的意思是,如果这个记录是在含参模版类中,那么这个记录在定义时,必须要求该类和其内部可能有的其他含参类都已指明类参数值;而如果还没有指定完整的类参数值,那么会在指定完整类参数值之后再执行这次转换,而如果没有匹配到任何记录,则会报错。若type只是简单的值类型,比如bit或int之间,或记录之间。对于记录之间的情况,cast会首先将记录转换为子类,如果类型不匹配,则不会做常量折叠。!cast 是一个特殊的情况,他的参数 a 可以是一个 int 或记录类型,如果满足记录类型的转换,则会返回一个类型名; -
!isa<type>(a):返回布尔值,如果a是type类型,返回1,否则返回0;
-
!subst(a, b, c):如果a和b是字符串类型或者是引用类型,将c中的b替换为a,类似于GNU make中的
$(subst)
语法; -
!foreach(a, b, c):对于b中的每一个值,将c中的b替换为a,类似于GNU make中的
$(foreach)
语法(不是 C++ 中的那个 foreach); -
!foldl(start, lst, a, b, expr):使用给定的start,对lst做left-fold操作。a和b是变量名,将在expr中被替换,如果expr视为函数
f(a,b)
,则left-fold将做这样的操作:f((...f(f(start, lst[0]), lst[1]), ...), lst[n-1])
,循环次数取决于lst的长度n。a与start相同类型,b与lst中元素相同类型,expr和start相同类型; -
!head(a):取列表a的第一个元素;
-
!tail(a):取列表a的最后一个元素(原文中是:`The 2nd-N elements of list ‘a’,是同一个意思吗);
-
!empty(a):返回布尔值,列表a是否为空;
-
!size(a):返回一个整数,表示列表a的元素个数;
-
!if(a, b, c):类似C中的三元操作符:
? :
,如果a为非0,返回b,否则返回c; -
!cond(condition_1 : val1, condition_2: val2, …, condition_n : valn):是
!if(a, b, c)
的扩展,避免多次的if嵌套。如果condition_1满足,返回val1,否则如果condition_2满足,返回val2,以此类推,如果condition_n仍然不满足,返回错误;例子:
!cond(!lt(x, 0) : "negative", !eq(x, 0) : "zero", 1 : "positive")
,注意到最后一个条件是恒成立,避免了可能的报错。 -
!eq(a, b):返回布尔值,如果a和b相等,返回1,否则返回0,注意这个只对
string
、int
和bit
对象生效,其他类型可以尝试先做!cast<string>
操作; -
!ne(a, b):返回布尔值,如果a和b不相等,返回1,否则返回0,a、b取值类型同
!eq(a, b)
。 -
!le(a, b), !lt(a, b), !ge(a, b), !gt(a, b):返回布尔值,分别是小于等于(little equal),小于(little than),大于等于(great equal),大于(great than),成立返回1,否则返回0;
-
!shl(a, b), !srl(a, b), !sra(a, b):位移操作,分别是逻辑左移(shift left logical),逻辑右移(shift right logical),算数右移(shift right arithmetic)。操作64位整数,如果如果移位大于63或小于0,则结果是无效的;
-
!add(a, b, …), !mul(a, b, …), !and(a, b, …), !or(a, b, …):运算指令,加(add),乘(mul),与(and), 或(or)
类和定义
类和定义(也叫做记录)是TableGen中最重要的信息承载类型。记录被def
和class
这样的关键词所描述。类还能扩展出参数模版、继承、多类等表现形式。配合值定义和let
关键字,还可以让实现代码更简洁(甚至不需要{}
来展开一个类或定义)。
一个简单的例子:
class C { bit V = 1; }
def X : C;
def Y : C {string Greeting = "hello";
}
- 1
- 2
- 3
- 4
- 5
这个例子中定义了两条记录:X
和Y
,这两条记录具有相同的信息V
,所以用了一个类C
来实现共有部分,同时,还扩展了记录Y
,使它多了一个Greeting的字符串。
通常来说,类用来实现一组相似的记录中共有的部分,然后把类会单独放在一个位置,同时,类也允许在它的子类或实现中用特殊值覆盖默认值,比如可以在上例中的X
中给V
重新赋值。
值的定义
值的定义是记录中的内容条目,必须先定义这个值,才能在其他值定义中引用这个值,或者可以使用let
来重置这个值。值的定义的组成结构:类型 + 值名字
,指定值的内容可以通过在后边跟等号+值的内容来完成,需要有终止符;
。
Let表达式
记录中的Let表达式被用于改变一个记录中某个值的内容。经常在当一个类定义了值而它的子类需要覆盖这个值的时候使用。Let表达式的组成结构:let + 值名字 + = + 新的值内容
。比如如下例子:
class D : C { let V = 0; }
def Z : D;
- 1
- 2
这个例子中,父类C中包含的V值在子类D中被重新赋值为0,而Z是D的实现,从而Z中的V值的内容为0。
需要注意的是,记录中变量的重新赋值实现的相对较晚,也就是在类内的值初始化完毕后才实现记录中的值赋值,如下例:
class A<int x> {int Y = x;int Yplus1 = !add(Y, 1);int xplus1 = !add(x, 1);
}
def Z : A<5> {let Y = 10;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
这个例子中,Z.xplus1的内容是6,而Z.Yplus1的内容是11,这是因为类A中先将所有x替换为5,然后将Y=5,然后才执行Let表达式,将Y=10。使用这种语法时要谨慎,多用llvm-tblgen
去测试。
另外,在multiclass
中,也可以使用Let表达式,尤其是在多层的multiclass
中,这使得TableGen的语法更为灵活。有关于multiclass
的语法下文中会讲到。
类模版参数
TableGen提供了这种含参类的功能,允许给类传参。模版类中的值是在实现记录时绑定的。比如下边例子:
class FPFormat<bits<3> val> {bits<3> Value = val;
}
def NotFP : FPFormat<0>;
def ZeroFP : 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>;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
这个例子中,实现了一个类似enum的模式,不同的记录中的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>;class Value<ModRefVal MR> {// Decode some information into a more convenient format, while providing// a nice interface to the user of the "Value" class.bit isMod = MR.Value{0};bit isRef = MR.Value{1};// other stuff...
}// Example uses
def bork : Value<Mod>;
def zork : Value<Ref>;
def hork : Value<ModRef>;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
通过llvm-tblgen
工具测试这个代码:
$ llvm-tblgen --print-records example.td
- 1
得到结果如下:
def bork { // Valuebit isMod = 1;bit isRef = 0;
}
def hork { // Valuebit isMod = 1;bit isRef = 1;
}
def zork { // Valuebit isMod = 0;bit isRef = 1;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
可见,TableGen可以展开所有记录的内容,并显示给开发者检查。
multiclass
定义和实例化
multiclass
并不是指多个类,而是指用一个类结构来实现多个类的功能。简单来说,就是带参数的类,如果有两个或多个记录具有一组相同的公共属性,那么用多类来声明这组属性,可以一定程度上减少代码量,也使得代码结构更加清晰。比方说,经常见到的3地址指令,第3个操作数可能是寄存器或者是立即数,这样我们就能用一个multiclass
来实现3地址指令模版的共有部分,然后用一个defm
来定义这两种不同的指令模式。示例如下:
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)>;
}// Instantiations of the ri_inst multiclass.
defm ADD : ri_inst<0b111, "add">;
defm SUB : ri_inst<0b101, "sub">;
defm MUL : ri_inst<0b100, "mul">;
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
最终的记录的名字是由defm
后边的名字和multiclass
中def
关键字后边的名字拼接而成的,也就是实际上定义了ADD_rr、ADD_ri、SUB_rr、SUB_ri、MUL_rr、MUL_ri等指令。defm
可以实现多个multiclass
,最终的实现会比较复杂些,不过也好理解。比如下边这个例子:
class Instruction<bits<4> opc, string Name> {bits<4> opcode = opc;string name = Name;
}multiclass basic_r<bits<4> opc> {def rm : Instruction<opc, "rm">;def rr : Instruction<opc, "rr">;
}multiclass basic_s<bits<4> opc> {defm SD : basic_r<opc>;defm SS : basic_r<opc>;def X : Instruction<opc, "x">;
}multiclass basic_p<bits<4> opc> {defm PD : basic_r<opc>;defm PS : basic_r<opc>;def Y : Instruction<opc, "y">;
}defm ADD : basic_p<0xf>, basic_s<0xf>;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
实际上定义的是:ADDPDrm、ADDPDrr、ADDPSrm、ADDPSrr、ADDSDrm、ADDSDrr、ADDSSrm、ADDSSrr、ADDY、ADDX这几个指令。
类似的,defm
在实现多个类时,可以即有multiclass
也有class
,但必须至少有一个multiclass
,且所有class
的列表必须在multiclass
后边。这种写法下,class
的内容是和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 rm : I<2>;def rr : I<4>;
}multiclass Y {defm SD : R, XS;defm SS : R, XD;
}defm Instr : Y;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
实际上定义的结果是:
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 };
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
如果有多个multiclass
和一个class
,那么这个class
的内容会合并到每个multiclass
中;如果有多个class
,则所有的class
的内容合并起来,再合并到每个multiclass
中;如果class
、multiclass
中出现相同属性,则以后边最后一个的值为准。
总之,语言规范就放在那里,自己的td写的越复杂,只会给自己和团队带来更多的理解难度,我的建议时,multiclass
是很便捷的东西,应该尽量使用,但不要过度使用。TableGen的灵活性比较大,我觉得这是一个原因。
文件相关
文件包含
TableGen支持include
关键字,能够扩展其他的td文件到当前的td文件中,和C系的文件包含意思一样。要包含的文件名用""
包含。比如:
include "foo.td"
- 1
需要注意的是,没有#
开头,因为TableGen里边没有C系的预处理概念。
Let表达式
Let表达式在文件中的作用和在记录中的作用基本相同,不同的是,文件中的let表达式可以有多个值来绑定多个记录,它是另一种能够提取记录的公共部分的一种方式。
下边是一个例子:
let isTerminator = 1, isReturn = 1, isBarrier = 1, hasCtrlDep = 1 indef RET : I<0xC3, RawFrm, (outs), (ins), "ret", [(X86retflag 0)]>;let isCall = 1 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", []>;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
能注意到,这个文件内的let表达式,使用let ... in { ... }
的语法,把多个记录包含在大括号内。Let表达式在文件内经常用于在一系列的记录中增加一些定义,这些记录不需要被展开,就像上例中,那几个CALL指令,没有展开即加入了isCall=1和Defs=[…]的定义属性。
循环
TableGen支持循环模块foreach
,可以做循环操作,例如:
foreach i = [0, 1, 2, 3] in {def R#i : Register<...>;def F#i : Register<...>;
}
- 1
- 2
- 3
- 4
经过4次循环,分别定义了:R0、R1、R2、R3、F0、F1、F2、F3。
如果循环体只有一个表达式,可以省略{}
。