最近科研项目需要加入一些新东西,需要我构建一个程序信息库,然后就想设计一套类似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插件
-
之后,创建一个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后,即可生成语法分析器
-
生成后文件目录
-
编写语法规则g4文件
按照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
名称和文件名要一致 - Parser 规则(即 non-terminal)以小写字母开始
- Lexer 规则(即 terminal)以大写字母开始
- 所有的 Lexer 规则无论写在哪里都会被重排到 Parser 规则之后
- 所有规则中若有冲突,先出现的规则优先匹配
- 用
'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,他们的区别主要在:
- 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()); }
-
之后,就可以根据需求遍历语法树,实现自己想要的需求。
-
最后附上我设计的查询语言的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); } } }
-
最后可以测试如下语句:
select class,method, package from [aa],[bb] where [app].method.name = "yyf" and [app].class.name = "www";
-
利用IDEA插件可以看到解析的语法树:
-