Tablegen Language Tutorial

Tablegen Language Tutorial

很难想象,网络上竟然搜索不到可以称为"教程"的Tablegen资料. 唯一可靠的资料是官方的ProgRef, 作为一个Reference, 它是非常合格的, 详尽而精确, 但是如果把它作为教程来阅读, 则有一些缺点:

  1. 过于详尽, 即便是一些不太重要的特性,也需要用完整精确的内容来描述.
  2. 过于严谨, 即便是一些简单的特性,也需要用严格的方式来描述,比如 EBNF 风格的 syntax notation, 至少我的大脑是无法 zero cost 的 parse 这种notation的.
  3. 内容排布不合理, 一些不重要的特性经常位于较为靠前的位置, 且总结性的内容较少.

为了避免这些问题, 本文将按 Quick Start 风格的 Tutorial 来组织,先熟悉最核心/最重要的概念, 再学习其他的功能. 通过这篇教程,你应该能够

  1. 阅读现有的绝大部分.td文件, 写出语法正确的.td文件
  2. 知道该去看ProgRef中的哪一部分来厘清自己的疑惑.

最后, 本文尽可能写的短小, 一方面你可以仅阅读本文快速上手, 另一方面, 如果你希望能完整的学习Tablegen, 阅读本文之后再去看ProgRef也不会显得浪费太多时间, 反而应该会让你看得更加顺畅.

Introduction

Tablegen 自身分为两部分,一部分是Frontend, 一部分是Backend. Frontend负责从.td文件解析结构化的数据, Backend负责从这些数据中生成新的代码. 也就是说, Tablegen 的总体功能是一个代码生成器. llvm-project 现在内嵌了四个 tablegen 的后端实现, 分别用于llvm,lldb,clang和mlir.

不同的后端对前端数据的解释方法非常不同, 这只能去看后端相关的文档, 本文主要关注前端的DSL, 描述tablegen解析.td文件的DSL规则,而不涉及后端的代码生成.

总的来说, 学习之前应该有一下基本认识:

  1. 应当把Tablegen DSL视为一种C/C++语言的变体,尽管你能看到def这样的Python关键词.
  2. .td文件仅仅用于记录, 不要把后端的功能和前端的实体绑定起来, 不同的后端可能对同样的数据有非常不同的解释.
  3. 虽然Tablegen声称自己是一种"声明式"语言, 但是仅有涉及field之间的交叉引用时,才是按照依赖顺序处理的,其他场合都可以认为代码是顺序执行的

.td 这个后缀意思是 "target (machine) description", 这对于llvm-tblgen是非常有意义的. 但对其他后端, 则显得不太合理,我想这也是一个历史问题, 否则可能叫.tg似乎更加合理.

Quick Start

我们将从下面的例子开始, 逐步介绍 Tablegen. 假设我们现在准备用Tablegen建立一个动物数据库, 那么我们可能会写出下面这样的例子.

class Animal<string type_ , int type_id_> {
  string name = NAME;
  string type = type_;
  int type_id = type_id_;
}

class Dog : Animal<"Mammalia" , 0> {
  string nick_name = ?;
}

def Husky : Dog {
  defvar s = "King of Funny";
  let nick_name = s;
}

class Animal<string type_ , int type_id_>

