2021SC@SDUSC
目录
概述
前面的两篇文章,简要的把用户命令(HQL)如何进入编译器,走完语法分析、词法分析,生成抽象语法树AST的过程了解了一遍。这有助于从整体上把握编译流程的顺序,有助于更好的分析后续的功能框架。接下来,编译流程进入第二个主要阶段:语义分析阶段。
如果把HQL语句比作一句话,那么前面的语法分析、词法分析就是用来分析这句话包含了哪些词、有哪些关键词。而语义分析,顾名思义,就是要读懂这句话。
Hive如何读懂HQL这句话?我们知道了,在前面的编译环节中,HQL提交给编译器,编译器所做的主要工作是:创建ParseDriver对象pd,将hql分析得到AST。
AST,即抽象语法树,AST是编译流程后续过程的输入,所以,它便是语义分析框架“读懂”hql语句的关键信息来源。现在我需要通过阅读源码,主要分析一下AST中都包含了哪些信息,这有更好的理解语义分析的工作原理。
AST相关类解析:ASTNode类
根据前面的分析,我们已经找到了HIVE进行语法分析和词法分析的依据文件。
编译器的语法定义文件是ql/parse/Hive.g,该文件中同时包含了词法定义和语法定义。
经查阅资料和咨询队友,Hive.g在经过程序处理后,会生成两个用于分析的Java类:HiveLexer和HiveParser,顾名思义,分别用于词法分析和语法分析。
这里分析的结果是AST(抽象语法树)且AST中节点的类型是CommonTree类。下面,我们直接来看AST节点类的代码
先看ASTNode类的主要成员数据和构造函数:
public class ASTNode extends CommonTree implements Node,Serializable {
private static final long serialVersionUID = 1L;
private transient StringBuilder astStr;
private transient ASTNodeOrigin origin;
private transient int startIndx = -1;
private transient int endIndx = -1;
private transient ASTNode rootNode;
private transient boolean isValidASTStr;
private transient boolean visited = false;
public ASTNode() {
}
/**
* Constructor.
*
* @param t
* Token for the CommonTree Node
*/
public ASTNode(Token t) {
super(t);
}
public ASTNode(ASTNode node) {
super(node);
this.origin = node.origin;
}
@Override
public Tree dupNode() {
return new ASTNode(this);
}
分析:
我们重点分析ASTNode的主要参数。
代码第一行,可以看出,ASTNode继承自CommonTree,并实现了序列化接口。
ASTNode类主要有下面一些私有成员数据:
private transient ASTNodeOrigin origin;
可知,一个ASTNode对象包含了一个ASTNodeOrigin对象。
private transient int startIndx = -1;
private transient int endIndx = -1;
这是两个私有变量,根据命名可以推测为索引起始点,可能用于树的遍历。
private transient boolean visited = false;
根据名称的含义,知道这是一个标识位,用于标识节点是否被访问过。
private transient ASTNode rootNode;
根据字面意思,很明显,这是根节点对象。
还可以知道,该类有多个的构造函数,第一个构造函数:public ASTNode(Token t) {super(t);}
参数是Token T ,一个toke对象,可知构造树节点所需的信息来自token对象。并且使用了super()函数,即调用了父类的以token对象为参数的构造方法。
下面分析几个ASTNode的主要成员函数
@Override
public void setUnknownTokenBoundaries() {
Deque<ASTNode> stack1 = new ArrayDeque<ASTNode>();
Deque<ASTNode> stack2 = new ArrayDeque<ASTNode>();
stack1.push(this);
while (!stack1.isEmpty()) {
ASTNode next = stack1.pop();
stack2.push(next);
if (next.children != null) {
for (int i = next.children.size() - 1; i >= 0 ; i--) {
stack1.push((ASTNode)next.children.get(i));
}
}
}
while (!stack2.isEmpty()) {
ASTNode next = stack2.pop();
if (next.children == null) {
if (next.startIndex < 0 || next.stopIndex < 0) {
next.startIndex = next.stopIndex = next.token.getTokenIndex();
}
} else if (next.startIndex >= 0 && next.stopIndex >= 0) {
continue;
} else if (next.children.size() > 0) {
ASTNode firstChild = (ASTNode)next.children.get(0);
ASTNode lastChild = (ASTNode)next.children.get(next.children.size()-1);
next.startIndex = firstChild.getTokenStartIndex();
next.stopIndex = lastChild.getTokenStopIndex();
}
}
}
这个函数前两句,建立了两个栈,栈的存放对象是AST节点,并且将当前节点压入第一个栈中。
之后,使用迭代,如果第一个栈非空,就从第一个栈中弹出抽象语法树节点,压入第二个栈中,并且,如果next节点存在子孙节点,就把其子孙节点压入栈中
第二个迭代过程的过程是:从第二个栈里弹栈,赋给next节点
如果next节点没有孩子节点且它的开始索引和结束索引之一<0,则将token.getTokenIndex()的值赋给这两个索引。如果next节点的子节点不为空,则分两种情况:如果next的startindex和stopindex均大于零,此时停止当前这层循环,开始下一次循环,否则如果next.children.size() > 0,再进行进一步处理。
经过分析,该函数的作用是,对于子树中的每个节点,确保start/stop index已经设立。先进行深度遍历,自下而上访问。仅更新满足条件:至少一个令牌索引< 0的节点。
紧接着,ASTNode类还有很多其他函数,如:
@Override
public void setParent(Tree t) {
super.setParent(t);
resetRootInformation();
}
@Override
public void addChild(Tree t) {
super.addChild(t);
resetRootInformation();
}
这些函数主要是继承并重写父类的函数,体现了父子类良好的继承性和差异性。
除了上面这些成员函数,下面这两个函数引起了我的注意。
/**
* @return information about the object from which this ASTNode originated, or
* null if this ASTNode was not expanded from an object reference
*/
public ASTNodeOrigin getOrigin() {
return origin;
}
/**
* Tag this ASTNode with information about the object from which this node
* originated.
*/
public void setOrigin(ASTNodeOrigin origin) {
this.origin = origin;
}
还记得上面的ASTNode类的成员数据里,有一个ASTNodeOrigin对象。上面这两个函数,官方对其的功能做出了注释,意思是:
返回有关此 ASTNode 源自的对象的信息,或 null, 如果此 ASTNode 不是从对象引用扩展的
使用有关此节点所在对象的信息,标记此 ASTNode 起源。
关于这个类更详细的信息以及和ASTNode类的联系等,将在之后分析介绍。
总结
ASTNode是Hive自己定义的一个树节点对象,它继承自Antlr的CommonTree类,并实现了Node和Serializable接口。我们在前面grammar options中看到,这个Parser返回的是CommonTree类型的对象,由于ASTNode是CommonTree的子类,所以并不违反协议。Hive之所以要这样做,主要是:
1.实现Node接口,以便可以使用Hive的遍历框架对AST进行遍历;
2.实现Serializable接口,以便可以将AST序列化。