添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

最近科研项目需要加入一些新东西,需要我构建一个程序信息库,然后就想设计一套类似sql的查询语言去查询库里包含的内容,因此就需要用到语法分析器生成工具了。之前编译原理课上用过flex+bison那一套,做毕设的时候也用过JavaCC,这次开始动手之前在github上搜了一下“sqlParser”的类似Java项目,发现很多项目都是使用ANTLR4来生成语法分析器的(如下图),于是我这次就尝试使用它来进行实现。 github

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);
    
  •