1 TableGen 程序員參考手冊

1.1 簡介

TableGen 的目的是根據來源檔案中的資訊產生複雜的輸出檔案,這些來源檔案比輸出檔案更容易編寫程式碼,並且也更容易隨著時間推移進行維護和修改。資訊以涉及類別和記錄的宣告方式編碼,然後由 TableGen 處理。內部化的記錄會傳遞給各種*後端*,後端會從記錄子集中提取資訊並產生一個或多個輸出檔案。這些輸出檔案通常是 C++ 的 .inc 檔案,但也可以是後端開發人員需要的任何類型的檔案。

本文檔詳細描述了 LLVM TableGen 工具。它適用於使用 TableGen 為專案產生程式碼的程式設計師。如果您正在尋找簡單的概述,請查看TableGen 概述。用於叫用 TableGen 的各種 *-tblgen 命令在tblgen 系列 - 描述到 C++ 程式碼 中有所描述。

後端的一個例子是 RegisterInfo,它為特定的目標機器產生暫存器檔案資訊,供 LLVM 目標無關程式碼產生器使用。有關 LLVM TableGen 後端的描述,請參閱TableGen 後端,有關編寫新後端的指南,請參閱TableGen 後端開發人員指南

以下是後端可以執行的幾項操作。

  • 為特定的目標機器產生暫存器檔案資訊。

  • 為目標產生指令定義。

  • 產生程式碼產生器用於將指令與中間表示 (IR) 節點匹配的模式。

  • 為 Clang 產生語義屬性識別碼。

  • 為 Clang 產生抽象語法樹 (AST) 宣告節點定義。

  • 為 Clang 產生 AST 語句節點定義。

1.1.1 概念

TableGen 來源檔案包含兩個主要項目:*抽象記錄*和*具體記錄*。在此 TableGen 文件和其他文件中,抽象記錄稱為*類別*。(這些類別不同於 C++ 類別,並且不會映射到它們。)此外,具體記錄通常只稱為記錄,儘管有時術語*記錄*指的是類別和具體記錄。區別應該在上下文中很清楚。

類別和具體記錄具有唯一的*名稱*,由程式設計師選擇或由 TableGen 產生。與該名稱關聯的是一個包含值*欄位*的清單和一個可選的*父類別*(有時稱為基類或超類)清單。欄位是後端將處理的主要數據。請注意,TableGen 不會為欄位賦予任何含義;含義完全取決於後端和包含這些後端輸出的程式。

備註

術語“父類別”可以指作為另一個類別父類別的類別,也可以指具體記錄繼承自的類別。這種對該術語的非標準使用是由於 TableGen 類似地處理類別和具體記錄。

後端處理由 TableGen 解析器構建的具體記錄的某些子集,並發出輸出檔案。這些檔案通常是 C++ .inc 檔案,這些檔案包含在需要這些記錄中數據的程式中。但是,後端可以產生任何類型的輸出檔案。例如,它可以產生一個數據檔案,其中包含帶有識別碼和替換參數標記的訊息。在諸如 LLVM 程式碼產生器之類的複雜用例中,可能會有許多具體記錄,並且其中一些記錄可能具有數量驚人的欄位,從而導致輸出檔案很大。

為降低 TableGen 檔案的複雜度,我們使用類別來抽象化一組記錄欄位。例如,一些類別可以抽象化機器暫存器檔案的概念,而其他類別可以抽象化指令格式,還有一些類別可以抽象化個別指令。TableGen 允許任意的類別階層,因此兩個概念的抽象類別可以共用第三個超類別,該超類別可以從兩個原始概念中抽象出共同的「子概念」。

為了讓類別更有用,具體記錄(或其他類別)可以請求一個類別作為父類別,並傳遞*範本參數*給它。這些範本參數可以用於父類別的欄位中,以自訂的方式初始化它們。也就是說,記錄或類別 A 可以請求父類別 S 並帶有一組範本參數,而記錄或類別 B 可以請求 S 並帶有另一組參數。如果沒有範本參數,則需要更多類別,範本參數的每個組合都需要一個類別。

類別和具體記錄都可以包含未初始化的欄位。未初始化的「值」以問號 (?) 表示。類別通常具有未初始化的欄位,預計在具體記錄繼承這些類別時填入這些欄位。即使如此,具體記錄的某些欄位可能仍未初始化。

TableGen 提供*多類別*來將一組記錄定義收集在一個地方。多類別是一種巨集,可以「呼叫」它來一次定義多個具體記錄。多類別可以繼承自其他多類別,這表示多類別會繼承其父類別多類別的所有定義。

附錄 C:範例記錄 說明了 Intel X86 目標中的一個複雜記錄,以及定義它的簡單方式。

1.2 原始程式檔

TableGen 原始程式檔是純 ASCII 文字檔。這些檔案可以包含語句、註解和空白行(請參閱詞彙分析)。TableGen 檔案的標準副檔名為 .td

TableGen 檔案可能會變得非常大,因此有一種 include 機制允許一個檔案包含另一個檔案的內容(請參閱包含檔案)。這允許將大型檔案分解成較小的檔案,並且還提供了一個簡單的程式庫機制,其中多個原始程式檔可以包含同一個程式庫檔案。

TableGen 支援一個簡單的前處理器,可用於將 .td 檔案的部分內容條件化。如需詳細資訊,請參閱前處理功能

1.3 詞彙分析

此處使用的詞彙和語法標記旨在模仿Python 的標記。特別是對於詞彙定義,這些產生式在字元級別運作,元素之間沒有隱含的空格。語法定義在詞彙層級運作,因此詞彙之間有隱含的空格。

