LLVM 编译器学习笔记之三 -- TableGen语言编写*.td文件
有关于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语法介绍
注释
使用C++的注释风格://
,不过也支持C风格的嵌套注释:/* */
。
数据类型
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)
类和定义
class C { bit V = 1; }
def X : C;
def Y : C {string Greeting = "hello";
}
这个例子中定义了两条记录: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。
如果循环体只有一个表达式,可以省略{}
。
LLVM 编译器学习笔记之三 -- TableGen语言编写*.td文件相关推荐
- LLVM 编译器学习笔记之三十六-- 指令调度Instruction scheduling
1.参考Instruction scheduling in LLVM - 知乎,在中.后端均存在指令调度 GenericScheduler:: 做寄存器压力感知的指令调度 PostGenericSch ...
- LLVM 编译器学习笔记之六十五 -- GVN(global value number)
1.实现方法:https://groups.google.com/g/llvm-dev/c/MUCiFJVLDi4 The GVN algorithm used in LLVM currently ( ...
- 学习笔记之三人表决器FPGA
学习笔记之三人表决器FPGA 新手入门,多多包涵,不足错误之处,望指出 三人表决器,顾名思义就是三人投票,只要达到两票以上,就取胜. 设a,b,c为三个投票的人,输出的为f,投为1,不投为0,两票以上 ...
- 方舟编译器学习笔记分类与导读
方舟学习笔记系列,从方舟开源到现在,已经写了50多篇,保证了每天一篇的更新频率.篇数增加之后,文章的分类以及文章之间的关系,逐渐变得复杂起来.本文将对已发表的学习笔记系列进行分类和导读,方便读者更好的 ...
- 【学习笔记】C++语言程序设计(郑莉):多态性
[学习笔记]C++语言程序设计(郑莉):多态性 1. 多态性 2. 运算符重载 2.1 运算符重载的规则 2.2 运算符重载为成员函数 2.3 运算符重载为非成员函数 3. 虚函数 3.1 一般虚函数 ...
- 游戏开发学习笔记——lua脚本语言——安装、汉化与小测试(解决lua运行代码乱码问题)
游戏开发学习笔记--lua脚本语言--安装.汉化与小测试 FOR THE SIGMA FOR THE GTINDER FOR THE ROBOMASTER 简介: Lua 是一种轻量小巧的脚本语言,用 ...
- linux系统管理学习笔记之三----软件的安装
linux系统管理学习笔记之三----软件的安装 2009-12-29 19:10:02 标签:linux 系统管理 [推送到技术圈] 版权声明:原创作品,允许转载,转载时请务必以超链接形式标明文章 ...
- 23 DesignPatterns学习笔记:C++语言实现 --- 2.2 Adapter
23 DesignPatterns学习笔记:C++语言实现 --- 2.2 Adapter 2016-07-22 (www.cnblogs.com/icmzn) 模式理解 1. Adapter 定义 ...
- Python学习笔记:Day 16 编写移动App
前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...
- Python学习笔记:Day 12 编写日志列表页
前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此, 写下这些 ...
最新文章
- 自控力极差的人如何自救?两个方法战胜拖延!
- SSH方式连接Git服务器需要注意的地方
- 三个基于.net的浏览器内核使用的比较
- Daily Scrum - 11/24
- 渣本毕业两年经验,看这一篇就够了!
- ElasticSearch 2 (27) - 信息聚合系列之故事开始
- python优先级排序_用Python实现优先级队列的3种方法
- Android 手机锁屏解锁后Activity走了onDestroy
- 攻防世界:logmein
- FastText在商品分类下的应用(第十届服创大赛全国三等奖)
- java fadein_原生JS实现 fadeIn / fadeOut 方法
- 北京超级云计算中心操作训练指南
- 推荐一本书《亚马逊网络书店传奇》
- php中文拼音模糊,两种php中文字符转拼音问题解决方法
- 搭建网站,需要几种服务器?
- php中UNIX时间戳转换为日期
- Scrum:产品负责人责任
- Ubuntu/Mac/Windows与手机传输文件
- 计算机数据表示实验报告,实验报告二数据的表示
- 转移动互联网时代的9大赚钱机会
热门文章
- linux大业内存,linux 内存占用过大分析
- c语言魔方编程,用C语言编程玩转魔方阵小游戏
- webview的一些使用小窍门和需注意的地方
- 短视频sdk:选择一个靠谱的短视频SDK 你需要了解这些
- 中台能力是什么?PaaS是什么?微服务是什么?
- mysql evict_SpringBoot+Mybatis+MySQL实现读写分离
- 【python 百度指数抓取】python 模拟登陆百度指数,图像识别百度指数
- 日语随记_(文本编辑*)
- popupwindow拦截点击物理返回键
- win10计算机网线直联,教你win10两台电脑网线直连传输文件的方法