前言
在阅读java ORM框架spring data jpa的源码时,发现Hibernate(spring data jpa依赖Hibernate核心代码)在底层使用了AST抽象语法树,将hql转换为sql,这激发了我研究AST的兴趣。
AST概述
AST(Abstract Syntax Tree)抽象语法树多用作编程语言的分析和转换,C语言编译器将c源码转换为汇编,java编译器将java代码转换为java字节码,还有一些比较高级的用法,比如同种语言代码的优化、不同种语言代码的相互转化等。
抽象语法树从术语定义上就能看出,本身是一种树状的数据结构,“1 + 2”使用抽象语法树可以表示为:
这样做的目的是,将原始语句分解成了单个的语法单元,同时保留了语法单元之间的层次结构,后续通过对语法单元的改造或者替换,重新按照某种规则遍历,即可完成原始语句到目标语句的转换。比如,以“1 + 2”为例,可以将“+”替换为add,1和2理解为add函数的参数,即可实现原始运算语句到函数调用的转换。
AST抽象语法树的使用,整体上可以分为三步:
- 解析:将原始语句解析为抽象语法树
- 转换:操作抽象语法树节点完成转换
- 生成:根据转换后的抽象语法树生成目标语句
其中,最重要的是需要理解抽象语法树的结构,这是解析和生成抽象语法树的基础。
AST结构
个人感觉,理解抽象语法树结构的最佳例子是xml格式文本,这两种形式的对比能够充分显示抽象语法树结构是为了表示空间或者时间或者逻辑关系上的层次。
<city>
<park>ZhongShan</park>
<people>
<id>123456</id>
<name>"Tom"</name>
<son>
<id>123457</id>
<name>"John"</name>
</son>
</people>
</city>
上面构造的xml是用来表示,有个城市中一个公园叫ZhongShan,这个城市住着一个人,他的id是123456,姓名叫Tom。Tom有一个儿子,id是123457,名叫John。这种嵌套的关系用AST表示,如图:
xml到AST图的转换,实际上是空间层次的对应关系。可能大家还是会对逻辑层次关系如果转换成AST有疑问,所以这里再以表达式“5-2*(3-1)+4”为例来进一步分析这种转换。
如图为“5-2*(3-1)+4”的抽象语法树,其实就是中缀表达式的树形表示。这里构造的基本逻辑是,越是排在后面的运算离根节点越近。括号中的表达式“3-1”最先被运算,因此位于最底层。而“+ 4”运算最后执行,所以这里的“+”位于根节点。表达式的AST图和xml的AST图不同之处在于,这里同层还存在顺序关系,左边的节点优先级要高于右边节点的优先级。
通过上述两个例子的演示,这里再介绍代码的抽象语法树似乎就容易理解多了。
while(b > 0)
{
if(a < b)
a = b-a;
else
b = a-b;
}
return a;
在代码的构造抽象语法树之前,首先需要明白,这些语句之间存在执行上的顺序关系,也存在不同层级下的嵌套关系。比如上述代码,while循环语句块由大括号包裹,和return语句处于一个层级,但while语句块先会被执行。而while语句块中又包含了循环判断“b != 0”和if的判断语句块,这属于while下语句的嵌套。其次,语句结构也更加复杂,比如上例中的语句除了表达式、赋值语句之外,还有while、if和对应的判断条件,所以会拆分出更多的语法单元。
statement_list语法单元,用来表示子节点都是并列依次执行的语句。while子树中包含了while循环的条件判断和if的语句块,if语句块包含了if的条件判断和两个赋值语句。在编译器构造同样类型语法树时,一般会使用与具体语言无关的语法单元的命名,这样在后续转换的转换只是对节点采用不同的翻译模式而已,做到了和具体语言的解耦。
AST解析
使用AST对原始语句解析时,需要先进行词法分析。
词法分析会根据既定的语法单元表,将原始语句分割成一维数组语法单元列表(token表)。语法单元表根据场景不同,如上述三个例子,可以自行定义。一般而言,词法分析时会将连续的空格当做分割符,自动切分语法单元。
获得token表之后,再使用语法分析,将一维无结构的token表转化为树形结构。在语法分析时,也会验证语法的正确性。如果出现不符合语法的语句,就会抛出错误,编译报错一般就是这个阶段的产物。
转换
根据目的的不同,AST转换没有一套固定的标准,有时候只是对匹配节点简单的替换,有时候可能是对匹配子树结构的调整或者替换。一般这个过程包含遍历和转换两步。
抽象语法树可以使用一般树的遍历方法。如果忘记了,可以温习一下先序遍历、中序遍历和后序遍历。目前使用比较多的antlr中,使用了先序遍历和后序遍历。
生成
生成是AST解析的****,生成过程中也需要用到树的遍历。虽然有时候生成和转换可能糅合在一起同时进行,但是生成逻辑比单纯进行转换时要复杂。
在生成的遍历过程中,需要对所有不同类型语法单元的节点的所有情况定义不同的处理逻辑,而不同类型语法节点下子树的遍历顺序也有可能会不同。所以需要为所有情况进行枚举。
这种情况的感性认知,可以以上述例子中代码语法抽象树为例。在statement_list节点下,子树代表的语句块需要按顺序并列,所以在先遍历完while语句块子树之后,回到statement_list节点,再到return子树下,生成return语句需要另起一行。而while节点下需要先考虑条件语句,在不回车换行的基础上添加括号,完成while( b > 0 )语句的构造。同理while节点下while体中需要自动添加{}…使用抽象语法树的生成逻辑需要事无巨细的列出可能遇到的所有情况和处理方式。