TableGen 支援 BCPL 風格註解 (// ...) 和可巢狀的 C 風格註解 (/* ... */)。TableGen 也提供簡單的 前置處理器功能

檔案中可自由使用分頁符,以便在列印檔案以供審閱時產生分頁。

以下是基本的標點符號

- + [ ] { } ( ) < > : ; . ... = ? #

1.3.1 字面常數

數值字面常數採用以下形式之一

TokInteger     ::=  DecimalInteger | HexInteger | BinInteger
DecimalInteger ::=  ["+" | "-"] ("0"..."9")+
HexInteger     ::=  "0x" ("0"..."9" | "a"..."f" | "A"..."F")+
BinInteger     ::=  "0b" ("0" | "1")+

請注意,DecimalInteger 符號包含可選的 +- 符號,這與大多數語言不同,在這些語言中,符號將被視為一元運算子。

TableGen 有兩種字串字面常數

TokString ::=  '"' (non-'"' characters and escapes) '"'
TokCode   ::=  "[{" (text not containing "}]") "}]"

TokCode 只不過是一個由 [{}] 分隔的多行字串字面常數。它可以跨越多行,並且會保留字串中的換行符。

目前的實作接受以下跳脫序列

\\ \' \" \t \n

1.3.2 識別碼

TableGen 具有類似名稱和識別碼的符號,這些符號區分大小寫。

ualpha        ::=  "a"..."z" | "A"..."Z" | "_"
TokIdentifier ::=  ("0"..."9")* ualpha (ualpha | "0"..."9")*
TokVarName    ::=  "$" ualpha (ualpha |  "0"..."9")*

請注意,與大多數語言不同,TableGen 允許 TokIdentifier 以整數開頭。如果發生歧義,則符號會被解釋為數值字面常數,而不是識別碼。

TableGen 具有以下保留關鍵字,這些關鍵字不能用作識別碼

assert     bit           bits          class         code
dag        def           dump          else          false
foreach    defm          defset        defvar        field
if         in            include       int           let
list       multiclass    string        then          true

警告

保留字 field 已被棄用,除非與 CodeEmitterGen 後端一起使用,在後端中,它用於區分一般的記錄欄位和編碼欄位。

1.3.3 驚嘆號運算子

TableGen 提供「驚嘆號運算子」,它們具有廣泛的用途

BangOperator ::=  one of
                  !add         !and         !cast        !con         !dag
                  !div         !empty       !eq          !exists      !filter
                  !find        !foldl       !foreach     !ge          !getdagarg
                  !getdagname  !getdagop    !gt          !head        !if
                  !interleave  !isa         !le          !listconcat  !listremove
                  !listsplat   !logtwo      !lt          !mul         !ne
                  !not         !or          !range       !repr        !setdagarg
                  !setdagname  !setdagop    !shl         !size        !sra
                  !srl         !strconcat   !sub         !subst       !substr
                  !tail        !tolower     !toupper     !xor

!cond 運算子的語法與其他驚嘆號運算子略有不同,因此單獨定義

CondOperator ::=  !cond

如需每個驚嘆號運算子的說明,請參閱 附錄 A:驚嘆號運算子

1.3.4 include 檔案

TableGen 具有 include 機制。include 檔案的內容會在詞法上替換 include 指令,然後解析,就好像它最初位於主檔案中一樣。

IncludeDirective ::=  "include" TokString

可以使用前置處理器指令將主檔案和 include 檔案的部分內容設為條件式。

PreprocessorDirective ::=  "#define" | "#ifdef" | "#ifndef"

1.4 類型

TableGen 語言是靜態類型的,使用簡單但完整的類型系統。類型用於檢查錯誤、執行隱式轉換以及幫助介面設計人員約束允許的輸入。每個值都必須具有關聯的類型。

TableGen 支援低階類型(例如 bit)和高階類型(例如 dag)的混合使用。這種靈活性讓您能夠方便且精簡地描述各種記錄。

Type    ::=  "bit" | "int" | "string" | "dag"
            | "bits" "<" TokInteger ">"
            | "list" "<" Type ">"
            | ClassID
ClassID ::=  TokIdentifier
bit

bit 是一個布林值,可以是 0 或 1。

int

int 類型表示一個簡單的 64 位元整數值,例如 5 或 -42。

string

string 類型表示任意長度的有序字元序列。

bits<*n*>

bits 類型是一個任意長度 *n* 的固定大小整數,它被視為單獨的位元。這些位元可以單獨存取。此類型的欄位可用於表示指令操作碼、暫存器編號或定址模式/暫存器/位移量。欄位的位元可以單獨設定或作為子欄位設定。例如,在指令地址中,定址模式、基址暫存器編號和位移量可以單獨設定。

list<*type*>

此類型表示一個清單,其元素的類型在尖括號中指定。元素類型是任意的;它甚至可以是另一個清單類型。清單元素從 0 開始索引。

dag

此類型表示節點的可嵌套有向無環圖 (DAG)。每個節點都有一個 *運算子* 和零個或多個 *參數*(或 *運算元*)。參數可以是另一個 dag 物件,允許節點和邊的任意樹狀結構。例如,DAG 用於表示程式碼產生器指令選擇演算法使用的程式碼模式。有關更多詳細資訊,請參閱有向無環圖 (DAG)

ClassID

在類型上下文中指定類別名稱表示定義值的類型必須是指定類別的子類別。這與 list 類型結合使用非常有用;例如,將清單的元素限制為共同的基類(例如,list<Register> 只能包含從 Register 類別衍生的定義)。ClassID 必須命名先前已宣告或定義的類別。

1.5 值和運算式

在 TableGen 語句中,有許多情況下需要一個值。一個常見的例子是在記錄的定義中,其中每個欄位都由名稱和可選值指定。在建立值運算式時,TableGen 允許相當多種不同的形式。這些形式允許以適合應用程序的語法編寫 TableGen 檔案。

請注意,所有值都有將其從一種類型轉換為另一種類型的規則。例如,這些規則允許您將 7 之類的值賦值給類型為 bits<4> 的實體。

Value         ::=  SimpleValue ValueSuffix*
                  | Value "#" [Value]
ValueSuffix   ::=  "{" RangeList "}"
                  | "[" SliceElements "]"
                  | "." TokIdentifier
RangeList     ::=  RangePiece ("," RangePiece)*
RangePiece    ::=  TokInteger
                  | TokInteger "..." TokInteger
                  | TokInteger "-" TokInteger
                  | TokInteger TokInteger
SliceElements ::=  (SliceElement ",")* SliceElement ","?
SliceElement  ::=  Value
                  | Value "..." Value
                  | Value "-" Value
                  | Value TokInteger

警告

RangePieceSliceElement 最後一種形式的特殊之處在於「-」包含在 TokInteger 中,因此 1-5 會被詞法分析為兩個連續的詞彙,值為 1-5,而不是「1」、「-」和「5」。不建議使用連字號作為範圍標點符號。

1.5.1 簡單值

SimpleValue 有多種形式。

SimpleValue ::=  TokInteger | TokString+ | TokCode

值可以是整數字面值、字串字面值或程式碼字面值。多個相鄰的字串字面值會像 C/C++ 中一樣進行串聯;簡單值是字串的串聯。程式碼字面值會變成字串,然後與字串沒有區別。

SimpleValue2 ::=  "true" | "false"

truefalse 字面值本質上是整數值 1 和 0 的語法糖。當布林值用於欄位初始化、位元序列、if 語句等時,它們可以提高 TableGen 檔案的可讀性。解析時,這些字面值會轉換為整數。

備註

雖然 truefalse 是 1 和 0 的字面值名稱,但我們建議您將它們僅用於布林值,作為一種程式碼風格規則。

SimpleValue3 ::=  "?"

問號表示未初始化的值。

SimpleValue4 ::=  "{" [ValueList] "}"
ValueList    ::=  ValueListNE
ValueListNE  ::=  Value ("," Value)*

此值表示一個位元序列,可用於初始化 bits<*n*> 欄位(注意大括號)。執行此操作時,這些值必須表示總共 *n* 個位元。

SimpleValue5 ::=  "[" ValueList "]" ["<" Type ">"]

此值是一個清單初始化式(注意方括號)。方括號中的值是清單的元素。可以使用可選的 Type 來指示特定的元素類型;否則,元素類型將從給定的值推斷出來。TableGen 通常可以推斷出類型,但在值為空清單 ([]) 時有時無法推斷。

SimpleValue6 ::=  "(" DagArg [DagArgList] ")"
DagArgList   ::=  DagArg ("," DagArg)*
DagArg       ::=  Value [":" TokVarName] | TokVarName

這表示一個 DAG 初始化式(注意括號)。第一個 DagArg 稱為 DAG 的「運算子」,並且必須是一個記錄。有關詳細資訊,請參閱有向無環圖 (DAG)

SimpleValue7 ::=  TokIdentifier

結果值是由識別碼命名的實體的值。可能的識別碼在此處說明,但在閱讀完本指南的其餘部分後,這些說明將更有意義。

  • class 的範本參數,例如在以下程式碼中使用 Bar

    class Foo <int Bar> {
      int Baz = Bar;
    }
    
  • classmulticlass 定義中的隱式範本參數 NAME(請參閱NAME)。

  • class 的本地欄位,例如在以下程式碼中使用 Bar

    class Foo {
      int Bar = 5;
      int Baz = Bar;
    }
    
  • 記錄定義的名稱,例如在 Foo 的定義中使用 Bar

    def Bar : SomeClass {
      int X = 5;
    }
    
    def Foo {
      SomeClass Baz = Bar;
    }
    
  • 記錄定義中的本地欄位,例如在以下情況中使用 Bar

    def Foo {
      int Bar = 5;
      int Baz = Bar;
    }
    

    可以透過相同的方式存取從記錄的父類別繼承的欄位。

  • multiclass 的模板參數,例如在以下情況中使用 Bar

    multiclass Foo <int Bar> {
      def : SomeClass<Bar>;
    }
    
  • 使用 defvardefset 語句定義的變數。

  • foreach 的迭代變數,例如在以下情況中使用 i

    foreach i = 0...5 in
      def Foo#i;
    
SimpleValue8 ::=  ClassID "<" ArgValueList ">"

此形式會建立一個新的匿名記錄定義(如同透過繼承自具有給定模板參數的給定類別的未命名 def 所建立的;請參閱 def),而值即為該記錄。可以使用後綴獲取記錄的欄位;請參閱 後綴值

以這種方式呼叫類別可以提供簡單的子常式功能。如需更多資訊,請參閱 將類別用作子常式

SimpleValue9 ::=  BangOperator ["<" Type ">"] "(" ValueListNE ")"
                 | CondOperator "(" CondClause ("," CondClause)* ")"
CondClause   ::=  Value ":" Value

Bang 運算子提供的函數無法透過其他簡單值使用。除了 !cond 之外,Bang 運算子會採用括號中包含的參數清單,並對這些參數執行某些函數,從而產生該 Bang 運算子的值。!cond 運算子會採用以冒號分隔的參數對清單。如需每個 Bang 運算子的描述,請參閱 附錄 A:Bang 運算子

1.5.2 後綴值

可以使用某些後綴指定上述 SimpleValue 值。後綴的目的是獲取主要值的子值。以下是一些主要「值」的可能後綴。

「值」{17}

最終值是整數「值」的第 17 位元(請注意大括號)。

「值」{8...15}

最終值是整數「值」的第 8-15 位元。可以透過指定 {15...8} 來反轉位元順序。

「值」[i]

最終值是清單「值」的元素「i」(請注意方括號)。換句話說,方括號充當清單上的下標運算子。僅當指定單個元素時才會出現這種情況。

「值」[i,]

最終值是一個包含清單的單個元素「i」的清單。簡而言之,就是具有單個元素的清單切片。

「值」[4...7,17,2...3,4]

最終值是一個新清單,它是清單「值」的切片。新清單包含元素 4、5、6、7、17、2、3 和 4。元素可以多次包含,並且可以按任何順序排列。僅當指定多個元素時才會出現此結果。

「值」[i,m...n,j,ls]

每個元素可以是一個表達式(變數、bang 運算符)。mn 的類型應為 intijls 的類型應為 intlist<int>

.欄位

最終值是指定記錄 _值_ 中指定 _欄位_ 的值。

1.5.3 貼上運算符

貼上運算符 (#) 是 TableGen 表達式中唯一可用的中綴運算符。它允許您串聯字串或清單,但有一些不尋常的功能。

DefDefm 陳述式中指定記錄名稱時,可以使用貼上運算符,在這種情況下,它必須構造一個字串。如果運算元是未定義的名稱 (TokIdentifier) 或全域 DefvarDefset 的名稱,則將其視為逐字字串。不會使用全域名稱的值。

貼上運算符可以用於所有其他值表達式中,在這種情況下,它可以構造一個字串或一個清單。相當奇怪的是,但與前面的情況一致,如果 _右側_ 運算元是未定義的名稱或全域名稱,則將其視為逐字字串。左側運算元會被正常處理。

值可以有一個尾隨的貼上運算符,在這種情況下,左側運算元會被串聯到一個空字串。

附錄 B:貼上運算符範例 提供了貼上運算符行為的範例。

1.6 陳述式

以下陳述式可能會出現在 TableGen 原始碼檔案的頂層。

TableGenFile ::=  (Statement | IncludeDirective
                 | PreprocessorDirective)*
Statement    ::=  Assert | Class | Def | Defm | Defset | Deftype
                 | Defvar | Dump  | Foreach | If | Let | MultiClass

以下章節描述了這些頂級陳述式中的每一個。

1.6.1 class — 定義抽象記錄類別

class 陳述式定義了一個抽象記錄類別,其他類別和記錄可以從中繼承。

Class           ::=  "class" ClassID [TemplateArgList] RecordBody
TemplateArgList ::=  "<" TemplateArgDecl ("," TemplateArgDecl)* ">"
TemplateArgDecl ::=  Type TokIdentifier ["=" Value]

類別可以通過“範本參數”清單進行參數化,其值可以在類別的記錄主體中使用。每次類別被另一個類別或記錄繼承時,都會指定這些範本參數。

如果沒有使用 = 為範本參數分配預設值,則該參數未初始化(具有“值”?),並且必須在繼承類別時在範本參數清單中指定(必需參數)。如果為參數分配了預設值,則無需在參數清單中指定它(可選參數)。在聲明中,所有必需的範本參數必須位於任何可選參數之前。範本參數預設值從左到右求值。

RecordBody 的定義如下。它可以包含當前類別繼承的父類別清單,以及欄位定義和其他語句。當類別 C 繼承自另一個類別 D 時,D 的欄位會有效地合併到 C 的欄位中。

一個給定的類別只能定義一次。如果滿足以下「任何」條件,則 class 語句被視為定義該類別(RecordBody 元素的描述如下)。

您可以透過指定空的 TemplateArgList 和空的 RecordBody 來宣告一個空的類別。這可以作為一種受限形式的前置宣告。請注意,從前置宣告的類別衍生的記錄不會繼承其欄位,因為這些記錄是在解析其宣告時建立的,因此在最終定義該類別之前。

每個類別都有一個名為 NAME(大寫)的隱式模板參數,它綁定到繼承該類別的 DefDefm 的名稱。如果該類別由匿名記錄繼承,則名稱未指定,但在全局唯一。

範例請參見 類別和記錄範例

1.6.1.1 記錄主體

記錄主體出現在類別和記錄定義中。記錄主體可以包含父類別清單,該清單指定當前類別或記錄繼承欄位的類別。這些類別稱為類別或記錄的父類別。記錄主體還包括定義的主體,其中包含類別或記錄的欄位規範。

RecordBody            ::=  ParentClassList Body
ParentClassList       ::=  [":" ParentClassListNE]
ParentClassListNE     ::=  ClassRef ("," ClassRef)*
ClassRef              ::=  (ClassID | MultiClassID) ["<" [ArgValueList] ">"]
ArgValueList          ::=  PostionalArgValueList [","] NamedArgValueList
PostionalArgValueList ::=  [Value {"," Value}*]
NamedArgValueList     ::=  [NameValue "=" Value {"," NameValue "=" Value}*]

包含 MultiClassIDParentClassList 僅在 defm 語句的類別清單中有效。在這種情況下,ID 必須是多類別的名稱。

參數值可以透過兩種形式指定

  • 位置引數 (value)。該值會被賦予到對應位置的引數。對於 Foo<a0, a1>a0 會被賦予到第一個引數,而 a1 會被賦予到第二個引數。

  • 命名引數 (name=value)。該值會被賦予到指定名稱的引數。對於 Foo<a=a0, b=a1>a0 會被賦予到名稱為 a 的引數,而 a1 會被賦予到名稱為 b 的引數。

必要引數也可以指定為命名引數。

請注意,不論使用哪種方式(命名或位置)指定,每個引數都只能指定一次,並且位置引數應該放在命名引數之前。

Body     ::=  ";" | "{" BodyItem* "}"
BodyItem ::=  (Type | "code") TokIdentifier ["=" Value] ";"
             | "let" TokIdentifier ["{" RangeList "}"] "=" Value ";"
             | "defvar" TokIdentifier "=" Value ";"
             | Assert

主體中的欄位定義指定要包含在類別或記錄中的欄位。如果未指定初始值,則欄位的值將為未初始化狀態。必須指定類型;TableGen 不會從值推斷類型。關鍵字 code 可用於強調欄位具有字串值,該值為程式碼。

let 形式用於將欄位重設為新值。這可以對直接在主體中定義的欄位或從父類別繼承的欄位執行。可以指定 RangeList 來重設 bit<n> 欄位中的某些位元。

defvar 形式定義了一個變數,其值可以在主體內的其他值表達式中使用。變數不是欄位:它不會成為正在定義的類別或記錄的欄位。變數用於在處理主體時保存臨時值。有關更多詳細信息,請參閱 記錄主體中的 Defvar

當類別 C2 繼承自類別 C1 時,它會取得 C1 的所有欄位定義。當這些定義合併到類別 C2 中時,傳遞給 C1 的任何模板引數都會被替換到定義中。換句話說,由 C1 定義的抽象記錄欄位會在合併到 C2 之前使用模板引數進行展開。

1.6.2 def — 定義具體記錄

def 語句定義了一個新的具體記錄。

Def       ::=  "def" [NameValue] RecordBody
NameValue ::=  Value (parsed in a special mode)

名稱值是可選的。如果指定,它會在特殊模式下解析,其中未定義(無法識別)的識別符號會被解釋為文字字串。特別是,全局識別符號被視為無法識別。這些包括由 defvardefset 定義的全局變數。記錄名稱可以是空字串。

如果未提供名稱值,則記錄為*匿名*。匿名記錄的最終名稱未指定,但全局唯一。

如果在 multiclass 語句中出現 def,則會進行特殊處理。詳情請參閱下面的 multiclass 章節。

記錄可以通過在其記錄主體的開頭指定 ParentClassList 子句來繼承一個或多個類別。父類別中的所有欄位都會被添加到記錄中。如果兩個或多個父類別提供相同的欄位,則記錄最終會使用最後一個父類別的欄位值。

作為一種特殊情況,記錄的名稱可以作為模板參數傳遞給該記錄的父類別。例如

class A <dag d> {
  dag the_dag = d;
}

def rec1 : A<(ops rec1)>;

DAG (ops rec1) 作為模板參數傳遞給類別 A。請注意,DAG 包含正在定義的記錄 rec1

建立新記錄的步驟有些複雜。請參閱 如何建立記錄

範例請參見 類別和記錄範例

1.6.3 範例:類別和記錄

這是一個簡單的 TableGen 檔案,其中包含一個類別和兩個記錄定義。

class C {
  bit V = true;
}

def X : C;
def Y : C {
  let V = false;
  string Greeting = "Hello!";
}

首先,定義抽象類別 C。它有一個名為 V 的欄位,該欄位是一個初始化為 true 的位元。

接下來,定義兩個從類別 C 衍生的記錄;也就是說,以 C 作為其父類別。因此,它們都繼承了 V 欄位。記錄 Y 還定義了另一個字串欄位 Greeting,它被初始化為 "Hello!"。此外,Y 覆蓋了繼承的 V 欄位,將其設定為 false。

類別對於將多個記錄的共同特徵隔離在一個地方非常有用。類別可以將通用欄位初始化為預設值,但繼承自該類別的記錄可以覆蓋預設值。

TableGen 支援定義參數化類別以及非參數化類別。參數化類別指定變數聲明的清單(這些變數可以選擇性地具有預設值),這些變數在類別被指定為另一個類別或記錄的父類別時被綁定。

class FPFormat <bits<3> val> {
  bits<3> Value = val;
}

def NotFP      : FPFormat<0>;
def ZeroArgFP  : 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>;

FPFormat 類別的目的是充當一種列舉類型。它提供了一個欄位 Value,它保存一個 3 位元的數字。它的模板參數 val 用於設定 Value 欄位。八個記錄中的每一個都定義為以 FPFormat 作為其父類別。列舉值在尖括號中作為模板參數傳遞。每個記錄都將繼承具有適當列舉值的 Value 欄位。

這是一個更複雜的帶有模板參數的類別範例。首先,我們定義一個類似於上面 FPFormat 類別的類別。它接受一個模板參數,並使用它來初始化一個名為 Value 的欄位。然後我們定義四個記錄,它們繼承了具有四個不同整數值的 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>;

這可能有點刻意,但假設我們想要獨立檢查 Value 欄位的兩個位元。我們可以定義一個類別,它接受一個 ModRefVal 記錄作為模板參數,並將其值拆分為兩個欄位,每個欄位一個位元。然後我們可以定義繼承自 ModRefBits 的記錄,並從中取得兩個欄位,一個對應作為模板參數傳遞的 ModRefVal 記錄中的每個位元。

class ModRefBits <ModRefVal mrv> {
  // Break the value up into its bits, which can provide a nice
  // interface to the ModRefVal values.
  bit isMod = mrv.Value{0};
  bit isRef = mrv.Value{1};
}

// Example uses.
def foo   : ModRefBits<Mod>;
def bar   : ModRefBits<Ref>;
def snork : ModRefBits<ModRef>;

這說明了一個類別如何被定義來重新組織另一個類別中的欄位,從而隱藏另一個類別的內部表示。

對範例執行 llvm-tblgen 會印出以下定義

def bar {      // Value
  bit isMod = 0;
  bit isRef = 1;
}
def foo {      // Value
  bit isMod = 1;
  bit isRef = 0;
}
def snork {      // Value
  bit isMod = 1;
  bit isRef = 1;
}

1.6.4 let — 覆寫類別或記錄中的欄位

let 陳述式收集一組欄位值(有時稱為*綁定*),並將其應用於 let 範圍內陳述式所定義的所有類別和記錄。

Let     ::=   "let" LetList "in" "{" Statement* "}"
            | "let" LetList "in" Statement
LetList ::=  LetItem ("," LetItem)*
LetItem ::=  TokIdentifier ["<" RangeList ">"] "=" Value

let 陳述式建立一個範圍,它是一個用大括號括起來的陳述式序列或一個沒有大括號的單一陳述式。LetList 中的綁定適用於該範圍內的陳述式。

LetList 中的欄位名稱必須是陳述式中定義的類別和記錄所繼承的類別中的欄位名稱。在記錄從其父類別繼承所有欄位*之後*,欄位值會應用於類別和記錄。因此,let 的作用是覆寫繼承的欄位值。let 無法覆寫模板參數的值。

當需要在多個記錄中覆寫少數欄位時,頂級 let 陳述式通常很有用。以下是兩個例子。請注意,let 陳述式可以嵌套。

let isTerminator = true, isReturn = true, isBarrier = true, hasCtrlDep = true in
  def RET : I<0xC3, RawFrm, (outs), (ins), "ret", [(X86retflag 0)]>;

let isCall = true 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 不會覆寫類別或記錄本身中定義的欄位。

1.6.5 multiclass — 定義多個記錄

雖然具有模板參數的類別是提取多個記錄之間共通性的好方法,但多類別允許一種方便的方法來一次定義多個記錄。例如,考慮一個三地址指令架構,其指令有兩種格式:reg = reg op regreg = reg op imm(例如 SPARC)。我們希望在一個地方指定這兩種常見格式存在,然後在另一個地方指定所有操作是什麼。multiclassdefm 陳述式可以實現這個目標。您可以將多類別視為展開為多個記錄的宏或模板。

MultiClass          ::=  "multiclass" TokIdentifier [TemplateArgList]
                         ParentClassList
                         "{" MultiClassStatement+ "}"
MultiClassID        ::=  TokIdentifier
MultiClassStatement ::=  Assert | Def | Defm | Defvar | Foreach | If | Let

如同一般的類別,多重類別 (multiclass) 也有名稱,並且可以接受模板參數。多重類別可以繼承自其他多重類別,這將導致其他多重類別被展開,並將其記錄定義貢獻給繼承的多重類別。多重類別的主體包含一系列使用 DefDefm 定義記錄的語句。此外,可以使用 DefvarForeachLet 語句來提取更多共同元素。也可以使用 IfAssert 語句。

與一般的類別一樣,多重類別也具有隱含的模板參數 NAME(請參閱 NAME)。當在多重類別中定義了一個具名(非匿名)記錄,並且該記錄的名稱不包含對模板參數 NAME 的使用時,將自動在名稱前面加上該參數的使用。也就是說,以下在多重類別中是等效的

def Foo ...
def NAME # Foo ...

當多重類別被 defm 語句「實例化」或「調用」時,就會建立多重類別中定義的記錄。多重類別中的每個 def 語句都會產生一個記錄。與頂層 def 語句一樣,這些定義可以繼承自多個父類別。

請參閱 範例:多重類別和 defm 以獲取範例。

1.6.6 defm — 調用多重類別以定義多個記錄

定義多重類別後,您可以使用 defm 語句來「調用」它們,並處理這些多重類別中的多個記錄定義。這些記錄定義由多重類別中的 def 語句指定,並間接由 defm 語句指定。

Defm ::=  "defm" [NameValue] ParentClassList ";"

可選的 NameValue 的組成方式與 def 的名稱相同。 ParentClassList 是由冒號後跟至少一個多重類別和任意數量的常規類別組成的清單。多重類別必須在常規類別之前。請注意,defm 沒有主體。

這個敘述會實例化所有在指定多類別中定義的記錄,無論是直接透過 def 敘述,還是間接透過 defm 敘述。這些記錄也會接收在父類別列表中包含的任何常規類別中定義的欄位。這對於將一組通用的欄位添加到由 defm 建立的所有記錄中非常有用。

名稱的解析方式與 def 使用的特殊模式相同。如果未包含名稱,則會提供一個未指定但全局唯一的名稱。也就是說,以下範例最終會使用不同的名稱

defm    : SomeMultiClass<...>;   // A globally unique name.
defm "" : SomeMultiClass<...>;   // An empty name.

defm 敘述可以用在多類別主體中。當發生這種情況時,第二個變體等同於

defm NAME : SomeMultiClass<...>;

更一般地說,當 defm 出現在多類別中,並且其名稱不包含對隱式範本參數 NAME 的使用時,則會自動在前面加上 NAME。也就是說,以下在多類別中是等效的

defm Foo        : SomeMultiClass<...>;
defm NAME # Foo : SomeMultiClass<...>;

請參閱 範例:多重類別和 defm 以獲取範例。

1.6.7 範例:多類別和 defm

以下是一個使用 multiclassdefm 的簡單範例。考慮一個三地址指令架構,其指令有兩種格式:reg = reg op regreg = reg op imm(立即數)。SPARC 就是這種架構的一個例子。

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)>;
}

