从简单SQL转换理解Antlr4访问者模式技术细节
A Mini Task
从一个小任务开始。
给定简单CREATE TABLE的语法规则CreateLexer.g4和CreateParser.g4,采用Visitor模式实现下述语法的自动转换输出.
语法转换规则:实现CREATE TABLE语法datetime类型向TIMESTAMP WITHOUT TIME ZONE类型的转换.
示例1:
输入:CREATE TABLE t1 (id integer, order_date datetime);
输出:CREATE TABLE t1 (id integer, order_date TIMESTAMP WITHOUT TIME ZONE);
默认选择IDEA开发环境,且所涉及插件和配置均已就绪。
语法规则解读
一份语法由一个语法声明和紧随其后的若干条规则构成,具有如下通用形式:
/* 可选的Javadoc风格的注释 */
grammer Name;
options {...}
import ...; // 导入其他语法规则,与当前语法规则合并.
tokens {<<Token1>>, ..., <<TokenN>>} // 定义当前语法动作所需未出现的词法符号.
@actionName {...}
<<rule1>> // 可能混杂在一起的文法规则和词法规则
...
<<ruleN>>
grammer关键字声明语法名称,与文件名一致。
复杂语法可以将词法规则和文法规则拆分成两个不同的文件,两个规则文件满足如下特征:
/* 词法规则文件头声明 */
lexer grammer Name;
/* 文法规则文件头声明. 需要额外声明对应token流词典对应的词法规则. */
parser grammer Name;
options
{ tokenVocab = Name; }
每个文法规则的产生式后面可以通过 #label
给产生式分支加标签命名,当备选产生分支较多时,标签非常好用.
语法规则详细参考文档,请参考:
官方手册<<The Definitive ANTLR 4 Reference>> Chapter 15 Grammer Reference章节内容.
本任务中,CREATE TABLE语法的核心规则文法,如下,
stat
: createTable
;
createTable
: CREATE TABLE
tableName createDefinitions
;
createDefinitions
: LR_BRACKET createDefinition (COMMA createDefinition)* RR_BRACKET
;
createDefinition
: uid columnDefinition
;
columnDefinition
: dataType
;
dataType
: typeName=(
CHAR | VARCHAR | TINYTEXT | TEXT
)
lengthOneDimension? BINARY? #stringDataType
| typeName=(
TINYINT | SMALLINT | MEDIUMINT | INT | INTEGER | BIGINT
)
lengthOneDimension? UNSIGNED? #lengthOneDimensionDataType
[...]
| typeName=(
BIT | TIME | TIMESTAMP | DATETIME | BINARY
| VARBINARY | YEAR
)
lengthOneDimension? #lengthOneDimensionDataType
;
lengthOneDimension
: LR_BRACKET decimalLiteral RR_BRACKET
;
tableName
: uid
;
uid : ID;
语法解析树可视化
文法规则文件stat规则处,右键选择"Test rule stat",打开ANTLR Preview调试界面,输出示例建表语句,如下图。
右侧自动生成对应的Parse Tree,可以直观显示解析结构,其中,叶子节点为词法终结符,通常为SQL关键字或者变量标识符;非叶子节点为文法非终结符,通常为文法规则.
对应的层次结构,如下图.
如果Parse Tree有飘红,可能某个词法或文法规则有问题,需要进一步定位修正.
词法和文法规则.g4文件,配置只生成visitor模式接口类,一键式自动化生成所有基类文件.
Visitor模式主调流程
以赋值表达式 sp=100;
为例,下图(来源于官方手册文档)展示Antlr语法分析器根据字符串自动生成语法解析树的过程。Parse Tree的叶子节点是输入的词法符号,比如,关键字,变量标识符等等。句子,即是符号的线性组合,本质上是语法分析树在人脑中的串行化。
第1小节的Task,对应生成语法分析树的基本流程代码,如下。
// Step1. 新建CharStream,从标准输入读取数据
CharStream input = CharStreams.fromString("CREATE TABLE t1 (id integer, order_date datetime);");
// Step2. 新建词法分析器,处理输入的charStream[!]
CreateLexer lexer = new CreateLexer(input);
// Step3. 新建词法符号的缓冲区,用于存储词法分析器将生成的词法符号
CommonTokenStream tokens = new CommonTokenStream(lexer);
// Step4. 新建语法分析器,处理词法符号缓冲区的内容[!]
CreateParser parser = new CreateParser(tokens);
// Step5. 针对stat规则,开始语法分析.
ParseTree tree = parser.stat();
System.out.println();
// 此处tree已经记录语法解析树的基本信息,可以采用LISP风格打印生成的树.
System.out.println(tree.toStringTree(parser));
本任务语法解析树进行深度优先遍历的实际访问路径如下图所示。
实现中,通过将解析树根节点tree传入visit接口,即可达到遍历语法解析树的目的。当然,要真正达到应用项目特殊的转换目的,需要重写实现访问路径所到之处node类型对应的visit()
接口。
// 遍历语法分析树,触发visit函数的回调.
CreateTableVisitorImpl visitor = new CreateTableVisitorImpl();
System.out.println(visitor.visit(tree));
Visitor模式技术细节
针对第4小节中提到的访问者模式,本节将展开详细的介绍。
访问者模式是面向对象设计模式中一种经典的行为模式。将更新封装到一个类中(访问操作),并由待更改类提供一个接收接口,从而实现需求变更和扩展。
Visitor模式的关键是双分派(Double-Dispatch)的技术。其中,visit/accept操作是一个双分派操作,意味着执行操作取决于请求的种类和接收者的类型。通常,使用访问者模式,涉及一个对象层次结构,其中,所有节点都是从根节点基类类型派生出来的。每个基类都有一个accept函数接收访客,接下来,每个派生类重载此函数,使得访客可以访问自己。
在对某个派生类对象Obj执行visitor动作时,使用obj.accept(visitor)即可。Visitor模式的优势在于结构和行为分离。如果需要增加新行为,只需要增加一个新的Visitor,并实现所有类对应的行为即可。
Antlr语法解析器,正是借鉴访问者模式,自动生成visitor模式的一系列抽象类和接口函数。类及类间关系如下图所示。
CreateParserBaseVisitor
类继承抽象类 AbstractParseTreeVisitor
,是接口类 CreateParserVisitor
的具体实现。如下代码片段,根节点类型stat及其每个派生类类型均有对应的visit方法。
public class CreateParserBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements CreateParserVisitor<T> {
@Override public T visitStat(CreateParser.StatContext ctx) { return visitChildren(ctx); }
@Override public T visitCreateTable(CreateParser.CreateTableContext ctx) { return visitChildren(ctx); }
@Override public T visitCreateDefinitions(CreateParser.CreateDefinitionsContext ctx) { return visitChildren(ctx); }
@Override public T visitCreateDefinition(CreateParser.CreateDefinitionContext ctx) { return visitChildren(ctx); }
@Override public T visitColumnDefinition(CreateParser.ColumnDefinitionContext ctx) { return visitChildren(ctx); }
@Override public T visitStringDataType(CreateParser.StringDataTypeContext ctx) { return visitChildren(ctx); }
@Override public T visitLengthOneDimensionDataType(CreateParser.LengthOneDimensionDataTypeContext ctx) { return visitChildren(ctx); }
@Override public T visitTableName(CreateParser.TableNameContext ctx) { return visitChildren(ctx); }
@Override public T visitUid(CreateParser.UidContext ctx) { return visitChildren(ctx); }
}
前一小节visitor遍历实现中,参数传的是语法解析树的根节点tree,visitor.visit(tree)
调用的是抽象类接口
public abstract class AbstractParseTreeVisitor<T> implements ParseTreeVisitor<T> {
@Override
public T visit(ParseTree tree) {
return tree.accept(this);
}
}
根据根节点上下文类StatContext,调用其accept接口,并根据是否CreateParserVisitor类实例,调用不同的visit接口。
public static class StatContext extends ParserRuleContext {
@Override
public <T> T accept(ParseTreeVisitor<? extends T> visitor) {
if ( visitor instanceof CreateParserVisitor ) return ((CreateParserVisitor<? extends T>)visitor).visitStat(this);
else return visitor.visitChildren(this);
}
}
至此,实现方法很显而易见。开发只需要基于抽象类 CreateParserBaseVisitor
实现即可(如类图中红色框出部分 CreateParserBaseVisitor
),并按需重写抽象类生成的抽象接口。从访问路线看,需要依次重写实现遍历Parse Tree的过程所到之处的node对应的visit函数,即可达到转换的目的。
派生类重载实现visitor函数
在遍历语法解析树过程中,需要对转换涉及的节点进行改写。此次Task需要对建表语句数据类型作转换,将datetime类型转成TIMESTAMP WITHOUT TIME ZONE类型,datetime挂在lengthOneDimensionDataType标签的dataType分支中,所以,只要在关键函数visitLengthOneDimensionDataType
的重写中,实现datetime类型的改写输出即可。
// 对datetime类型自动转换处理.
if (ctx.typeName.getType() == CreateParser.DATETIME) {
return String.format("TIMESTAMP WITHOUT TIME ZONE");
} else {
return String.format("%s", ctx.typeName.getText());
}
总结
基于Antlr的开发应用,使用流程基本理解的基础上,知道需要继承哪个Visitor抽象类去实现以及怎么从各个派生类的Context去读取节点类型的更多信息,即可掌握语言识别程序的开发密码。
重难点主要在于如何从复杂的语法解析树中快速解析和准确构造新的字符串。
参考
[1]. The Definitive ANTLR 4 Reference
[2]. https://github.com/antlr
[3]. Antlr官方提供的.g4语法文件
- 点赞
- 收藏
- 关注作者
评论(0)