Clang AST介绍
AST(Abstracted Syntax Tree)即抽象语法树,对于任何一门编程语言来说都是非常重要的工具,对于一般的compiler来说,都是将源码转换为AST,之后经由AST转换到特定的IR,在IR上进行一些与硬件特性无关的优化,之后再将优化后的IR转换为对应的汇编。因此AST直观的反应了使用者的编程思想。AST上一般进行的转换和优化不多,更多的是对语言特性的支持和检查。AST的中文介绍https://blog.csdn.net/philosophyatmath/article/details/38170131(入门介绍)
对于AST是如何构建的,之前的词法token分析是如何进行的,在https://www.twilio.com/blog/abstract-syntax-trees中已经进行了很好的说明,不在进行赘述。一般而言,每种编程语言都有对应的ast,包括Python,Perl,Fortran,C,Java等,也有很多开源的AST Viewer工具可供使用,比如https://astexplorer.net/。
本文主要针对LLVM的前端进行介绍。在现有LLVM的版本中,默认使用的前端是Clang,Clang包含token和AST构建的全过程。(Clang官方的introduction:https://clang.llvm.org/docs/IntroductionToTheClangAST.html)
AST树是源代码抽象语法结构,以树的形式表示语言的语法结构。树的每个节点都有相对应的源码的支持,树节点以不同的类型进行区分,下边将分别进行介绍。
对于LLVM Clang来说,顶层结构是TranslationUnitDecl,对AST树的遍历,实际上是遍历整个TranslationUnitDecl。遍历它的方式,一般通过:
DeclContext::decl_iterator D = Context.getTranslationUnitDecl()->decls_begin(); DeclContext::decl_iterator DEnd = Context.getTranslationUnitDecl()->decls_end(); while (D != DEnd)
的方式进行。
- Clang的使用命令
Clang尽最大可能兼容了gcc,因此将gcc修改为Clang,大概率只是换个名字就行,现在也有人尝试使用Clang编译一般的库,一般都能编过,而且代码运行效率一般不会特别差。有些系统下可能会面临std库无法找到的问题,需要额外添加-I和-L选项。
Clang相比gcc,额外增加了很多功能,比如AST树的打印。对于源文件test.cpp:
void foo(int* a, int *b) { if (a[0] > 1) { b[0] = 2; } }
使用Clang -fsyntax-only -Xclang -ast-dump test.cpp
简单解释下这里的命令,-fsyntax-only意味着只解析语法,不进行编译和链接;
-Xclang 是要使用clang特定的Xclang功能
-ast-dump是要打印AST
这里会得到一个额外着色的ast,在源码中,使用dump是没有额外着色的,需要使用dumpColor()得到。
2. AST树节点一般介绍(LangOptions()是CPP)
我会以这样一段程序对AST树节点进行介绍,包含了函数声明、函数调用、变量声明、for循环、if语句、指针变量等。
1 int foo(int a, int b,int *c){ 2 int ret = 0; 3 if(a > b){ 4 ret = a; 5 } 6 else { 7 ret = b; 8 } 9 for(int temp=0; temp<100; ++temp){ 10 *c = (*c + temp); 11 } 12 return ret; 13 } 14 15 int main(){ 16 int a = 1, b = 2; 17 int c =0; 18 int d = foo(a, b, &c); 19 return 0; 20 }
FunctionDeclaration
ParmVarDecl是参数节点
在AST层级,不区分函数声明和函数定义,统一用FunctionDecl来标识,两个区分主要看是否有函数体(Body),可以使用bool hasBody()来进行判断。
CompoundStmt
代表大括号,函数实现、struct、enum、for的body等一般用此包起来。
DeclStmt
定义语句,里边可能有VarDecl等类型的定义
VarDecl
变量定义,如果有初始化,可以通过getInit()获取到对应的初始化Expr
IfStmt
If语句,包括三部分Cond、TrueBody、FalseBody三部分,分别可以通过getCond(),getThen(), getElse()三部分获取,Cond和Then是必须要有的,Else可能为空
BinaryOperator
二元操作Op,=,>,<,<=,>=,==等各种二元操作都继承它,从继承关系来说:
通常通过getLHS()和getRHS()来分别获得其左右子节点
ImplicitCastExpr
隐形转换表达式,在左右值转换和函数调用等各个方面都会用到。
IntegerLiteral
定点Integer值
UnaryOperator
一元操作
CallExpr
函数调用Expr,子节点有调用的参数列表
ReturnStmt
返回语句
ForStmt
For语句对应,包括Init/Cond/Inc 对应(int a=0;a<mm;a++)这三部分,还有一部分是body,可以分别使用getInit() / getCond() / getInc() / getBody()来分别进行获取
ParenExpr
括号表达式
对于一些结构体的操作,比如struct或者enum,有自己的格式。
比如
struct dict { int a; int b; }; void vpp(){ struct dict news; struct dict olds = {1,2}; news.a = 1; news.b = b + 2; }
RecordDecl首先是struct的声明,采用的是RecordDecl的形式。
后边会出现对应的struct dict的类型
在使用时会发现,仍然使用DeclStmt的形式去声明使用InitExpr的方式来初始化,只不过这种特殊的格式是使用了InitListExpr的方式。
对于成员变量的使用,采用MemberExpr的方式来取值。
Reference:
https://clang.llvm.org/docs/index.html
https://clang.llvm.org/docs/UsersManual.html