// Define records for each instruction in the RR and RI formats.
defm ADD : ri_inst<0b111, "add">;
defm SUB : ri_inst<0b101, "sub">;
defm MUL : ri_inst<0b100, "mul">;

每次使用 ri_inst 多類別都會定義兩個記錄,一個帶有 _rr 後綴,另一個帶有 _ri 後綴。回想一下,使用多類別的 defm 的名稱會被添加到該多類別中定義的記錄的名稱之前。所以得到的定義名稱如下

ADD_rr, ADD_ri
SUB_rr, SUB_ri
MUL_rr, MUL_ri

如果沒有 multiclass 功能,則必須按如下方式定義指令。

def ops;
def GPR;
def Imm;
class inst <int opc, string asmstr, dag operandlist>;

class rrinst <int opc, string asmstr>
  : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"),
           (ops GPR:$dst, GPR:$src1, GPR:$src2)>;

class riinst <int opc, string asmstr>
  : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"),
           (ops GPR:$dst, GPR:$src1, Imm:$src2)>;

// Define records for each instruction in the RR and RI formats.
def ADD_rr : rrinst<0b111, "add">;
def ADD_ri : riinst<0b111, "add">;
def SUB_rr : rrinst<0b101, "sub">;
def SUB_ri : riinst<0b101, "sub">;
def MUL_rr : rrinst<0b100, "mul">;
def MUL_ri : riinst<0b100, "mul">;

