当前位置: 代码迷 >> 综合 >> LLVM 编译器学习笔记之三 -- TableGen语言编写*.td文件
  详细解决方案

LLVM 编译器学习笔记之三 -- TableGen语言编写*.td文件

热度:90   发布时间:2024-02-27 10:45:32.0

 

有关于TableGen语言语法的文章,LLVM官方发布有两篇,第一篇是:TableGen Language Introduction,第二篇是:TableGen Language Reference(version llvm 10.0.0)。文章开头声明说,第一篇不是规范的参考文档,第二篇是规范的参考文档,并且两篇都有点年久失修。我把两篇都看了一下,确实感觉第二篇更规范一些,尤其是语法描述的章节,特别严谨。但是,我这里还是选择以第一篇的内容作为参考文档,主要是因为从易读性的角度来说,第一篇更容易理解(读者友好型),当然,第二篇作为参考查阅更便捷(活学活用Ctrl-f),关键在于能通过学习,掌握TableGen语言语法。

需要说明:本文中提到的记录是指官方文章中的record,即通过defdefm等语法定义的实例化的数据对象。

 

文章目录

    • 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”:单行的字符串值,可以直接赋给stringcode

  • [{ … }]:代码片段,通常用于赋给code,但其实就是多行字符串值;

  • [ X, Y, Z ]<type>:列表,type是指定列表中元素类型,一般情况下可省略,TableGen前端能够推测类型,极少数特殊情况下需要明确指定;

  • { a, b, 0b10 }:初始化bits<n>这样的类型,第一位是a变量的值,第二位是b变量的值,第三位和第四位是’0b1’ 和 ‘0b0’;

  • value:值的引用,比如上边出现的XYZab

  • 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,注意这个只对stringintbit对象生效,其他类型可以尝试先做!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中最重要的信息承载类型。记录被defclass这样的关键词所描述。类还能扩展出参数模版、继承、多类等表现形式。配合值定义和let关键字,还可以让实现代码更简洁(甚至不需要{}来展开一个类或定义)。

一个简单的例子:

class C { bit V = 1; }
def X : C;
def Y : C {string Greeting = "hello";
}
  • 1
  • 2
  • 3
  • 4
  • 5

这个例子中定义了两条记录:XY,这两条记录具有相同的信息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后边的名字和multiclassdef关键字后边的名字拼接而成的,也就是实际上定义了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中;如果classmulticlass中出现相同属性,则以后边最后一个的值为准。

总之,语言规范就放在那里,自己的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。

如果循环体只有一个表达式,可以省略{}