TableGen 概覽

簡介

TableGen 的目的是幫助開發人員開發和維護特定領域資訊的記錄。由於這些記錄可能數量龐大,因此 TableGen 經過特別設計,允許編寫靈活的描述,並將這些記錄的共同特徵提取出來。這減少了描述中的重複,降低了出錯的可能性,並使特定領域資訊的結構化更加容易。

TableGen 前端會解析檔案、實例化宣告,並將結果傳遞給特定領域的 後端 進行處理。如需 TableGen 的詳細說明,請參閱 TableGen 程式設計師參考手冊。如需執行各種 TableGen 類型的 *-tblgen 命令的詳細資訊,請參閱 tblgen - 描述到 C++ 程式碼

TableGen 目前主要用於 LLVM 目標獨立程式碼產生器Clang 診斷和屬性

請注意,如果您經常使用 TableGen 並使用 emacs 或 vim,您可以在 LLVM 發行版的 llvm/utils/emacsllvm/utils/vim 目錄中找到 emacs「TableGen 模式」和 vim 語言檔案。

TableGen 程式

TableGen 檔案由 TableGen 程式 llvm-tblgen 解譯,該程式位於建置目錄下的 bin 中。它沒有安裝在系統中(或您的 sysroot 設定的位置),因為它除了 LLVM 的建置過程之外沒有其他用途。

執行 TableGen

TableGen 的執行方式與其他 LLVM 工具相同。第一個(可選)參數指定要讀取的檔案。如果未指定檔名,llvm-tblgen 會從標準輸入讀取。

要使用 TableGen,必須使用其中一個 後端。這些後端可以在命令列上選擇(輸入「llvm-tblgen -help」以查看清單)。例如,要取得繼承特定類型的所有定義的清單(這對於建置這些記錄的列舉清單很有用),請使用 -print-enums 選項

$ llvm-tblgen X86.td -print-enums -class=Register
AH, AL, AX, BH, BL, BP, BPL, BX, CH, CL, CX, DH, DI, DIL, DL, DX, EAX, EBP, EBX,
ECX, EDI, EDX, EFLAGS, EIP, ESI, ESP, FP0, FP1, FP2, FP3, FP4, FP5, FP6, IP,
MM0, MM1, MM2, MM3, MM4, MM5, MM6, MM7, R10, R10B, R10D, R10W, R11, R11B, R11D,
R11W, R12, R12B, R12D, R12W, R13, R13B, R13D, R13W, R14, R14B, R14D, R14W, R15,
R15B, R15D, R15W, R8, R8B, R8D, R8W, R9, R9B, R9D, R9W, RAX, RBP, RBX, RCX, RDI,
RDX, RIP, RSI, RSP, SI, SIL, SP, SPL, ST0, ST1, ST2, ST3, ST4, ST5, ST6, ST7,
XMM0, XMM1, XMM10, XMM11, XMM12, XMM13, XMM14, XMM15, XMM2, XMM3, XMM4, XMM5,
XMM6, XMM7, XMM8, XMM9,

$ llvm-tblgen X86.td -print-enums -class=Instruction
ABS_F, ABS_Fp32, ABS_Fp64, ABS_Fp80, ADC32mi, ADC32mi8, ADC32mr, ADC32ri,
ADC32ri8, ADC32rm, ADC32rr, ADC64mi32, ADC64mi8, ADC64mr, ADC64ri32, ADC64ri8,
ADC64rm, ADC64rr, ADD16mi, ADD16mi8, ADD16mr, ADD16ri, ADD16ri8, ADD16rm,
ADD16rr, ADD32mi, ADD32mi8, ADD32mr, ADD32ri, ADD32ri8, ADD32rm, ADD32rr,
ADD64mi32, ADD64mi8, ADD64mr, ADD64ri32, ...

預設後端會印出所有記錄。還有一個通用後端,它使用 -dump-json 選項啟用,會將所有記錄輸出為 JSON 資料結構。

如果您打算使用 TableGen,您很可能會需要寫一個 後端,它會提取您需要的特定資訊,並以適當的方式格式化。您可以透過在 C++ 中擴展 TableGen 本身,或使用任何可以消耗 JSON 輸出的語言編寫腳本來做到這一點。

範例

如果沒有其他參數,llvm-tblgen 會解析指定的檔案並印出所有類別,然後是所有定義。這是一個查看各種定義會完整展開成什麼的好方法。在 X86.td 檔案上執行此操作會印出這個(在撰寫本文時)

...
def ADD32rr {   // Instruction X86Inst I
  string Namespace = "X86";
  dag OutOperandList = (outs GR32:$dst);
  dag InOperandList = (ins GR32:$src1, GR32:$src2);
  string AsmString = "add{l}\t{$src2, $dst|$dst, $src2}";
  list<dag> Pattern = [(set GR32:$dst, (add GR32:$src1, GR32:$src2))];
  list<Register> Uses = [];
  list<Register> Defs = [EFLAGS];
  list<Predicate> Predicates = [];
  int CodeSize = 3;
  int AddedComplexity = 0;
  bit isReturn = 0;
  bit isBranch = 0;
  bit isIndirectBranch = 0;
  bit isBarrier = 0;
  bit isCall = 0;
  bit canFoldAsLoad = 0;
  bit mayLoad = 0;
  bit mayStore = 0;
  bit isImplicitDef = 0;
  bit isConvertibleToThreeAddress = 1;
  bit isCommutable = 1;
  bit isTerminator = 0;
  bit isReMaterializable = 0;
  bit isPredicable = 0;
  bit hasDelaySlot = 0;
  bit usesCustomInserter = 0;
  bit hasCtrlDep = 0;
  bit isNotDuplicable = 0;
  bit hasSideEffects = 0;
  InstrItinClass Itinerary = NoItinerary;
  string Constraints = "";
  string DisableEncoding = "";
  bits<8> Opcode = { 0, 0, 0, 0, 0, 0, 0, 1 };
  Format Form = MRMDestReg;
  bits<6> FormBits = { 0, 0, 0, 0, 1, 1 };
  ImmType ImmT = NoImm;
  bits<3> ImmTypeBits = { 0, 0, 0 };
  bit hasOpSizePrefix = 0;
  bit hasAdSizePrefix = 0;
  bits<4> Prefix = { 0, 0, 0, 0 };
  bit hasREX_WPrefix = 0;
  FPFormat FPForm = ?;
  bits<3> FPFormBits = { 0, 0, 0 };
}
...

此定義對應於 x86 架構的 32 位元暫存器對暫存器 add 指令。def ADD32rr 定義了一個名為 ADD32rr 的記錄,並且行尾的註釋表示定義的超類。記錄的本文包含 TableGen 為記錄組裝的所有資料,表示指令是「X86」命名空間的一部分,模式表示程式碼產生器如何選擇指令,它是一個雙位址指令,具有特定的編碼等等。記錄中資訊的內容和語義特定於 X86 後端的需要,並且僅作為範例顯示。

如您所見,程式碼產生器支援的每條指令都需要大量資訊,而且手動指定所有資訊將難以維護、容易出錯,並且一開始就很麻煩。因為我們使用的是 TableGen,所有資訊都來自以下定義

let Defs = [EFLAGS],
    isCommutable = 1,                  // X = ADD Y,Z --> X = ADD Z,Y
    isConvertibleToThreeAddress = 1 in // Can transform into LEA.
def ADD32rr  : I<0x01, MRMDestReg, (outs GR32:$dst),
                                   (ins GR32:$src1, GR32:$src2),
                 "add{l}\t{$src2, $dst|$dst, $src2}",
                 [(set GR32:$dst, (add GR32:$src1, GR32:$src2))]>;

此定義利用了自訂類別 I(從自訂類別 X86Inst 擴展而來),該類別定義在 X86 特定的 TableGen 檔案中,用於分解其類別指令共有的共同特徵。TableGen 的一個關鍵特性是它允許最終使用者定義他們在描述資訊時喜歡使用的抽象化。

語法

TableGen 的語法大致基於 C++ 範本,具有內建類型和規範。此外,TableGen 的語法還引入了一些自動化概念,例如多類別、foreach、let 等等。

基本概念

TableGen 檔案由兩個關鍵部分組成:「類別」和「定義」,兩者都被視為「記錄」。

TableGen 記錄具有唯一的名稱、值清單和超類別清單。值清單是 TableGen 為每個記錄建立的主要資料;它保存了應用程式的特定領域資訊。這些資料的解釋留給特定的 後端,但結構和格式規則由 TableGen 負責處理並固定。