可以在多類別中使用 defm 來「調用」其他多類別,並建立在這些多類別中定義的記錄,以及在當前多類別中定義的記錄。在以下範例中,basic_sbasic_p 多類別包含引用 basic_r 多類別的 defm 敘述。basic_r 多類別只包含 def 敘述。

class Instruction <bits<4> opc, string Name> {
  bits<4> opcode = opc;
  string name = Name;
}

multiclass basic_r <bits<4> opc> {
  def rr : Instruction<opc, "rr">;
  def rm : Instruction<opc, "rm">;
}

multiclass basic_s <bits<4> opc> {
  defm SS : basic_r<opc>;
  defm SD : basic_r<opc>;
  def X : Instruction<opc, "x">;
}

multiclass basic_p <bits<4> opc> {
  defm PS : basic_r<opc>;
  defm PD : basic_r<opc>;
  def Y : Instruction<opc, "y">;
}

defm ADD : basic_s<0xf>, basic_p<0xf>;

最後的 defm 建立以下記錄,五個來自 basic_s 多類別,五個來自 basic_p 多類別

ADDSSrr, ADDSSrm
ADDSDrr, ADDSDrm
ADDX
ADDPSrr, ADDPSrm
ADDPDrr, ADDPDrm
ADDY

defm 敘述,無論是在頂層還是在多類別中,除了可以繼承多類別之外,還可以繼承常規類別。規則是常規類別必須列在多類別之後,並且必須至少有一個多類別。