class是一个用于定义类型的关键词, 可选带有一个<>包围的模板参数列表, 可选带有一个基类列表, 也可选带有一个{//stmts}型的init-body.

init-body类似于构造函数, 是一段代码,里面主要有三种stmt, 分别是defvar,let,filed-def,

  • defvar a = b;用于定义局部变量, 它支持类型推导. defvar创建的值不允许被修改,defvar初始化时不能引用field.
  • Type var_name = init_value;用于进行field-def,它将会创建一个新的filed,并用init_value初始化. (注意,这种形式的stmt只能作为filed-def使用,所以不会出现在其他scope中.)
  • let c = d;用于修改已经存在的field

模板参数列表主要用于传递额外的模板参数, 每个class的模板参数中会有一个隐含的NAME参数, 这个参数会在class被实例化时传入.
class Animal中,我们设计了type_type_id_这两个参数, 在init-body中,使用这两个参数及built-in的NAME参数创建了3个新的field.

使用模板参数的<>符号也额外强调了实参值都是constexpr

基类列表则允许多继承, 每个基类的init-body会在当前class的init-body之前被执行.

class Dog : Animal<"Mammalia" , 0>

class Dog继承自class Animal, 和 C++ 一样, 我们只能继承自具体类, 所以必须在继承时进行特化. 我们在init-body内为Dog创建了一个额外的field,即string nick_name = ?;, 这里的?是一个特殊的builtin,它表示一个未初始化的值. 如果你乐意, 加一个模板参数来传递nick_name也是可以的, 这只是一个风格问题.

def Husky : Dog

def是一个用于实例化class的关键词,def时,名字是可选的,可选带有一个基类列表,可选带一个init_body.

def创建的对象及class的实例一般被称为concrete record. 如果没有特殊说明,本文之后的record均特指concreate record (这是因为官方的ProgRef还把class称为abstract record,所以"record"可能会引起混淆)

如果def时没有给出名字,那么这将创建一个匿名record, 匿名record虽然可以被后端读取到, 但是一般约定后端不对匿名record做任何处理.

def创建的匿名record一般没有任何作用,因为没有任何方法可以引用它.创建匿名record的另一种方式是手动调用class, 例如int c = Dog<>.type_id;Dog d = Dog<>;.

需要注意的是, 匿名record总是匿名的,defvar a = Dog<>;Dog b = Dog<>并不能将其转换为具名record, ab仅仅提供了引用的symbol.

def的基类列表及init-bodyclass功能一致. 在此,为了展示defvarlet的用法,我们在def Huskyinit-body中创建了一个变量s,并通过let nick_name = "King of Funny"; 修改了基类中已经定义的field.

Record-Creation:

总的来说,record creation 是两步执行的,第一步递归执行init-body,优先执行基类列表中靠左的class的init-body, 第二步则按照依赖关系更新field之间的交叉引用.

例如,

class X {
    int a = 0; // 1. 不涉及field引用, 更新 a 为 0
    int b = !add(a,1); // 2. 涉及field引用, 更新 b->a
    int c = a; // 3. 涉及 field引用, 更新 c->a
    int d = 1; // 4. 不涉及field引用, 更新 d 为 1
}

def x : X{
    let a = 11; // 4 不涉及field引用, 更新 a 为11
    let c = !add(b,1);// 5. 涉及field引用,更新 c->b
    let d = !add(c,1); // 6. 涉及field引用,更新 d->c
    defvar const10 = 10;// 7. defvar
    int e = !add(d,const10); // 8. 涉及filed引用,更新 e->d
}

例如, 上面的代码在按照1,2,3,4,5,6,7,8的顺序执行完之后,所有init-body就执行完成了,最后按照依赖关系, 额外执行一次交叉引用的更新,导致a=11,b=12,c=13,d=14,e=24

Value

最后,在 Qucik Start 的结尾, 我们需要补充一个已经出现过很多次的concept: "Value".

在Tablegen中,所有的value本质上都是由 constexpr 输出的值, 而形成这些constexpr的基本类型则仅有bit, int, string, bits<N>, list<T>, dag及 concreate record.

  • bit a = 0;,只能取0,1的二进制值.

  • int a = 0;,没什么特别的.注意,整数literal可以用0xffff,0b1111这样的表示

  • string a = "foo";,没什么特别的.注意,string literal 有"foo",[{foo}]两种形式,后者类似于python的"""foo""",主要功能是用于多行字符串.

  • bits<4> a={0,1,0,1};,代表了若干bit,可以用a{0}这样的语法访问特定位,a{0..2}方位特定的一段.

  • list<int> a=[1,2,3];,代表了list,可以用a[0],a[0..2]这样的语法访问. list内的T可以是自定义的class,例如list<Animal>,则此时只能用子类的实例初始化. [] expression 还支持type-hint,这主要用于空列表, 例如defvar a = []<int>

  • dag a = (record ARGs...);,形式上是一个括号表达式, 代表了单个dag node, 括号表达式整体代表了dag node的出边.

    • 括号表达式的第一个元素必须是一个record,然后用空格分开.
    • ARGs...是一个逗号表达式,可以有任意多的元素, 代表dag node 的入边, 如果入边是另外一个dag node, 那么就可以形成图的结构了.
    • ARG的典型形式有三种, value0 , $tag1, value2:$tag2
    • value0, 仅使用一个值value0
    • $tag1, 仅有一个tag1标记,而没有值
    • value2:$tag2,同时有tag2标记,且有值
    • dag中的record, tagvalue对前端没有意义,由后端来解释.
    • 在构建dag时,允许使用类似CRTP的策略def rec1 : A<(my_op rec1)>;
  • record也是value, 需要value的地方,都可能使用record.(匿名record具名record均可)

其他要点

Preprocessor

tablegen支持include,其语义和C的#include完全一致,在命令行中还支持-Ipath/to/search来动态添加搜索目录.

tablegen支持以#ifdef为核心的一系列预处理宏,其用法和C也完全一致, 例如实现条件编译及header-guard, 在命令行中也可以通过-D动态添加定义.

Expression

Tablegen自身没有operator token, 但是支持一系列bang operator, 这些bang operator 可以视为 tablegen 的 built-in method, 例如int Yplus1 = !add(Y, 1);

另外, #可以作用于stringlist类型,用于进行concat, 它的规则比较复杂,可以参考ProgRef. 比较典型的应用场景是在defm中与NAME配合构造具名record的名字.

init-body 之外的 defvar,let

defvar功能和init-body一致,只不过这样创建的var作用域不同.

不在init-body内的的let都称为global-let,它的语法有所不同,具体为

let a=1,b=2 in {
  // stmts
}

它的功能为: 对于新scope内的所有classdef, 在其init-body开始的地方插入一系列let语句,例如

let a=1 in {
  class X {
    STMTS0
  }
  def x : X {
    STMTS1
  }
}

等价为

class X {
  let a = 1;
  STMTS0;
}
def x : X {
  let a = 1;
  STMTS1;
}

这种插入行为导致了一个隐形的依赖: let 修改的field必须在对应的init-body执行时已经存在,如果不存在,前端会直接报错.

multiclass && defm

multiclassdefm 实现了 macro 功能, 前者负责定义macro, 后者负责展开macro, 展开的规则如下:

  1. 所有multiclass型基类都生成一个defm指令
  2. 所有class基类都在展开时作为新基类插入到defm/def.
  3. 展开后的body内所有record都会进行name-mangle

例如,对于下面的例子

class SomeCls{
}
multiclass FooBase{
}
multiclass FooMember{
}
multiclass Foo : FooBase{
  def _a;
  defm _b:FooMember; 
}

那么对于defm foo : Foo,SomeCls;,它展开后等价于下面的代码.

defm foo  : FooBase; // multi基类直接生成指令
def foo_a : SomeCls; // 对应原来的`def _a;`,被mangle且插入了新的基类
defm foo_b : FooMember,SomeCls;// 对应原来的 `defm _b:FooMember`,被mangle且插入了新的基类

在需要时,我们可以在multiclass的body内手动使用NAME进行mangle,这个NAMEdefm时传入的, 手动mangle会抑制自动mangle机制.例如

multiclass Foo{
  def a_#NAME; 
}
defm foo:Foo;

展开后为def a_foo;

defset,if,foreach,assert

defset是一个语法糖,它可以自动把一系列record收集到一个列表var里,例如

 defset list animal_list = {
  def a:Animal;
  def b:Animal;
 }

 // 等价于
def a:Animal;
def b:Animal;
defvar animal_list = [a,b];

if, foreach, assert 的功能则和你想象的一模一样, 在需要时再直接看文档即可.

参考资料

  1. ProgRef: https://llvm.org/docs/TableGen/ProgRef.html

发表评论

邮箱地址不会被公开。 必填项已用*标注