TableGen 語言語法介紹

有關於TableGen語言語法的文章,LLVM官方發佈有兩篇,第一篇是:TableGen Language Introduction,第二篇是:TableGen Language Reference。文章開頭聲明說,第一篇不是規範的參考文檔,第二篇是規範的參考文檔,並且兩篇都有點年久失修。我把兩篇都看了一下,確實感覺第二篇更規範一些,尤其是語法描述的章節,特別嚴謹。但是,我這裏還是選擇以第一篇的內容作爲參考文檔,主要是因爲從易讀性的角度來說,第一篇更容易理解(讀者友好型),當然,第二篇作爲參考查閱更便捷(活學活用Ctrl-f),關鍵在於能通過學習,掌握TableGen語言語法。

需要說明:本文中提到的記錄是指官方文章中的record,即通過defdefm等語法定義的實例化的數據對象。

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”:單行的字符串值,可以直接賦給stringcode

  • [{ … }]:代碼片段,通常用於賦給code,但其實就是多行字符串值;

  • [ X, Y, Z ]<type>:列表,type是指定列表中元素類型,一般情況下可省略,TableGen前端能夠推測類型,極少數特殊情況下需要明確指定;

  • { a, b, 0b10 }:初始化bits<n>這樣的類型,第一位是a變量的值,第二位是b變量的值,第三位和第四位是’10’;

  • 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)

  • !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,注意這個只對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";
}

這個例子中定義了兩條記錄:XY,這兩條記錄具有相同的信息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後邊的名字和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>;

實際上定義的是: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中;如果classmulticlass中出現相同屬性,則以後邊最後一個的值爲準。

總之,語言規範就放在那裏,自己的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。

如果循環體只有一個表達式,可以省略{}

發佈了42 篇原創文章 · 獲贊 20 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章