class XD {
  bits<4> Prefix = 11;
}
class XS {
  bits<4> Prefix = 12;
}
class I <bits<4> op> {
  bits<4> opcode = op;
}

multiclass R {
  def rr : I<4>;
  def rm : I<2>;
}

multiclass Y {
  defm SS : R, XD;    // First multiclass R, then regular class XD.
  defm SD : R, XS;
}

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 };
}

也可以在多類別中使用 let 語句,提供另一種從記錄中提取共同性的方法,尤其是在使用多級多類別實例化的情況下。

multiclass basic_r <bits<4> opc> {
  let Predicates = [HasSSE2] in {
    def rr : Instruction<opc, "rr">;
    def rm : Instruction<opc, "rm">;
  }
  let Predicates = [HasSSE3] in
    def rx : Instruction<opc, "rx">;
}

multiclass basic_ss <bits<4> opc> {
  let IsDouble = false in
    defm SS : basic_r<opc>;

  let IsDouble = true in
    defm SD : basic_r<opc>;
}

defm ADD : basic_ss<0xf>;

1.6.8 defset — 建立定義集

defset 語句用於將一組記錄收集到一個全局記錄列表中。

Defset ::=  "defset" Type TokIdentifier "=" "{" Statement* "}"

通過 defdefm 在大括號內定義的所有記錄都按常規定義,並且它們也被收集在給定名稱(TokIdentifier)的全局列表中。

指定的類型必須是 list<class>,其中 class 是某個記錄類別。 defset 語句為其語句建立了一個作用域。在 defset 的作用域中定義不是 class 類型的記錄是一個錯誤。

defset 語句可以嵌套。內部 defset 將記錄添加到其自身的集合中,並且所有這些記錄也被添加到外部集合中。

使用 ClassID<...> 語法在初始化表達式內創建的匿名記錄不會被收集到集合中。

1.6.9 deftype — 定義類型

deftype 語句用於定義類型。該類型可以在定義之後的所有語句中使用。

Deftype ::=  "deftype" TokIdentifier "=" Type ";"

= 左邊的標識符被定義為一個類型名稱,其實際類型由 = 右邊的類型表達式給出。

目前,只有原始類型和類型別名可以作為來源類型,並且 deftype 語句只能出現在頂層。

1.6.10 defvar — 定義變數

defvar 語句用於定義全局變數。其值可以在定義之後的所有語句中使用。

Defvar ::=  "defvar" TokIdentifier "=" Value ";"

= 左邊的標識符被定義為一個全局變數,其值由 = 右邊的值表達式給出。變數的類型會自動推斷。

一旦定義了變數,就不能將其設置為另一個值。

在頂層 foreach 中定義的變數在每次循環迭代結束時都會超出作用域,因此它們在一次迭代中的值在下次迭代中不可用。以下 defvar 將無法正常工作

defvar i = !add(i, 1);

也可以使用記錄主體中的 defvar 定義變數。有關更多詳細信息,請參閱記錄主體中的 Defvar

1.6.11 foreach — 迭代執行一系列語句

foreach 語句會迭代執行一系列語句,並根據值的序列變更變數。

Foreach         ::=  "foreach" ForeachIterator "in" "{" Statement* "}"
                    | "foreach" ForeachIterator "in" Statement
ForeachIterator ::=  TokIdentifier "=" ("{" RangeList "}" | RangePiece | Value)

foreach 的主體是一系列用大括號括起來的語句,或是一個沒有大括號的單一語句。對於範圍列表、範圍片段或單一值中的每個值,語句會被重新評估一次。在每次迭代中,TokIdentifier 變數會被設定為該值,並可在語句中使用。

語句列表會建立一個內部作用域。區域於 foreach 的變數會在每次迴圈迭代結束時超出作用域,因此它們的值不會從一次迭代延續到下一次迭代。Foreach 迴圈可以巢狀。

foreach i = [0, 1, 2, 3] in {
  def R#i : Register<...>;
  def F#i : Register<...>;
}

此迴圈定義了名為 R0R1R2R3 的記錄,以及 F0F1F2F3

1.6.12 dump — 將訊息列印到標準錯誤輸出

dump 語句會將輸入字串列印到標準錯誤輸出。它旨在用於除錯目的。

  • 在頂層,訊息會立即列印。

  • 在記錄/類別/多重類別中,dump 會在包含記錄的每個實例化點進行評估。

Dump ::=  "dump"  string ";"

例如,它可以與 !repr 結合使用,以調查傳遞給多重類別的值

multiclass MC<dag s> {
  dump "s = " # !repr(s);
}

1.6.13 if — 根據測試選擇語句

if 語句允許根據表達式的值選擇兩個語句群組之一。

If     ::=  "if" Value "then" IfBody
           | "if" Value "then" IfBody "else" IfBody
IfBody ::=  "{" Statement* "}" | Statement

會評估值表達式。如果它評估為 true(與 bang 運算子使用的意義相同),則會處理 then 保留字後面的語句。否則,如果有 else 保留字,則會處理 else 後面的語句。如果值為 false 且沒有 else 分支,則不會處理任何語句。

因為 then 語句周圍的大括號是可選的,所以這個語法規則與「懸掛 else」子句一樣存在歧義,其解決方法與平常相同:在 if v1 then if v2 then {...} else {...} 的情況下,else 與內部的 if 相關聯,而不是與外部的 if 相關聯。

if 的 then 和 else 分支的 IfBody 建立了一個內部作用域。在主體中定義的任何 defvar 變數在主體完成後都會超出作用域(有關詳細資訊,請參閱記錄主體中的 Defvar)。

if 語句也可以在記錄 Body 中使用。

1.6.14 assert — 檢查條件是否為真

assert 語句檢查布林條件以確保其為真,如果為假則印出錯誤訊息。

Assert ::=  "assert" condition "," message ";"

如果布林條件為真,則該語句不執行任何操作。如果條件為假,則會印出非致命錯誤訊息。訊息可以是任意字串運算式,它會作為註釋包含在錯誤訊息中。assert 語句的確切行為取決於其放置位置。

  • 在頂層,斷言會立即被檢查。

  • 在記錄定義中,語句會被儲存,並且所有斷言在記錄完全建構後都會被檢查。

  • 在類別定義中,斷言會被儲存並由繼承自該類別的所有子類別和記錄繼承。然後在記錄完全建構時檢查斷言。

  • 在多類別定義中,斷言會與多類別的其他組件一起儲存,然後在每次使用 defm 初始化多類別時進行檢查。

在 TableGen 檔案中使用斷言可以簡化 TableGen 後端中的記錄檢查。以下是在兩個類別定義中使用 assert 的範例。

class PersonName<string name> {
  assert !le(!size(name), 32), "person name is too long: " # name;
  string Name = name;
}

class Person<string name, int age> : PersonName<name> {
  assert !and(!ge(age, 1), !le(age, 120)), "person age is invalid: " # age;
  int Age = age;
}

def Rec20 : Person<"Donald Knuth", 60> {
  ...
}

1.7 其他詳細資訊

1.7.1 有向無環圖 (DAG)

可以使用 dag 資料類型在 TableGen 中直接表示有向無環圖。DAG 節點由一個運算子和零個或多個參數(或運算元)組成。每個參數可以是任何所需的類型。通過使用另一個 DAG 節點作為參數,可以建構任意 DAG 節點圖。

dag 實例的語法為

( 運算子 參數 1, 參數 2,)

運算子必須存在且必須是記錄。 可以有零個或多個參數,以逗號分隔。 運算子和參數可以有三種格式。

格式

含義

參數值

:名稱

參數值和關聯名稱

名稱

具有未設定(未初始化)值的參數名稱

可以是任何 TableGen 值。 名稱(如果存在)必須是 TokVarName,它以美元符號 ($) 開頭。 名稱的目的是使用特定含義標記 DAG 中的運算子或參數,或將一個 DAG 中的參數與另一個 DAG 中的同名參數關聯起來。

