Clang之语法抽象语法树AST

语法分析器的任务是确定某个单词流是否能够与源语言的语法适配,即设定一个称之为上下文无关语言(context-free language)的语言集合,语法分析器建立一颗与(词法分析出的)输入单词流对应的正确语法树。语法分析树的建立过程主要有两种方法:自顶向下语法分析法和自底向上分析法。AST作为语法分析树(parse tree)的一种简写方式,它独立于具体编程语言(C++、Java、C等),而且与语法分析树的建立过程无关(自顶向下和自底向上逻辑等价),是联系编译器前端、后端的重要接口。Clang的AST树与其他一些AST有些区别,如前者括号表达式为未裁剪模式(in an unreduced form),后者一般会尽量省去多余的括号,这样方便建立重构工具(clang\docs\IntroductionToTheClangAST.rst中说明)。

一、AST的直观印象

可以使用clang –emit-ast input.cpp生成AST的二进制文件input.ast,也可以直接打印输出如下:

clang -Xclang -ast-dump -fsyntax-only input.cpp
TranslationUnitDecl 0x5db3130 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x5db3670 <<invalid sloc>> <invalid sloc> implicit __int128_t ‘__int128‘
|-TypedefDecl 0x5db36d0 <<invalid sloc>> <invalid sloc> implicit __uint128_t ‘unsigned __int128‘
|-TypedefDecl 0x5db3a90 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list ‘__va_list_tag [1]‘
|-CXXRecordDecl 0x5db3ae0 <./test.h:1:1, line:7:1> line:1:7 class hello definition
| |-CXXRecordDecl 0x5db3bf0 <col:1, col:7> col:7 implicit referenced class hello
| |-AccessSpecDecl 0x5db3c80 <line:2:1, col:7> col:1 public
| |-CXXConstructorDecl 0x5db3d20 <line:3:1, col:9> col:1 hello ‘void (void)‘
| | `-CompoundStmt 0x5dfb108 <col:8, col:9>
| |-CXXDestructorDecl 0x5dfafa0 <line:4:1, col:10> col:1 ~hello ‘void (void)‘
| | `-CompoundStmt 0x5dfb120 <col:9, col:10>
| |-AccessSpecDecl 0x5dfb050 <line:5:1, col:8> col:1 private
| `-FieldDecl 0x5dfb090 <line:6:1, col:5> col:5 hello_private ‘int‘
|-VarDecl 0x5dfb150 <input.cpp:8:1, col:5> col:5 innerDefiner ‘int‘
|-VarDecl 0x5dfb1c0 <line:11:1, col:5> col:5 outDefiner ‘int‘
|-FunctionDecl 0x5dfb2f0 <line:13:1, line:15:1> line:13:6 used testFunction ‘void (int)‘
…

从上可以看出,每一行包括AST node的类型,行号、列号以及类型的信息。最顶部一般是TranslationUnitDecl【一个Cpp文件以及那些#include包括的文件称为翻译单元(TranslaitonUnit)】,如上面所示,test.h中的类也会进入AST树中。TypedefDecl、CXXRecordDecl、CompoundStmt等称为AST node,比较常见的有Stmt、Decl和Expr等。

二、AST树

AST树的所有信息都打包进了ASTContext(All information about the AST for a translation unit is bundled up in the class)。ASTContext中有一个重要的成员函数getTranslationUnitDecl,获取TranslationUnitDecl(其父类是Decl,DeclContext),这是AST树的顶层(top level)结构,可以通过其decls_begin()/decls_end()遍历其保存的nodes,下面代码打印Kind,查看保存的node类型,正与上命令行使用-emit-ast输出的一级目录相同。

TranslationUnitDecl *dc=Unit->getASTContext().getTranslationUnitDecl();
if(dc){
for(DeclContext::decl_iterator dit=dc->decls_begin() ; dit!= dc->decls_end();dit++){
std::cout<<dit->getDeclKindName()<<std::endl;}

AST树的本地化存储和读入借助ASTWriter和ASTReader,Clang还提供了一些高层次的类ASTUnit(Utility class for loading a ASTContext from an AST file),将AST树保存为二进制文件,也可以加载AST文件构建ASTContext。

  • 加载AST文件构建ASTContext:

    ASTUnit::LoadFromASTFile("input.ast",Diags,opts);
  • 将AST树保存为二进制文件。
ASTUnit* Unit=ASTUnit::LoadFromCompilerInvocationAction(invocation, Diags1); 

if(Unit&& !Unit->Save("output")){//这里的保存成功是返回false std::cout<<"save success!"<<std::endl; }

三、AST树的生成

构建AST树的核心类是ParseAST(Parse the entire file specified, notifying the ASTConsumer as the file is parsed),为了方便用户加入自己的actions,clang提供了众多的hooks。为更好的使用这些hooks,需弄清楚这几个类的关系—RecursiveASTVisitor,ASTConsumer,ParseAST, FrontendAction,CompilerInstance。      初始化CompilerInstance之后,调用其成员函数ExcutionAction, ExcutionAction会间接依次调用FrontendAction的6个成员函数(直接调用的是FrontendAction的三个public 接口,BeginSourceFile,Execute,EndSourceFile),而FrontendAction的ExecuteAction会 最终调用语法分析函数ParseAST(未强制要求ParseAST放入ExcuteAction,但ASTFrontendAction如此)。 ParseAST在分析过程中,又会插入ASTConsumer的多个句柄(用得最多是HandleTopLevelDecl和 HandleTranslationUnit)。FrontendAction是文件级别的动作,ASTConsumer则与一个Translation Unit内部处理过程相关。RecursiveASTVisitor是针对AST node的遍历,一般需要ASTConsumer中呈现的AST node(如TranslationUnitDecl)作为参数。   

FrontendAction:Abstract base class for actions which can be performed by the frontend.FrontendAction 是抽象类,Clang还提供了几个继承子类 ASTFrontendAction,PluginASTAction,PreprocessorFrontendAction。 FrontendAction有三个public interface。

BeginSourceFile:该函数运行在options和FrontendAction初始化完成之后,每个文件Parse之前。如果该函数返回false,则后面的步骤不会执行。

Excute:Set the source manager‘s main input file, and run the action.

EndSourceFile():在parse完之后,做一些清理和内存释放工作。(Perform any per-file post processing, deallocate per-file objects, and run statistics and output file cleanup code)。


CreateASTConsumer(CompilerInstance &CI, StringRef InFile)


在Compile之前,创建ASTConsumer。在建立AST的过程中,ASTConsumer提供了众多的Hooks。被FrontendAction的公共接口BeginSourceFile调用。


BeginInvocation(CompilerInstance &CI)


在BeginSourceFileAction执行之前,该函数内还可以修改CompilerInvocation,即CompilerInstance编译参数选项。被FrontendAction的公共接口BeginSourceFile调用。


BeginSourceFileAction(CompilerInstance &CI,StringRef Filename)


处理单个输入文件之前,做一些处理工作。被FrontendAction的公共接口BeginSourceFile调用。


ExecuteAction()


执行动作。被FrontendAction的公共接口Execute调用。


EndSourceFileAction()


Callback at the end of processing a single input,被FrontendAction的公共接口EndSourceFile调用。


shouldEraseOutputFiles()


Determine if the output files should be erased or not. 被FrontendAction的公共接口EndSourceFile调用。

ASTConsumer:Abstract interface for reading ASTs,有两个重要的句柄HandleTopLevelDecl和HandleTranslationUnit。其他句柄有:HandleInlineMethodDefinition,HandleTagDeclDefinition,HandleTagDeclRequiredDefinition,HandleCXXImplicitFunctionInstantiation等。

CompilerInstance:是一个编译器实例,综合了一个Compiler需要的objects,如Preprocessor,ASTContext(真正保存AST内容的类),DiagnosticsEngine,TargetInfo等等。

CompilerInvocation:为编译器执行提供各种参数,它综合了TargetOptions 、DiagnosticOptions、HeaderSearchOptions、CodeGenOptions、DependencyOutputOptions、FileSystemOptions、PreprocessorOutputOptions等各种参数。如下从命令行解析成CompileInvocation。

int main(int argc,char **argv){
  CompilerInvocation *invocation;
 if(argc>1){
IntrusiveRefCntPtr<clang::DiagnosticsEngine> Diags;
invocation=clang::createInvocationFromCommandLine(llvm::makeArrayRef(argv+1,argv+argc),Diags) ;

四、RecursiveASTVisitor

AST nodes are modeled on a class hierarchy that does not have a common ancestor,AST nodes模型化的这些类包括Stmt,Type, Attr,Decl,DeclContext等,这些高度抽象的类又有众多子类,为了实现统一方式访问这些内部数据结构,RecursiveASTVisitor采用了“非严格意义”访问者设计模式(参见http://blog.csdn.net/zhengzhb/article/details/7489639),RecursiveASTVisitor是“抽象访问者”,“访问者”则是用户自己定义的RecursiveASTVisitor子类,“抽象元素类”是如上所述的Stmt,Type等。严格意义上的访问者设计模式,“元素类”都有一个统一的接口(如accept());而在这里,“元素类”没有统一的接口,发起访问只能通过“访问者”,而且没有统一的访问接口。

五、RecursiveASTVisitor功能

RecursiveASTVisitor主要完成以下三任务(称为#Task1,#Task2,#Task3),代码中原文(删除解释部分):

1、traverse the AST (i.e. go to each node);

2、at a given node, walk up the class hierarchy, starting from the node‘s dynamic type, until the top-most class (e.g. Stmt,Decl, or Type) is reached.

3、 given a (node, class) combination, where ‘class‘ is some base class of the dynamic type of ‘node‘, call a user-overridable function to actually visit the node.

#Task1要求给定一个root节点,深度优先方法递归遍历下去。#Task1只是一种dispatch过程,由TraverseDecl、TraverseStmt、TraverseType等Traverse*(*表示node类型)成员函数实现,具体访问node还需#Task2和#Task3完成。

#Task2,#Task3实现的是针对一个具体节点的user-overridable function,#Task2通过WalkUpFrom*实现,#Task3通过Visit*实现。下面通过例子简单说明。

class Visitor : public RecursiveASTVisitor<Visitor> {
           TraverseNamespaceDecl(decl);
    virtual bool VisitDecl(Decl * decl){
         std::cout<<"Visit Decl!"<<std::endl;
          return true;}
    virtual bool VisitNamedDecl(NamedDecl *decl) {
std::cout<<"VisitNamedDecl!"<<decl->getQualifiedNameAsString()<<std::endl;
      return true;
    }
   virtual bool VisitNamespaceDecl(NamespaceDecl *decl){
      if(decl)
std::cout<<"VisitNamespaceDecl:"<<decl->getQualifiedNameAsString()<<std::endl;
         return true;}
};
Visitor vt;
vt.TraverseNamespaceDecl(decl);//decl是NamespaceDecl指针

Test1:假设被编译文件包含Namespace In{}申明。打印如下:

Visit Decl!
Visit NamedDecl!In
Visit NamespaceDecl:In

Test2:假设被编译文件包含:Namespace In{int a;},打印如下:

Visit Decl!
Visit NamedDecl!In
Visit NamespaceDecl:In
Visit Decl!
Visit NamedDecl!In::a

(1)Test2在Test1基础上还遍历了Namespace内部的申明,所以TraverseNamespace是以Namespace为root深度遍历整棵树。查看RecursiveASTVisitor.h实现过程如下:

Derived &getDerived() { return *static_cast<Derived *>(this); }

#define TRY_TO(CALL_EXPR)                                                        do {                                                                             if (!getDerived().CALL_EXPR)                                                     return false;                                                              } while (0)

template <typename Derived>
bool RecursiveASTVisitor<Derived>::TraverseDecl(Decl *D) {
  if (!D)
    return true;
  if (!getDerived().shouldVisitImplicitCode() && D->isImplicit())
    return true;
  switch (D->getKind()) {
#define ABSTRACT_DECL(DECL)
#define DECL(CLASS, BASE)                                                        case Decl::CLASS:                                                                if (!getDerived().Traverse##CLASS##Decl(static_cast<CLASS##Decl *>(D)))          return false;                                                                break;
#include "clang/AST/DeclNodes.inc"}

template <typename Derived>
bool RecursiveASTVisitor<Derived>::TraverseDeclContextHelper(DeclContext *DC) {
  if (!DC)
    return true;
  for (auto *Child : DC->decls()) {
    if (!isa<BlockDecl>(Child) && !isa<CapturedDecl>(Child))
      TRY_TO(TraverseDecl(Child));
  }

#define DEF_TRAVERSE_DECL(DECL, CODE)                                          \
  template <typename Derived>                                                    bool RecursiveASTVisitor<Derived>::Traverse##DECL(DECL *D) {                     TRY_TO(WalkUpFrom##DECL(D));                                                   { CODE; }                                                                      TRY_TO(TraverseDeclContextHelper(dyn_cast<DeclContext>(D)));                   return true;                                                                 }
…
DEF_TRAVERSE_DECL(
    NamespaceDecl,
    {})

在上面代码中,大量运用了宏(“##”是分隔强制连接标志),生成了许多成员函数。展开宏,合并函数,还原代码如下:

template <typename Derived>
  bool RecursiveASTVisitor<Derived>::TraverseNamespaceDecl(DECL *D) {
  Derived * temp1= static_cast<Derived *>(this);// getDerived()
temp1-> WalkUpFromNamespaceDecl(D);//TRY_TO展开
  DeclContext *DC= dyn_cast<DeclContext>(D);
  If(!DC) return true;
//展开TraverseDeclContextHelper
      for (auto *Child : DC->decls()) {
if (!isa<BlockDecl>(Child) && !isa<CapturedDecl>(Child))
//展开TraverseDecl
    if (!Child)
        return true;
    if (!temp1->shouldVisitImplicitCode() && Child->isImplicit())
        return true;
  }
switch (D->getKind()) {
…
case Decl::NamedDecl://test2中被编译文件定义了“int a”,需要用到该分支temp1->TraverseNamedDecl(static_cast<NamedDecl *>(D));
break;
}}
Return true;}

由上展开代码得,在Traverse某个node时,会for循环node中保存的Decls,然后每个Decls再调用对应的Traverse函数,所以Test2比Test1多遍历了”int a;”对应的node。

(2)在Traverse node之初,会调用WalkUpFrom*函数。其内部实现如下:

#define DECL(CLASS, BASE)                                                        bool WalkUpFrom##CLASS##Decl(CLASS##Decl *D) {                                   TRY_TO(WalkUpFrom##BASE(D));                                                   TRY_TO(Visit##CLASS##Decl(D));                                                 return true;                                                                 }                                                                              bool Visit##CLASS##Decl(CLASS##Decl *D) { return true; }
#include "clang/AST/DeclNodes.inc"

clang/AST/DeclNodes.inc定义了如下:

#  define NAMESPACE(Type, Base) NAMED(Type, Base)
# define NAMED(Type, Base) DECL(Type, Base)
NAMESPACE(Namespace, NamedDecl)
NAMED(Named, Decl)

所以最终存在两个宏DECL(Namespace,NamedDecl),DECL(Named,Decl),还原代码得:

bool RecursiveASTVisitor<Derived>::WalkUpFromNameSpaceDecl(NameSpaceDecl *D) {
Derived * temp1= static_cast<Derived *>(this);// getDerived()
Temp1-> WalkUpFromNamedDecl(D);
Temp1->VisitNameSpaceDecl(D);
      return true;
  }
bool RecursiveASTVisitor<Derived>::WalkUpFromNamedDecl(NamedDecl *D) {
Derived * temp1= static_cast<Derived *>(this);// getDerived()
Temp1-> WalkUpFromDecl(D);
Temp1->VisitNamedDecl(D);
      return true;
  }
bool RecursiveASTVisitor<Derived>::WalkUpFromNamedDecl(NamedDecl *D) {
Derived * temp1= static_cast<Derived *>(this);// getDerived()
Temp1-> WalkUpFromDecl(D);
Temp1->VisitNamedDecl(D);
      return true;
  }
  bool RecursiveASTVisitor<Derived>::WalkUpFromDecl(Decl *D) { return getDerived().VisitDecl(D); }
  bool VisitDecl(Decl *D) { return true; }

所以WalkUpFrom会不断递归调用父节点的WalkUpFrom函数,最终调用的是VisitDecl,VisitNamedDecl,VisitNamespaceDecl,这正是上面所说#task 2,如果用户实现了WalkUpFromXX可以阻断向上的递归。

六、如何实现RecursiveASTVisitor继承类

申明一个类A,时期继承模板类RecursiveASTVisitor<A>,当需要访问某种节点时,就重载函数VisitXXX(XXX b)【如VisitNameDecl(NameDecl)】。

七、示例代码

http://yunpan.cn/cdYYj7IEE7WYD下clang/AST测试.rar

提取码:919d

时间: 2024-10-11 05:21:55

Clang之语法抽象语法树AST的相关文章

AST抽象语法树 Javascript版

在javascript世界中,你可以认为抽象语法树(AST)是最底层. 再往下,就是关于转换和编译的"黑魔法"领域了. 现在,我们拆解一个简单的add函数 function add(a, b) { return a + b } 首先,我们拿到的这个语法块,是一个FunctionDeclaration(函数定义)对象. 用力拆开,它成了三块: 一个id,就是它的名字,即add 两个params,就是它的参数,即[a, b] 一块body,也就是大括号内的一堆东西 add没办法继续拆下去了

五分钟了解抽象语法树(AST)babel是如何转换的?

抽象语法树 什么是抽象语法树? It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code. 抽象语法树是源代码语法结构的一种抽象表示.它以树状的形式表现编程语言的语法结构,树上的每个节点

复杂网络,抽象语法树

近期看了一些软件抽象为复杂网络,以及软件抽象成静态语法树的文章.做一个小总结. 1.复杂网络是由大量的边和点组成的,边点都可以有类型,加权值,边还可以有方向.如何计算边和点的权值是一个关键点,如何在不执行代码的情况下确定边的方向,目前不确定是否已经解决. 有许多工具,可以直接扫描软件源代码,抽象为复杂网络.然而我还没亲身实践,且做个记录. Dependency Finder分析编译后的java代码,能够提取依赖图. Doxygen是使用c++开发的基于源代码注释的文档生成工具.但是这个注释,是人

使用PHP-Parser生成AST抽象语法树

0.前言 最近项目的流程逐渐清晰,但是很多关键性的技术没有掌握,也只能一步一步摸索. 由于要做基于数据流分析的静态代码分析,所以前端的工作如:词法分析.语法分析必不可少.Yacc和Lex什么的就不再考虑了,查了一天的资料,发现两款比较适合,一款是Java下的ANTLR,另一款是专门做PHP AST生成的PHP-Parser. ANTLR是编译原理领域比较著名的工具了,相对于Yacc和Lex,更加实用.但是对PHP的语法文件只有一个,折腾了半天才生成调通,发现不太适合,对于"$a=1"生

JavaScript的工作原理:解析、抽象语法树(AST)+ 提升编译速度5个技巧

这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! JavaScript 是如何工作的:深入V8引擎&编写优化代码的5个技巧! JavaScript 是如何工作的:内存管理+如何处理4个常见的内存泄漏 ! JavaScript 是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式! JavaScript 是如何工作的:

AST 抽象语法树

提起 AST 抽象语法树,大家可能并不感冒.但是提到它的使用场景,也许会让你大吃一惊.原来它一直在你左右与你相伴,而你却不知. 一.什么是抽象语法树 在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码.树上的每个节点都表示源代码中的一种结构. 之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节. 二.使用场景 JS 反编译,语法

Babel(抽象语法树,又称AST)

文章:https://juejin.im/post/5a9315e46fb9a0633a711f25 https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md 你了解过Babel吗? 了解过抽象语法树,又称AST,有学习过,也写过一个基于AST的乞丐版模板引擎,先是词法解析token,然后生产抽象语法树,然后更改抽象语法树,当然这是插件做的事情,最后根据新的A

解释抽象语法树

创建了抽象语法树之后,有两个选择:解释或编译.解释,简单地说,就是遍历树,同时执行操作:编译,就是改变成其他形式,对于机器执行来说可能更简单,通常可能更快.这一小节先讨论如何解释结果,下面一小节再讨论编译的内容,最后,再讨论何时应该用解释,何时应该用编译的问题. 下面的例子是一个很小解释器,解释抽象语法树的主要工作由函数interpret 完成,它遍历树,并同时执行需要的动作.逻辑相当简单,如果发现一个文字值或标识符,就返回相应值: | Ident (s) ->variableDict.[s]

抽象语法树(Abstract Syntax Tree)

抽象语法树(AST)表示组成程序的结构,可以让程序员更容易使用,F# 适宜这种开发的一个原因就是它的联合类型.这种类型非常适合表示语言,因为它可以用来表示相关而结构不相同的项目.下面就是抽象语法树的例子: type Ast = | Ident of string | Val of System.Double | Multi of Ast * Ast | Div of Ast * Ast | Plus of Ast * Ast | Minus of Ast * Ast 树非常简单,只包含一种类型: