2019-12-04
最近科研项目需要加入一些新东西,需要我构建一个程序信息库,然后就想设计一套类似sql的查询语言去查询库里包含的内容,因此就需要用到语法分析器生成工具了。之前编译原理课上用过flex+bison那一套,做毕设的时候也用过JavaCC,这次开始动手之前在github上搜了一下“sqlParser”的类似Java项目,发现很多项目都是使用ANTLR4来生成语法分析器的(如下图),于是我这次就尝试使用它来进行实现。
ANTLR 是 ANother Tool for Language Recognition 的缩写,官网:http://www.antlr.org/
它是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。
而且开发过程也比较简单,一般开发流程如下:
.g4
语法文件;ANTLR4可以在多种环境中运行,下面主要介绍在IDEA里如何使用ANTLR4:
首先,安装ANTLR4插件
之后,创建一个maven项目,在pom.xml中导入依赖,其中版本最好选择最新版本
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.7.2</version>
</dependency>
接着,在项目中创建g4语法文件
最后,点击Configure ANTLR添加生成配置
设置生成文件的目录和package名
点击Generate ANTLR Recongnizer后,即可生成语法分析器
按照Antlr4规范编写特定语言的语法规则文件(绝大部分语言的都已提供,详见语法库);
这里是官方提供的一个简单计算器的例子:
grammar Calc;
// parser
prog: stmt;
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
名称和文件名要一致'string'
单引号引出字符串|
用于分隔两个产生式,(a|b)
括号用于指定子产生式,?+*
用法同正则表达式# label
可以给某条产生式命名,在生成的代码中即可根据标签分辨不同产生式/* block comment */
以及 // line comment
<assoc=right>
指定右结合MUL: '*';
指定了某个字符串的名字,在程序里面就能用这个名字了fragment
可以给 Lexer 规则中的公共部分命名创建一个main方法调用分析器
public class Main {
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()
可以检验输入的语句是否符合我们定义的语法,如果不符合会相应报错
在确定编写的语法文件没有问题之后,我们就可以遍历语法分析器构建的抽象语法书,对输入的语句进行语义的分析或计算
ANTLR4中提供了两种遍历模式:Listener和Visitor,他们的区别主要在:
先在上面的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());
}
最后附上我设计的查询语言的g4文件和遍历Listener文件
Query.g4:
grammar Query;
// parser
query : expr;
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);
}
}
}