以下 bang 運算子可用於處理 DAG:!con!dag!empty!foreach!getdagarg!getdagname!getdagop!setdagarg!setdagname!setdagop!size

1.7.2 記錄主體中的 Defvar

除了定義全局變量外,defvar 語句還可以在類或記錄定義的 Body 內部使用,以定義局部變量。 classmulticlass 的模板參數可以在值表達式中使用。 變量的作用域從 defvar 語句到主體的末尾。 它不能在其作用域內設置為不同的值。 defvar 語句也可以在 foreach 的語句列表中使用,這將建立一個作用域。

內部作用域中名為 V 的變量會遮蔽(隱藏)外部作用域中的任何變量 V。 特別是在以下幾種情況下

  • 記錄主體中的 V 會遮蔽全局 V

  • 記錄主體中的 V 會遮蔽模板參數 V

  • 模板參數中的 V 會遮蔽全局 V

  • foreach 語句列表中的 V 會遮蔽周圍記錄或全局作用域中的任何 V

foreach 中定義的變量在每次循環迭代結束時超出作用域,因此它們在一次迭代中的值在下次迭代中不可用。 以下 defvar 將不起作用

defvar i = !add(i, 1)

1.7.3 如何建立記錄

以下是 TableGen 在建立記錄時採取的步驟。 類只是抽象記錄,因此經歷相同的步驟。

  1. 建構記錄名稱 (NameValue) 並建立一個空的記錄。

  2. 從左到右解析 ParentClassList 中的父類別,由上而下訪問每個父類別的祖先類別。

  1. 將父類別中的欄位添加到記錄中。

  2. 將模板參數替換到這些欄位中。

  3. 將父類別添加到記錄的繼承類別列表中。

  1. 將任何頂級 let 綁定應用於記錄。 回想一下,頂級綁定僅適用於繼承的欄位。

  2. 解析記錄的正文。

  • 將任何欄位添加到記錄中。

  • 根據本地 let 語句修改欄位的值。

  • 定義任何 defvar 變數。

  1. 遍歷所有欄位以解析任何欄位間的引用。

  2. 將記錄添加到最終記錄列表中。

因為欄位之間的引用是在應用 let 綁定(步驟 3)之後解析的(步驟 5),所以 let 語句具有不尋常的能力。 例如

class C <int x> {
  int Y = x;
  int Yplus1 = !add(Y, 1);
  int xplus1 = !add(x, 1);
}

let Y = 10 in {
  def rec1 : C<5> {
  }
}

def rec2 : C<5> {
  let Y = 10;
}

在這兩種情況下,一種是使用頂級 let 綁定 Y,另一種是使用本地 let 做同樣的事情,結果是

def rec1 {      // C
  int Y = 10;
  int Yplus1 = 11;
  int xplus1 = 6;
}
def rec2 {      // C
  int Y = 10;
  int Yplus1 = 11;
  int xplus1 = 6;
}

Yplus1 是 11,因為在解析 !add(Y, 1) 之前執行了 let Y。 請明智地使用此功能。

1.8 將類別用作子常式

簡單值 中所述,可以在表達式中調用類別並傳遞模板參數。 這會導致 TableGen 建立一個繼承自該類別的新匿名記錄。 與往常一樣,記錄會接收類別中定義的所有欄位。

此功能可以用作簡單的子常式工具。 該類別可以使用模板參數來定義各種變數和欄位,這些變數和欄位最終會出現在匿名記錄中。 然後,可以在調用類別的表達式中檢索這些欄位,如下所示。 假設欄位 ret 包含子常式的最終值。

int Result = ... CalcValue<arg>.ret ...;

使用模板參數 arg 調用 CalcValue 類別。 它會計算 ret 欄位的值,然後在 Result 欄位的初始化中在「調用點」檢索該值。 在此範例中建立的匿名記錄除了攜帶結果值之外沒有其他用途。

以下是一個實際範例。類別 isValidSize 會判斷指定的位元組數是否代表有效的資料大小。位元 ret 會相應地被設定。欄位 ValidSize 的初始值是透過呼叫 isValidSize 並傳入資料大小,然後從結果匿名記錄中取得 ret 欄位來取得。

class isValidSize<int size> {
  bit ret = !cond(!eq(size,  1): 1,
                  !eq(size,  2): 1,
                  !eq(size,  4): 1,
                  !eq(size,  8): 1,
                  !eq(size, 16): 1,
                  true: 0);
}

def Data1 {
  int Size = ...;
  bit ValidSize = isValidSize<Size>.ret;
}

1.9 前置處理工具

TableGen 中內嵌的前置處理器僅用於簡單的條件式編譯。它支援以下指令,這些指令的說明方式較為非正式。

LineBegin              ::=  beginning of line
LineEnd                ::=  newline | return | EOF
WhiteSpace             ::=  space | tab
CComment               ::=  "/*" ... "*/"
BCPLComment            ::=  "//" ... LineEnd
WhiteSpaceOrCComment   ::=  WhiteSpace | CComment
WhiteSpaceOrAnyComment ::=  WhiteSpace | CComment | BCPLComment
MacroName              ::=  ualpha (ualpha | "0"..."9")*
PreDefine              ::=  LineBegin (WhiteSpaceOrCComment)*
                            "#define" (WhiteSpace)+ MacroName
                            (WhiteSpaceOrAnyComment)* LineEnd
PreIfdef               ::=  LineBegin (WhiteSpaceOrCComment)*
                            ("#ifdef" | "#ifndef") (WhiteSpace)+ MacroName
                            (WhiteSpaceOrAnyComment)* LineEnd
PreElse                ::=  LineBegin (WhiteSpaceOrCComment)*
                            "#else" (WhiteSpaceOrAnyComment)* LineEnd
PreEndif               ::=  LineBegin (WhiteSpaceOrCComment)*
                            "#endif" (WhiteSpaceOrAnyComment)* LineEnd

可以在 TableGen 檔案中的任何位置定義 巨集名稱。名稱沒有值;只能測試它是否已定義。

巨集測試區域以 #ifdef#ifndef 指令開始。如果巨集名稱已定義 (#ifdef) 或未定義 (#ifndef),則會處理指令與對應的 #else#endif 之間的原始碼。如果測試失敗但有 #else 子句,則會處理 #else#endif 之間的原始碼。如果測試失敗且沒有 #else 子句,則不會處理測試區域中的任何原始碼。

測試區域可以巢狀,但必須正確巢狀。在檔案中開始的區域必須在該檔案中結束;也就是說,其 #endif 必須在同一個檔案中。

可以使用 *-tblgen 命令列上的 -D 選項來外部定義 巨集名稱

llvm-tblgen self-reference.td -Dmacro1 -Dmacro3

1.10 附錄 A:驚嘆號運算子

驚嘆號運算子在值運算式中充當函數。驚嘆號運算子會接受一個或多個參數,對其進行運算,並產生結果。如果運算子產生布林結果,則結果值為 1 表示 true,0 表示 false。當運算子測試布林參數時,它會將 0 解釋為 false,將非 0 解釋為 true。

警告

不建議使用 !getop!setop 驚嘆號運算子,而建議使用 !getdagop!setdagop

!add(a, b, ...)

此運算子會將 ab 等相加,並產生總和。

!and(a, b, ...)

此運算子對 ab 等進行位元 AND 運算,並產生結果。如果所有參數都是 0 或 1,則可以執行邏輯 AND。

!cast<類型>(a)

此運算子對 a 執行轉型並產生結果。如果 a 不是字串,則會執行直接轉型,例如在 intbit 之間,或是在記錄類型之間。這允許將記錄轉型為類別。如果將記錄轉型為 string,則會產生記錄的名稱。

如果 a 是字串,則將其視為記錄名稱,並在所有已定義記錄的清單中查找。預期產生的記錄的類型為指定的 類型

例如,如果 !cast<類型>(名稱) 出現在多類別定義中,或在多類別定義內實例化的類別中,並且 名稱 沒有參考多類別的任何模板參數,則必須在原始程式碼檔案中較早的位置實例化具有該名稱的記錄。如果 名稱 確實參考了模板參數,則查找會延遲到實例化多類別的 defm 語句(或之後,如果 defm 出現在另一個多類別中,並且 名稱 所參考的內部多類別的模板參數被自身包含對外部多類別的模板參數的引用的值替換)。

如果 a 的類型與 類型 不匹配,TableGen 會引發錯誤。

!con(a, b, ...)

此運算子會串聯 DAG 節點 ab 等。它們的操作必須相同。

!con((op a1:$name1, a2:$name2), (op b1:$name3))

會產生 DAG 節點 (op a1:$name1, a2:$name2, b1:$name3)

!cond(條件 1 : 值 1, 條件 2 : 值 2, ..., 條件 n : 值 n)

