2021SC@SDUSC
目录
概述
在上一篇的分析中,用户指令经过如下函数调用历程:
processCmd()—>CommandProcessor get()-->processLocalCmd()-->run-->runInternal()-->compileInternal()-->compile()
经过上述的层层调用,Driver的run方法最终会执行compile()操作,由Compiler作后续的语法解析和语义分析,这就是HQL语句从CLI进入编译器的简单程序逻辑。
前序分析②:语法分析生成AST的环节
接上一篇的函数调用,我们来分析compile()函数,这个函数包含了编译流程的大部分函数调用。这个函数很长(约3000行),我们知道,编译流程第一步骤的首要目标就是生成AST了,因此我首先快速浏览了整段代码,找到与生成树相关的内容。
// Trigger query hook before compilation
hookRunner.runBeforeParseHook(command);
ASTNode tree;
try {
tree = ParseUtils.parse(command, ctx);
} catch (ParseException e) {
parseError = true;
throw e;
} finally {
hookRunner.runAfterParseHook(command, parseError);
}
perfLogger.PerfLogEnd(CLASS_NAME, PerfLogger.PARSE);
根据之前的学习和分析,整个编译流程的第一步,是将HQL转化为AST,这个步骤主要由parse组件完成。
很明显,tree = ParseUtils.parse(command, ctx);一句,便是我们想寻找的重点。
ParseUtils封装了ParseDriver 对sql的解析工作,ParseUtils的parse方法,来看parse函数:
public static ASTNode parse(
String command, Context ctx, String viewFullyQualifiedName) throws ParseException {
ParseDriver pd = new ParseDriver();
ASTNode tree = pd.parse(command, ctx, viewFullyQualifiedName);
tree = findRootNonNullToken(tree);
handleSetColRefs(tree);
return tree;
}
第一句ParseDriver pd = new ParseDriver(),构造一个ParseDriver对象pd,;
紧接着ASTNode tree = pd.parse(command, ctx, viewFullyQualifiedName),用pd对象调用parse函数,这个函数的返回的是一个AST树实例,由此可见,生成抽象语法树的重点就在该函数,下面看parse()函数:
public ASTNode parse(String command, Context ctx, boolean setTokenRewriteStream)
throws ParseException {
if (LOG.isDebugEnabled()) {
LOG.debug("Parsing command: " + command);
}
HiveLexerX lexer = new HiveLexerX(new ANTLRNoCaseStringStream(command));
TokenRewriteStream tokens = new TokenRewriteStream(lexer);
if ( setTokenRewriteStream) {
ctx.setTokenRewriteStream(tokens);
}
lexer.setHiveConf(ctx.getConf());
}
HiveParser parser = new HiveParser(tokens);
if (ctx != null) {
parser.setHiveConf(ctx.getConf());
}
parser.setTreeAdaptor(adaptor);
HiveParser.statement_return r = null;
try {
r = parser.statement();
} catch (RecognitionException e) {
e.printStackTrace();
throw new ParseException(parser.errors);
}
if (lexer.getErrors().size() == 0 && parser.errors.size() == 0) {
LOG.debug("Parse Completed");
} else if (lexer.getErrors().size() != 0) {
throw new ParseException(lexer.getErrors());
} else {
throw new ParseException(parser.errors);
}
ASTNode tree = (ASTNode) r.getTree();
tree.setUnknownTokenBoundaries();
return tree;
}
我注意到下面这句
HiveLexerX lexer = new HiveLexerX(new ANTLRNoCaseStringStream(command));
这个new ANTLRNoCaseStringStream(command)是什么东西?
经查阅代码知,ANTLRNoCaseStringStream类继承自ANTLRStringStream类。
它实际上是一个辅助类,ANTLRStringStream是Lexer使用的输入流之一(基于字符串的输入流)。这里定义的辅助类的作用就是,在Lexer使用其LA()(lookahead)从流中查看字符的时候,将字符转换成大写字符,从而实现大小写不敏感的lexer。
在Hive.g定义的语法中,只要识别大写字母就可以了。然而,真正存放在$token.text中的值仍然是原始String中的值(这种值是lexer通过ANTLRStringStream的consume()方法获得的,这个方法没有在辅助类中override)。
那么,这个HiveLexerX又是什么?起什么作用呢?
经过分析和猜测,我认为HiveLexerX的主要作用可能是进行词法分析。什么说它的作用是词法分析呢?
我们知道,词法分析的作用是将语句中的关键字,特殊符号之类的有特殊含义的符号解释为一个token。
而在parse包下有如下几个文件:
HiveLexerX.g
HiveParser.g
SelectClauseParser.g
FromClauseParser.g
IdentifiersParser.g
这些文件大概率与HiveLexerX密切相关。
其中HiveLexerX.g 文件中,对HQL中的关键字进行了声明(见下图),即对词的声明。由此可见HiveLexerX可能主要负责词法分析。
事实上经过查阅一些资料得知,HiveLexerX类继承自生成的HiveLexer;HiveParserX继承自生成的HiveParser。HiveLexerX 和HiveParserX 是实际使用的词法/语法分析类,它们的唯一目的就是更好地显示、记录分析过程中的错误信息。由此印证了我的猜测。
有关词法和语法解析的内容主要由队友进行,在此处,程序将进行一系列复杂的解析过程,这里不再展开分析。
总的来说,ParseDriver负责组织、驱动整个词法、语法分析流程,并得到分析后的AST。该类维护了一个静态成员xlateMap,它将关键字的名字(以KW_开头的名字,与Hive.g中定义的相同)映射到关键字本身。xlateMap也包含内置操作符名字到操作符字符串的映射。该类的一个重要的函数parse(),也就是我们上面提到的那个方法,是分析流程的关键,它的输入是一个HQL String,输出是一个AST。
重点看倒数第三行:
ASTNode tree = (ASTNode) r.getTree();
经过上述词法分析、语法分析,终于此处生成了目标结果AST,并通过函数调用返回。这便是后续语义分析框架所需的输入了。
通过这两篇前序步骤的简要分析,我更加清楚的理解了从HQL语句到进入驱动器再进行编译阶段的语法词法分析的函数调用历程,这也有助于从全局角度更好理解Hive的编译流程。后面,我将进行我的主要工作之一:语义分析框架的代码分析。