TableGen 定義是「記錄」的具體形式。這些通常没有任何未定義的值,並以「def」關鍵字標記。

def FeatureFPARMv8 : SubtargetFeature<"fp-armv8", "HasFPARMv8", "true",
                                      "Enable ARMv8 FP">;

在這個例子中,FeatureFPARMv8 是一個以某些值初始化的 SubtargetFeature 記錄。類別的名稱是透過關鍵字 class 定義的,可以在同一個檔案中或其他包含的檔案中定義。大多數目標 TableGen 檔案都包含在 include/llvm/Target 中的通用檔案。

TableGen 類別是用於構建和描述其他記錄的抽象記錄。這些類別允許最終用戶為他們所針對的領域(例如 LLVM 代碼生成器中的「Register」、「RegisterClass」和「Instruction」)或為實現者構建抽象,以幫助分解記錄的共同屬性(例如「FPInst」,用於表示 X86 後端中的浮點指令)。TableGen 會追蹤用於構建定義的所有類別,因此後端可以找到特定類別的所有定義,例如「Instruction」。

class ProcNoItin<string Name, list<SubtargetFeature> Features>
      : Processor<Name, NoItineraries, Features>;

在這裡,類別 ProcNoItin 接收類型為 string 的參數 Name 和一個目標功能列表,透過傳遞參數以及硬編碼 NoItineraries 來特化類別 Processor。

TableGen 多類別是同時實例化的抽象記錄組。每個實例化都可以產生多個 TableGen 定義。如果一個多類別繼承自另一個多類別,則子多類別中的定義將成為當前多類別的一部分,就像它們是在當前多類別中聲明的一樣。

multiclass ro_signed_pats<string T, string Rm, dag Base, dag Offset, dag Extend,
                        dag address, ValueType sty> {
def : Pat<(i32 (!cast<SDNode>("sextload" # sty) address)),
          (!cast<Instruction>("LDRS" # T # "w_" # Rm # "_RegOffset")
            Base, Offset, Extend)>;

def : Pat<(i64 (!cast<SDNode>("sextload" # sty) address)),
          (!cast<Instruction>("LDRS" # T # "x_" # Rm # "_RegOffset")
            Base, Offset, Extend)>;
}

defm : ro_signed_pats<"B", Rm, Base, Offset, Extend,
                      !foreach(decls.pattern, address,
                               !subst(SHIFT, imm_eq0, decls.pattern)),
                      i8>;

如需 TableGen 的詳細說明,請參閱 TableGen 程序員參考

TableGen 後端

如果沒有後端,TableGen 檔案就沒有真正的意義。運行 *-tblgen 時的預設操作是以文字格式打印信息,但这僅對調試 TableGen 檔案本身有用。然而,TableGen 的強大之處在於將源檔案解釋為內部表示形式,然後可以將其生成為您想要的任何內容。

TableGen 的當前用法是創建包含表的巨大包含檔案,您可以直接包含這些檔案(如果輸出是您正在編碼的語言),或者透過包含檔案周圍的宏在預處理中使用它們。

如果後端已經以 C 格式打印表格,或者輸出只是一系列字符串(用於錯誤和警告消息),則可以使用直接輸出。如果需要在不同的上下文中使用相同的信息(例如指令名稱),則應使用預處理輸出,因此您的後端應打印一個元信息列表,該列表可以轉換為不同的編譯時格式。

有關可用後端的列表,請參閱 TableGen 後端,有關如何編寫和調試新後端的資訊,請參閱 TableGen 後端開發人員指南

工具和資源

除了本文檔之外,還可以在 TableGen 的 README 中找到 TableGen 的工具和資源列表。

TableGen 的缺陷

儘管 TableGen 非常通用,但它也有一些缺陷,這些缺陷已經被多次指出。常見的主題是,雖然 TableGen 允許您構建特定領域的語言,但您創建的最終語言缺乏其他 DSL 的強大功能,這反過來又大大增加了 TableGen 檔案的大小和複雜性。

同時,TableGen 允許您透過自訂後端,賦予基本概念幾乎任何意義,這可能會扭曲原始設計,讓新手難以理解邪惡的 TableGen 檔案。

有些人贊成進一步擴展語義,但要確保後端遵循嚴格的規則。其他人則建議我們應該轉向功能更強大、專為特定目的設計的 DSL,甚至重複使用現有的 DSL。