此運算子會測試 條件 1,如果結果為真,則返回 值 1。如果為假,則運算子會測試 條件 2,如果結果為真,則返回 值 2。依此類推。如果沒有條件為真,則會報告錯誤。

此範例產生整數的符號字

!cond(!lt(x, 0) : "negative", !eq(x, 0) : "zero", true : "positive")
!dag(運算, 參數, 名稱)

此運算子會建立具有給定運算和參數的 DAG 節點。參數名稱 參數必須是長度相等的清單,或是未初始化的 (?)。名稱 參數的類型必須是 list<string>

由於型別系統的限制,*參數* 必須是具有共同型別的項目清單。在實務上,這表示它們應該具有相同的型別,或是具有共同父類別的記錄。混合使用 dag 和非 dag 項目是不可能的。但是,可以使用 ?

範例:!dag(op, [a1, a2, ?], ["name1", "name2", "name3"]) 會產生 (op a1-value:$name1, a2-value:$name2, ?:$name3)

!div(*a*, *b*)

這個運算子會執行 *a* 除以 *b* 的帶符號除法,並產生商數。除以 0 會產生錯誤。INT64_MIN 除以 -1 會產生錯誤。

!empty(*a*)

如果字串、清單或 DAG *a* 為空,則此運算子會產生 1;否則產生 0。如果 dag 沒有參數,則為空;運算子不算在內。

!eq( *a* *b*)

如果 *a* 等於 *b*,則此運算子會產生 1;否則產生 0。參數必須是 bitbitsintstring 或記錄值。使用 !cast<string> 來比較其他類型的物件。

!exists<*type*>(*name*)

如果存在名稱為 *name* 的給定 *type* 記錄,則此運算子會產生 1;否則產生 0。*name* 應該是 *string* 類型。

!filter(*var*, *list*, *predicate*)

這個運算子會透過過濾 *list* 中的元素來建立新的 list。為了執行過濾,TableGen 會將變數 *var* 綁定到每個元素,然後評估 *predicate* 運算式,該運算式可能會參考 *var*。述詞必須產生布林值(bitbitsint)。值的解釋方式與 !if 相同:如果值為 0,則元素不會包含在新清單中。如果值是任何其他值,則會包含該元素。

!find(*string1*, *string2*[, *start*])

這個運算子會在 *string1* 中搜尋 *string2* 並產生其位置。搜尋的起始位置可以由 *start* 指定,其範圍可以在 0 到 *string1* 的長度之間;預設值為 0。如果找不到字串,則結果為 -1。

!foldl(init, list, acc, var, expr)

此運算符會對 *list* 中的項目執行左摺疊。變數 *acc* 作為累加器,並初始化為 *init*。變數 *var* 綁定到 *list* 中的每個元素。運算式會針對每個元素進行評估,並且可能會使用 *acc* 和 *var* 來計算累加值,而 !foldl 會將其存回 *acc*。*acc* 的類型與 *init* 相同;*var* 的類型與 *list* 的元素相同;*expr* 必須與 *init* 具有相同的類型。

以下範例計算 RecList 中的記錄列表中 Number 欄位的總和

int x = !foldl(0, RecList, total, rec, !add(total, rec.Number));

如果您的目標是篩選列表並產生僅包含某些元素的新列表,請參閱 !filter

!foreach(var, sequence, expr)

此運算符會建立新的 list/dag,其中每個元素都是 *sequence* list/dag 中對應元素的函數。為了執行函數,TableGen 會將變數 *var* 綁定到元素,然後評估運算式。運算式可能會參考變數 *var* 並計算結果值。

如果您只想建立一個具有一定長度的列表,其中包含重複多次的相同值,請參閱 !listsplat

!ge(a, b)

如果 *a* 大於或等於 *b*,則此運算符會產生 1;否則為 0。參數必須是 bitbitsintstring 值。

!getdagarg<type>(dag,key)

此運算符會透過指定的 *key* 從指定的 *dag* 節點擷取參數,該參數可以是整數索引或字串名稱。如果該參數無法轉換為指定的 *type*,則會傳回 ?

!getdagname(dag,index)

此運算符會透過指定的 *index* 從指定的 *dag* 節點擷取參數名稱。如果該參數沒有關聯的名稱,則會傳回 ?

!getdagop(dag) –或– !getdagop<type>(dag)

此運算子會產生給定 dag 節點的運算子。範例:!getdagop((foo 1, 2)) 的結果為 foo。回想一下,DAG 運算子始終是記錄。

!getdagop 的結果可以直接用於任何記錄類別都可接受的環境中(通常將其放入另一個 dag 值)。但在其他環境中,必須明確地將其轉換為特定類別。<type> 語法用於簡化此操作。

例如,要將結果賦值給 BaseClass 類型的值,您可以編寫以下任一項

BaseClass b = !getdagop<BaseClass>(someDag);
BaseClass b = !cast<BaseClass>(!getdagop(someDag));

但是,若要建立重複使用另一個運算子的新 DAG 節點,則不需要轉換

dag d = !dag(!getdagop(someDag), args, names);
!gt(a, b)

如果 a 大於 b,則此運算子會產生 1;否則產生 0。參數必須是 bitbitsintstring 值。

!head(a)

此運算子會產生列表 a 的第零個元素。(另請參閱 !tail。)

!if(test, then, else)

此運算子會評估 test,它必須產生 bitint。如果結果不是 0,則會產生 then 運算式;否則會產生 else 運算式。

!interleave(list, delim)

此運算子會串聯 list 中的項目,在每對項目之間插入 delim 字串,並產生結果字串。列表可以是字串、整數、位元或位元的列表。空列表會產生空字串。分隔符號可以是空字串。

!isa<type>(a)

如果 a 的類型是給定 type 的子類型,則此運算子會產生 1;否則產生 0。

!le(a, b)

如果 a 小於或等於 b,則此運算子會產生 1;否則產生 0。參數必須是 bitbitsintstring 值。

!listconcat(list1, list2, ...)

此運算子會串連列表引數 list1list2 等,並產生結果列表。這些列表必須具有相同的元素類型。

!listremove(list1, list2)

此運算子會傳回 list1 的副本,並移除也出現在 list2 中的所有元素。這些列表必須具有相同的元素類型。

!listsplat(value, count)

此運算子會產生一個長度為 count 的列表,其元素都等於 value。例如,!listsplat(42, 3) 的結果為 [42, 42, 42]

!logtwo(a)

此運算子會產生 a 以 2 為底的對數,並產生整數結果。0 或負數的對數會產生錯誤。這是一個向下取整的運算。

!lt(a, b)

如果 a 小於 b,此運算子會產生 1;否則產生 0。引數必須是 bitbitsintstring 值。

!mul(a, b, ...)

此運算子會將 ab 等相乘,並產生乘積。

!ne(a, b)

如果 a 不等於 b,此運算子會產生 1;否則產生 0。引數必須是 bitbitsintstring 或記錄值。請使用 !cast<string> 來比較其他類型的物件。

!not(a)

此運算子會對 a 執行邏輯 NOT 運算,a 必須是整數。引數 0 會產生 1(真);任何其他引數都會產生 0(假)。

!or(a, b, ...)

此運算子會對 ab 等執行位元 OR 運算,並產生結果。如果所有引數都是 0 或 1,則可以執行邏輯 OR 運算。

!range([start,] end[,step])

此運算子會產生半開區間序列 [start : end : step),其型別為 list<int>start 的預設值為 0step 的預設值為 1step 可以是負數,但不能為 0。如果 start < endstep 為負數,或 start > endstep 為正數,則結果為空清單 []<list<int>>

例如:

  • !range(4) 等同於 !range(0, 4, 1),結果為 [0, 1, 2, 3]

  • !range(1, 4) 等同於 !range(1, 4, 1),結果為 [1, 2, 3]

  • !range(0, 4, 2) 的結果為 [0, 2]

  • !range(0, 4, -1)!range(4, 0, 1) 的結果為空。

!range(list)

等同於 !range(0, !size(list))

!repr(value)

value 表示為字串。不保證值的字串格式穩定。僅供除錯使用。

!setdagarg(dag,key,arg)

此運算子會產生一個 DAG 節點,其運算子和參數與 dag 相同,但會將 key 指定的參數值替換為 arg。該 key 可以是整數索引或字串名稱。

!setdagname(dag,key,name)

此運算子會產生一個 DAG 節點,其運算子和參數與 dag 相同,但會將 key 指定的參數名稱替換為 name。該 key 可以是整數索引或字串名稱。

!setdagop(dag, op)

此運算子會產生一個 DAG 節點,其參數與 dag 相同,但其運算子會替換為 op

範例:!setdagop((foo 1, 2), bar) 的結果為 (bar 1, 2)

!shl(a, count)

此運算子會將 a 邏輯左移 count 位元,並產生結果值。此運算會對 64 位元整數執行;對於 0…63 以外的位移計數,結果未定義。

