2019-12-04
最近科研项目需要加入一些新东西,需要我构建一个程序信息库,然后就想设计一套类似sql的查询语言去查询库里包含的内容,因此就需要用到语法分析器生成工具了。之前编译原理课上用过flex+bison那一套,做毕设的时候也用过JavaCC,这次开始动手之前在github上搜了一下“sqlParser”的类似Java项目,发现很多项目都是使用ANTLR4来生成语法分析器的(如下图),于是我这次就尝试使用它来进行实现。
ANTLR4是什么?
ANTLR 是 ANother Tool for Language Recognition 的缩写,官网:
http://www.antlr.org/
它是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。
而且开发过程也比较简单,一般开发流程如下:
定义
.g4
语法文件;
使用 ANTLR 4 生成词法分析器(Lexer)和语法分析器(Parser)目标编程语言代码,支持的编程语言:Java、JavaScript、Python、C 和 C++ 等;
遍历 AST(Abstract Syntax Tree 抽象语法树),ANTLR 4 支持两种模式:访问者模式(Visitor)和监听器模式(Listener)。
ANTLR4的IDEA开发
ANTLR4可以在多种环境中运行,下面主要介绍在IDEA里如何使用ANTLR4:
首先,安装ANTLR4插件
stmt: expr # printExpr
| ID '=' expr # assign
| NEWLINE # blank
expr: <assoc=right> expr op='^' expr # pow
| expr op=('*'|'/') expr # mulDiv
| expr op=('+'|'-') expr # addSub
| NUM # int
| ID # id
| '(' expr ')' # parens
// lexer
MUL : '*';
DIV : '/';
ADD : '+';
SUB : '-';
ID : Letter LetterOrDigit*;
NUM : Number;
fragment Letter: [a-zA-Z_];
fragment Digit : ('0' .. '9') +;
fragment Number: ('0' .. '9') + ('.' ('0' .. '9') +)?;
fragment LetterOrDigit: Letter | Digit;
NEWLINE: '\r'? '\n';
WS : [ \t]+ -> skip;
语法规则和编译原理上描述的类似,一些主要的说明如下:
grammar
名称和文件名要一致
Parser 规则(即 non-terminal)以小写字母开始
Lexer 规则(即 terminal)以大写字母开始
所有的 Lexer 规则无论写在哪里都会被重排到 Parser 规则之后
所有规则中若有冲突,先出现的规则优先匹配
用
'string'
单引号引出字符串
|
用于分隔两个产生式,
(a|b)
括号用于指定子产生式,
?+*
用法同正则表达式
在产生式后面
# label
可以给某条产生式命名,在生成的代码中即可根据标签分辨不同产生式
不需要指定开始符号
规则以分号终结
/* block comment */
以及
// line comment
默认的左结合,可以用
<assoc=right>
指定右结合
可以处理直接的左递归,不能处理间接的左递归
如果用
MUL: '*';
指定了某个字符串的名字,在程序里面就能用这个名字了
用
fragment
可以给 Lexer 规则中的公共部分命名
public
static
void
run
(
String
expr
)
throws
Exception
{
//对每一个输入的字符串,构造一个 CodePointCharStream
CodePointCharStream
cpcs
=
CharStreams
.
fromString
(
expr
);
//用 cpcs 构造词法分析器 lexer,词法分析的作用是产生记号
AntlrTestLexer
lexer
=
new
AntlrTestLexer
(
cpcs
);
//用词法分析器 lexer 构造一个记号流 tokens
CommonTokenStream
tokens
=
new
CommonTokenStream
(
lexer
);
//再使用 tokens 构造语法分析器 parser,至此已经完成词法分析和语法分析的准备工作
AntlrTestParser
parser
=
new
AntlrTestParser
(
tokens
);
//最终调用语法分析器的规则 prog,完成对表达式的验证
ParseTree
tree
=
parser
.
prog
();
public
static
void
main
(
String
[]
args
)
throws
Exception
{
run
(
"a+b-3"
);
其中
parser.prog()
可以检验输入的语句是否符合我们定义的语法,如果不符合会相应报错
Listener模式会由ANTLR提供的walker对象自动调用,而Visitor模式则必须通过显式的访问调用遍历其子级,如果 忘记在节点的子节点上调用visit(),意味着这些子树不会被访问;
Listener模式不能返回值,而Visitor模式可以返回任何自定义类型。 因此,Listener模式就只能用一些变量来存储中间值,而Visitor可以直接返回计算值;
Listener模式使用分配在堆上的显式堆栈,而Visitor模式使用调用堆栈来管理树遍历,在深度嵌套的AST上使用访客时,这可能会导致StackOverFlow异常。
我目前只尝试了Listener的方法,实现如下:
先在上面的run()方法最后加上遍历代码
ParseTreeWalker walker = new ParseTreeWalker();
QueryListenerImp evalByListener = new QueryListenerImp();
walker.walk(evalByListener, tree);
然后实现一个QueryListenerImp继承自QueryBaseListener
public class QueryListenerImp extends QueryBaseListener {
然后充血其中的方法,例如下面代码,在访问Expr节点结束后,输出节点内容
@Override
public void exitExpr(QueryParser.ExprContext ctx) {
System.out.println(ctx.getText());
expr : SELECT result FROM repo (WHERE orCondition SEMI)?;
result : resultType (COMMA resultType)*;
resultType : 'class'
| 'method'
| 'package'
| 'dependenceGraph'
| 'controlFlowFraph'
| 'callGraph';
repo : app (COMMA app)*;
app : '[' ID ']';
// TODO 改为与或表达式
orCondition : andCondition (operator=OR andCondition)*
andCondition : conditionExpr (operator=AND conditionExpr)*
conditionExpr : conditionLeft op=('=' | '!=') conditionRight;
conditionLeft : app DOT type DOT attr;
conditionRight : STRING;
type: 'package'
| 'class'
| 'method';
attr : 'name'
| 'type'
| 'visibility'
| 'isAbstract';
// keyword
SELECT : 'select';
FROM : 'from';
WHERE : 'where';
AND : 'and';
OR : 'or';
NOT : 'not';
COMMA : ',';
SEMI : ';';
DOT : '.';
// lexer
ID : Letter LetterOrDigit*;
STRING
: '\'' ( ~('\''|'\\') | ('\\' .) )* '\''
| '"' ( ~('"'|'\\') | ('\\' .) )* '"'
fragment Letter: [a-zA-Z_];
fragment Digit : ('0' .. '9') +;
fragment LetterOrDigit: Letter | Digit;
WS : [ \t\n\r]+ -> skip ;
QueryListenerImp.java:
package modelQuery.parser;
import org.antlr.v4.runtime.tree.ParseTreeProperty;
import java.util.HashSet;
public class QueryListenerImp extends QueryBaseListener {
HashSet<String> resultSet = new HashSet<>();
HashSet<String> appSet = new HashSet<>();
StringBuilder condition = new StringBuilder();
boolean Sign = false;
public ParseTreeProperty<String> values = new ParseTreeProperty<>();
@Override
public void exitExpr(QueryParser.ExprContext ctx) {
if(Sign)
return;
else {
System.out.println("resultSet: " + resultSet);
System.out.println("appSet: " + appSet);
System.out.println("condition: " + values.get(ctx.orCondition()));
// 后面调用XQuery查询xml信息库中内容
@Override
public void exitResultType(QueryParser.ResultTypeContext ctx) {
resultSet.add(ctx.getText());
System.out.println("exitResultType: " + ctx.getText());
@Override
public void exitRepo(QueryParser.RepoContext ctx) {
for(QueryParser.AppContext appCtx : ctx.app()) {
appSet.add(appCtx.getText());
@Override
public void exitOrCondition(QueryParser.OrConditionContext ctx) {
if(ctx.operator != null){
values.put(ctx, values.get(ctx.andCondition(0)) + " or " + values.get(ctx.andCondition(1)));
} else {
values.put(ctx, values.get(ctx.andCondition(0)));
@Override
public void exitAndCondition(QueryParser.AndConditionContext ctx) {
if(ctx.operator != null) {
values.put(ctx, values.get(ctx.conditionExpr(0)) + " and " + values.get(ctx.conditionExpr(1)));
} else {
values.put(ctx, values.get(ctx.conditionExpr(0)));
@Override
public void exitConditionExpr(QueryParser.ConditionExprContext ctx) {
String left = values.get(ctx.conditionLeft());
values.put(ctx, left + ctx.op.getText() + ctx.conditionRight().getText());
@Override
public void exitConditionLeft(QueryParser.ConditionLeftContext ctx) {
String app = ctx.app().getText();
if(!appSet.contains(app)) {
System.err.println(ctx.start.getLine() + ":" + ctx.start.getStartIndex() + ", variable " + ctx.app().getText() + " not defined");
Sign = true;
return;
} else {
String type = ctx.type().getText();
String attr = ctx.attr().getText();
values.put(ctx, "$x/" + type + "/@" + attr);