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