!size(a)

此運算符產生字串、清單或 dag a 的大小。 DAG 的大小是參數的數量;不包含運算符本身。

!sra(a, count)

此運算符將 a 算術右移 count 位元,並產生結果值。運算在 64 位元整數上執行;對於 0…63 以外的移位計數,結果未定義。

!srl(a, count)

此運算符將 a 邏輯右移 count 位元,並產生結果值。運算在 64 位元整數上執行;對於 0…63 以外的移位計數,結果未定義。

!strconcat(str1, str2, ...)

此運算符將字串參數 str1str2 等連接起來,並產生結果字串。

!sub(a, b)

此運算符從 a 中減去 b,並產生算術差。

!subst(target, repl, value)

此運算符將 value 中所有出現的 target 替換為 repl,並產生結果值。 value 可以是字串,在這種情況下會執行子字串替換。

value 可以是記錄名稱,在這種情況下,如果 target 記錄名稱等於 value 記錄名稱,則運算符會產生 repl 記錄;否則會產生 value

!substr(string, start[, length])

此運算符擷取給定 string 的子字串。子字串的起始位置由 start 指定,其範圍可以介於 0 到字串長度之間。子字串的長度由 length 指定;如果未指定,則擷取字串的其餘部分。 startlength 參數必須是整數。

!tail(a)

此運算符產生一個新的清單,其中包含清單 a 中除第零個元素以外的所有元素。 (另請參閱 !head。)

!tolower(a)

此運算符將字串輸入 a 轉換為小寫。

!toupper(a)

此運算符將字串輸入 a 轉換為大寫。

!xor(a, b, ...)

此運算符對 ab 等進行位元互斥或運算,並產生結果。如果所有參數都是 0 或 1,則可以執行邏輯互斥或運算。

1.11 附錄 B:貼上運算子範例

以下範例說明在記錄名稱中使用貼上運算子的方式。

defvar suffix = "_suffstring";
defvar some_ints = [0, 1, 2, 3];

def name # suffix {
}

foreach i = [1, 2] in {
def rec # i {
}
}

第一個 def 並未使用 suffix 變數的值。第二個 def 則有使用 i 迭代器變數的值,因為它不是全域名稱。系統會產生以下記錄。

def namesuffix {
}
def rec1 {
}
def rec2 {
}

以下第二個範例說明在欄位值運算式中使用貼上運算子的方式。

def test {
  string strings = suffix # suffix;
  list<int> integers = some_ints # [4, 5, 6];
}

strings 欄位運算式在貼上運算子的兩側皆使用 suffix。它在左側會正常評估,但在右側則會逐字採用。integers 欄位運算式會使用 some_ints 變數的值和一個字面清單。系統會產生以下記錄。

def test {
  string strings = "_suffstringsuffix";
  list<int> ints = [0, 1, 2, 3, 4, 5, 6];
}

1.12 附錄 C:範例記錄

LLVM 支援的目標機器之一是 Intel x86。以下 TableGen 輸出顯示建立來代表 32 位元暫存器到暫存器 ADD 指令的記錄。

def ADD32rr { // InstructionEncoding Instruction X86Inst I ITy Sched BinOpRR BinOpRR_RF
  int Size = 0;
  string DecoderNamespace = "";
  list<Predicate> Predicates = [];
  string DecoderMethod = "";
  bit hasCompleteDecoder = 1;
  string Namespace = "X86";
  dag OutOperandList = (outs GR32:$dst);
  dag InOperandList = (ins GR32:$src1, GR32:$src2);
  string AsmString = "add{l}  {$src2, $src1|$src1, $src2}";
  EncodingByHwMode EncodingInfos = ?;
  list<dag> Pattern = [(set GR32:$dst, EFLAGS, (X86add_flag GR32:$src1, GR32:$src2))];
  list<Register> Uses = [];
  list<Register> Defs = [EFLAGS];
  int CodeSize = 3;
  int AddedComplexity = 0;
  bit isPreISelOpcode = 0;
  bit isReturn = 0;
  bit isBranch = 0;
  bit isEHScopeReturn = 0;
  bit isIndirectBranch = 0;
  bit isCompare = 0;
  bit isMoveImm = 0;
  bit isMoveReg = 0;
  bit isBitcast = 0;
  bit isSelect = 0;
  bit isBarrier = 0;
  bit isCall = 0;
  bit isAdd = 0;
  bit isTrap = 0;
  bit canFoldAsLoad = 0;
  bit mayLoad = ?;
  bit mayStore = ?;
  bit mayRaiseFPException = 0;
  bit isConvertibleToThreeAddress = 1;
  bit isCommutable = 1;
  bit isTerminator = 0;
  bit isReMaterializable = 0;
  bit isPredicable = 0;
  bit isUnpredicable = 0;
  bit hasDelaySlot = 0;
  bit usesCustomInserter = 0;
  bit hasPostISelHook = 0;
  bit hasCtrlDep = 0;
  bit isNotDuplicable = 0;
  bit isConvergent = 0;
  bit isAuthenticated = 0;
  bit isAsCheapAsAMove = 0;
  bit hasExtraSrcRegAllocReq = 0;
  bit hasExtraDefRegAllocReq = 0;
  bit isRegSequence = 0;
  bit isPseudo = 0;
  bit isExtractSubreg = 0;
  bit isInsertSubreg = 0;
  bit variadicOpsAreDefs = 0;
  bit hasSideEffects = ?;
  bit isCodeGenOnly = 0;
  bit isAsmParserOnly = 0;
  bit hasNoSchedulingInfo = 0;
  InstrItinClass Itinerary = NoItinerary;
  list<SchedReadWrite> SchedRW = [WriteALU];
  string Constraints = "$src1 = $dst";
  string DisableEncoding = "";
  string PostEncoderMethod = "";
  bits<64> TSFlags = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0 };
  string AsmMatchConverter = "";
  string TwoOperandAliasConstraint = "";
  string AsmVariantName = "";
  bit UseNamedOperandTable = 0;
  bit FastISelShouldIgnore = 0;
  bits<8> Opcode = { 0, 0, 0, 0, 0, 0, 0, 1 };
  Format Form = MRMDestReg;
  bits<7> FormBits = { 0, 1, 0, 1, 0, 0, 0 };
  ImmType ImmT = NoImm;
  bit ForceDisassemble = 0;
  OperandSize OpSize = OpSize32;
  bits<2> OpSizeBits = { 1, 0 };
  AddressSize AdSize = AdSizeX;
  bits<2> AdSizeBits = { 0, 0 };
  Prefix OpPrefix = NoPrfx;
  bits<3> OpPrefixBits = { 0, 0, 0 };
  Map OpMap = OB;
  bits<3> OpMapBits = { 0, 0, 0 };
  bit hasREX_WPrefix = 0;
  FPFormat FPForm = NotFP;
  bit hasLockPrefix = 0;
  Domain ExeDomain = GenericDomain;
  bit hasREPPrefix = 0;
  Encoding OpEnc = EncNormal;
  bits<2> OpEncBits = { 0, 0 };
  bit HasVEX_W = 0;
  bit IgnoresVEX_W = 0;
  bit EVEX_W1_VEX_W0 = 0;
  bit hasVEX_4V = 0;
  bit hasVEX_L = 0;
  bit ignoresVEX_L = 0;
  bit hasEVEX_K = 0;
  bit hasEVEX_Z = 0;
  bit hasEVEX_L2 = 0;
  bit hasEVEX_B = 0;
  bits<3> CD8_Form = { 0, 0, 0 };
  int CD8_EltSize = 0;
  bit hasEVEX_RC = 0;
  bit hasNoTrackPrefix = 0;
  bits<7> VectSize = { 0, 0, 1, 0, 0, 0, 0 };
  bits<7> CD8_Scale = { 0, 0, 0, 0, 0, 0, 0 };
  string FoldGenRegForm = ?;
  string EVEX2VEXOverride = ?;
  bit isMemoryFoldable = 1;
  bit notEVEX2VEXConvertible = 0;
}

在記錄的第一行中,您可以看到 ADD32rr 記錄繼承自八個類別。雖然繼承階層很複雜,但使用父類別比為每條指令指定 109 個個別欄位要簡單得多。

以下是用來定義 ADD32rr 和其他多個 ADD 指令的程式碼片段

defm ADD : ArithBinOp_RF<0x00, 0x02, 0x04, "add", MRM0r, MRM0m,
                         X86add_flag, add, 1, 1, 1>;

defm 語法告訴 TableGen,ArithBinOp_RF 是一個多類別,其中包含繼承自 BinOpRR_RF 的多個具體記錄定義。而該類別又繼承自 BinOpRR,後者繼承自 ITySched,依此類推。欄位繼承自所有父類別;例如,IsIndirectBranch 繼承自 Instruction 類別。