有關於TableGen語言語法的文章,LLVM官方發佈有兩篇,第一篇是:TableGen Language Introduction,第二篇是:TableGen Language Reference。文章開頭聲明說,第一篇不是規範的參考文檔,第二篇是規範的參考文檔,並且兩篇都有點年久失修。我把兩篇都看了一下,確實感覺第二篇更規範一些,尤其是語法描述的章節,特別嚴謹。但是,我這裏還是選擇以第一篇的內容作爲參考文檔,主要是因爲從易讀性的角度來說,第一篇更容易理解(讀者友好型),當然,第二篇作爲參考查閱更便捷(活學活用Ctrl-f),關鍵在於能通過學習,掌握TableGen語言語法。
需要說明:本文中提到的
記錄
是指官方文章中的record
,即通過def
或defm
等語法定義的實例化的數據對象。
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++中的通用容器類;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變量的值,第三位和第四位是’10’; -
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)
。 -
!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會首先將記錄轉換爲子類,如果類型不匹配,則不會做常量摺疊。如果時記錄類型的強制轉換,則會返回一個類型名;
-
!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)
語法; -
!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";
}
這個例子中定義了兩條記錄:X
和Y
,這兩條記錄具有相同的信息V
,所以用了一個類C
來實現共有部分,同時,還擴展了記錄Y
,使它多了一個Greeting的字符串。
通常來說,類用來實現一組相似的記錄中共有的部分,然後把類會單獨放在一個位置,同時,類也允許在它的子類或實現中用特殊值覆蓋默認值,比如可以在上例中的X
中給V
重新賦值。
值的定義
值的定義是記錄中的內容條目,必須先定義這個值,才能在其他值定義中引用這個值,或者可以使用let
來重置這個值。值的定義的組成結構:類型 + 值名字
,指定值的內容可以通過在後邊跟等號+值的內容來完成,需要有終止符;
。
Let表達式
記錄中的Let表達式被用於改變一個記錄中某個值的內容。經常在當一個類定義了值而它的子類需要覆蓋這個值的時候使用。Let表達式的組成結構:let + 值名字 + = + 新的值內容
。比如如下例子:
class D : C { let V = 0; }
def Z : D;
這個例子中,父類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;
}
這個例子中,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>;
這個例子中,實現了一個類似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>;
通過llvm-tblgen
工具測試這個代碼:
$ llvm-tblgen --print-records example.td
得到結果如下:
def bork { // Value
bit isMod = 1;
bit isRef = 0;
}
def hork { // Value
bit isMod = 1;
bit isRef = 1;
}
def zork { // Value
bit isMod = 0;
bit isRef = 1;
}
可見,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">;
...
最終的記錄的名字是由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>;
實際上定義的是: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;
實際上定義的結果是:
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 };
}
如果有多個multiclass
和一個class
,那麼這個class
的內容會合併到每個multiclass
中;如果有多個class
,則所有的class
的內容合併起來,再合併到每個multiclass
中;如果class
、multiclass
中出現相同屬性,則以後邊最後一個的值爲準。
總之,語言規範就放在那裏,自己的td寫的越複雜,只會給自己和團隊帶來更多的理解難度,我的建議時,multiclass
是很便捷的東西,應該儘量使用,但不要過度使用。TableGen的靈活性比較大,我覺得這是一個原因。
文件相關
文件包含
TableGen支持include
關鍵字,能夠擴展其他的td文件到當前的td文件中,和C系的文件包含意思一樣。要包含的文件名用""
包含。比如:
include "foo.td"
需要注意的是,沒有#
開頭,因爲TableGen裏邊沒有C系的預處理概念。
Let表達式
Let表達式在文件中的作用和在記錄中的作用基本相同,不同的是,文件中的let表達式可以有多個值來綁定多個記錄,它是另一種能夠提取記錄的公共部分的一種方式。
下邊是一個例子:
let isTerminator = 1, isReturn = 1, isBarrier = 1, hasCtrlDep = 1 in
def 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", []>;
}
能注意到,這個文件內的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<...>;
}
經過4次循環,分別定義了:R0、R1、R2、R3、F0、F1、F2、F3。
如果循環體只有一個表達式,可以省略{}
。