zoukankan      html  css  js  c++  java
  • antlr4笔记(转)

    转载: https://abcdabcd987.com/notes-on-antlr4/

    ANTLR4 是一个非常厉害的程序/库,可以用来生成 Lexer 和 Parser,而且生成的接口非常易用。昨天匆匆把 The Definitive ANTLR 4 Reference 扫了一遍,现在把一些常用的东西记下来。

    安装

    $ cd /usr/local/lib
    $ curl -O http://www.antlr.org/download/antlr-4.5-complete.jar
    
    $ vim ~/.zshrc # or vim ~/.bashrc
    export CLASSPATH=".:/usr/local/lib/antlr-4.5-complete.jar:$CLASSPATH"
    alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.5-complete.jar:$CLASSPATH" org.antlr.v4.Tool'
    alias grun='java org.antlr.v4.runtime.misc.TestRig'
    $ . ~/.zshrc # or restart the terminal
    

    语法

    最重要的一点是,官方已经提供了非常多的常用的语言的语法文件了,拿来看看可以学到很多,甚至可以删删改改直接拿来用: https://github.com/antlr/grammars-v4

    • grammar 名称和文件名要一致
    • Parser 规则(即 non-terminal)以小写字母开始
    • Lexer 规则(即 terminal)以大写字母开始
    • 所有的 Lexer 规则无论写在哪里都会被重排到 Parser 规则之后
    • 所有规则中若有冲突,先出现的规则优先匹配
    • 用 'string' 单引号引出字符串
    • | 用于分隔两个产生式,(a|b) 括号用于指定子产生式,?+*用法同正则表达式
    • 在产生式后面 # label 可以给某条产生式命名,在生成的代码中即可根据标签分辨不同产生式
    • 不需要指定开始符号
    • 规则以分号终结
    • /* block comment */ 以及 // line comment
    • 默认的左结合,可以用 <assoc=right> 指定右结合
    • 可以处理直接的左递归,不能处理间接的左递归
    • 如果用 MUL: '*'; 指定了某个字符串的名字,在程序里面就能用这个名字了
    • 用 fragment 可以给 Lexer 规则中的公共部分命名

    例子:

    stmt:   expr NEWLINE                    # printExpr
        |   ID '=' expr NEWLINE             # assign
        |   NEWLINE                         # blank
        ;
    
    expr:   <assoc=right> expr op='^' expr  # pow
        |   expr op=('*'|'/') expr          # mulDiv
        |   expr op=('+'|'-') expr          # addSub
        |   INT                             # int
        |   ID                              # id
        |   '(' expr ')'                    # parens
    
    MUL : '*';
    DIV : '/';
    ADD : '+';
    SUB : '-';
    ID  : Letter LetterOrDigit*
    fragment Letter: [a-zA-Z_]
    fragment Digit: [0-9]
    fragment LetterOrDigit: Letter | Digit
    NEWLINE: '
    '? '
    '
    WS  : [ 	]+ -> skip
    

    Reference:

    • Chapter 4
      • 1 Matching an Arithmetic Expression Language
      • 2 Building a Calculator Using a Visitor
    • Chapter 5
      • 4 Dealing with Precedence, Left Recursion, and Associativity
      • 5 Recognizing Common Lexical Structures

    常见 Lexer 规则

    //------ Puncuation
    call : ID '(' exprList ')' ;
    // or define token labels
    call : ID LP exprList RP ;
    LP   : '(';
    RP   : ')';
    
    //------ Keywords
    returnStmt : 'return' expr ';' ;
    
    //------ Identifiers
    ID : ID_LETTER (ID_LETTER | DIGIT)* ;
    fragment ID_LETTER : 'a'..'z' | 'A'..'Z' | '_' ;
    fragment DIGIT : '0'..'9';
    
    //------ Numbers
    INT   : DIGIT+ ;
    FLOAT : DIGIT+ '.' DIGIT*
          | '.' DIGIT+
          ;
    
    //------ Strings
    STRING : '"' (ESC | .)*? '"' ;
    fragment ESC : '\' [btnr"\] ;  // , 	, 
    , ...
    
    //------ Comments
    LINE_COMMENT  : '//' .*? '
    ' -> skip;
    BLOCK_COMMENT : '/*' .*? '*/' -> skip;
    
    //------ Whitespace
    WS : [ 	
    
    ]+ -> skip
    

    Reference:

    • Chapter 5
      • 5 Recognizing Common Lexical Structures

    整合到自己的程序中

    ANTLR 4 提供了 Visitor 和 Listener 两种模式,通过这两种模式可以很轻松地把 Parser 的结果做各种处理。ANTLR 4 默认会生成 Listener 模式,如果不需要要加上 -no-listener,如果要生成 Visitor 模式要加上 -visitor

    $ antlr4 -visitor Calc.g4
    $ ls
    Calc.g4               CalcBaseVisitor.java  CalcListener.java
    Calc.tokens           CalcLexer.java        CalcParser.java
    CalcBaseListener.java CalcLexer.tokens      CalcVisitor.java
    

    运行 ANTLR 4 会生成以下文件:

    • <Grammar>Lexer.java: Lexer
    • <Grammar>Parser.java: Parser
    • <Grammar>Listener.java: Listener 接口
    • <Grammar>BaseListener.java: Listener 默认实现
    • <Grammar>Visitor.java: Visitor 接口
    • <Grammar>BaseVisitor.java: Visitor 默认实现
    • <Grammar>[Lexer].tokens: 当语法被拆分成多个多个文件时用于同步编号

    使用方法就是把 *.java 复制到项目中合适的位置,然后编写调用代码、Visitor及(或)Listener。

    调用代码

    import org.antlr.v4.runtime.*;
    import org.antlr.v4.runtime.tree.*;
    import java.io.*;
    
    public class Calc {
        public static void main(String[] args) throws IOException {
            InputStream is = new FileInputStream("example/1.txt"); // or System.in;
            ANTLRInputStream input = new ANTLRInputStream(is);
            CalcLexer lexer = new CalcLexer(input);
            CommonTokenStream tokens = new CommonTokenStream(lexer);
            CalcParser parser = new CalcParser(tokens);
            ParseTree tree = parser.calc(); // calc is the starting rule
    
            System.out.println("LISP:");
            System.out.println(tree.toStringTree(parser));
            System.out.println();
    
            System.out.println("Visitor:");
            EvalVisitor evalByVisitor = new EvalVisitor();
            evalByVisitor.visit(tree);
            System.out.println();
    
            System.out.println("Listener:");
            ParseTreeWalker walker = new ParseTreeWalker();
            Evaluator evalByListener = new Evaluator();
            walker.walk(evalByListener, tree);
        }
    }
    
    

    可以看到使用方法就是把输入流包装一下喂给 Lexer,之后将 Token 流喂给 Parser,最后调用 ParseTree::<starting> 生成解析树。

    解析树可以直接用 .toStringTree 按照 LISP 风格打印出来。

    使用 Visitor 模式的话,就是新建 Visitor 对象,之后 visit(tree)

    使用 Listener 模式的话,需要一个 ParseTreeWalker 和一个 Listener 对象,然后用这个 walker 在树上用这个 Listener 行走。

    不论是 Visitor 模式还是 Listener 模式,解决的痛点都是把结构和行为分开,真的十分佩服这些设计模式的创造者。下面简单讲下这两个模式。

    Visitor 模式

    假设有一个复杂的结构,其中有个基类 B ,以及很多的派生类 Derived1Derived2, …。然后我们现在有一些动作 Action1Action2, …。

    用 Visitor 模式的话,首先要在每个基类中指定一个 accept 函数来接受访客,接下来每个派生类重载这个函数,让传进来的访客访问自己。

    另外一方面,规定 IVisitor 接口,里面对每个不同类型的派生类 Derived 都有分别的 void visit(Derived obj); 函数。每一个 Visitor 都要实现这个接口。

    在对某个派生类对象obj执行某个动作visitor时,用 obj.accept(visitor);

    具体可以看下面这个例子。由基类 Shape 派生出了 Rectangle 和 Circle。我们分别想要求每种图形的周长和面积,于是编写了 PerimeterVisitor 和 AreaVisitor 两个 Visitor。注意调用的方式,是让派生类接受访问者,再让访问者访问自己。

    import java.util.*//------ Interfaces
    interface IShapeVisitor {
        void visit(Rectangle r);
        void visit(Circle c);
    }
    
    abstract class Shape {
        public abstract void accept(IShapeVisitor visitor);
    }
    
    //------ Shapes
    class Rectangle extends Shape {
        public double height;
        public double width;
        Rectangle(double height, double width) { this.height = height; this.width = width; }
    
        @Override
        public void accept(IShapeVisitor visitor) { visitor.visit(this); }
    }
    
    class Circle extends Shape {
        public double radius;
        Circle(double radius) { this.radius = radius; }
    
        @Override
        public void accept(IShapeVisitor visitor) { visitor.visit(this); }
    }
    
    //------ Visitors
    class PerimeterVisitor implements IShapeVisitor {
        @Override
        public void visit(Rectangle r) {
            System.out.println((r.height + r.width) * 2);
        }
    
        @Override
        public void visit(Circle c) {
            System.out.println(2 * Math.PI * c.radius);
        }
    }
    
    class AreaVisitor implements IShapeVisitor {
        @Override
        public void visit(Rectangle r) {
            System.out.println(r.height * r.width);
        }
    
        @Override
        public void visit(Circle c) {
            System.out.println(Math.PI * Math.pow(c.radius, 2.));
        }
    }
    
    //------ Test
    public class VisitorTest {
        public static void main(String[] args) {
            List<Shape> shapes = new ArrayList<>();
            shapes.add(new Rectangle(3, 4));
            shapes.add(new Circle(1));
    
            PerimeterVisitor perimeterVisitor = new PerimeterVisitor();
            shapes.forEach(x -> x.accept(perimeterVisitor));
    
            AreaVisitor areaVisitor = new AreaVisitor();
            shapes.forEach(x -> x.accept(areaVisitor));
        }
    }
    
    

    在上面的例子里面,我们也可以在基类里面加上一个 .getArea() 而不使用 Visitor 模式。那么用 Visitor 的好处是什么呢?就是前面说到的把结构和行为分离。

    假设我现在要多增加一个行为 Action ,我不需要改动我的结构,也就是不用在每个派生类里面多重载一个 .getAction()。不改动结构有什么好处呢?第一, Java 中每个 public 类都需要独立成一个文件,如果要在每个类里面都加上这么个行为,那么就需要分别打开一个个文件,与此同时这个行为的代码也被拆散到了一个个文件中,这无疑是非常不利于维护的。第二,有些情况下,我们对结构代码没有控制权,这个时候我们就不能往里面加代码了。

    要增加一个行为,我需要做的只是增加一个 Visitor,在这个 Visitor 里面实现所有类的对应的行为即可。程序的其余部分完全不需要管。

    Listener 模式

    Listener 模式对于 Javascript 用户来说应该是非常熟悉的。简单地说,某段程序定义了一系列的事件,我们可以编写当某些事件发生时做什么的回调函数,也就是 Listener,并且绑定到这些事件上。那么这段程序触发了这些事件的时候,就会调用我们的回调函数。

    一个很简单的例子就是在做树的遍历的时候,遍历程序提供 enterNode 和 exitNode 事件,我们就可以编写当进入节点和退出节点时要处理的事情。

    使用 ANTLR 4 中的 Visitor 模式

    下面以一个计算器为例子,语法如下:

    grammar Calc;
    
    calc: stmt*;
    
    stmt:   expr NEWLINE             # printExpr
        |   ID '=' expr NEWLINE      # assign
        |   NEWLINE                  # blank
        ;
    
    expr:   expr op=('*'|'/') expr   # mulDiv
        |   expr op=('+'|'-') expr   # addSub
        |   NUMBER                   # literal
        |   ID                       # id
        |   '(' expr ')'             # paren
        ;
    
    
    MUL : '*';
    DIV : '/';
    ADD : '+';
    SUB : '-';
    
    ID      : [a-zA-Z_]+ ;
    NUMBER  : DIGIT+
            | DIGIT+ '.' DIGIT*
            | '.' DIGIT+
            ;
    fragment DIGIT : [0-9];
    NEWLINE : '
    '? '
    ';
    WS      : [ 	]+ -> skip;
    

    我们实现一个求值的 Visitor。

    import java.util.HashMap;
    import java.util.Map;
    
    public class EvalVisitor extends CalcBaseVisitor<Double> {
        public Map<String, Double> vars = new HashMap<>();
    
        // stmt : ID '=' expr NEWLINE ;
        @Override
        public Double visitAssign(CalcParser.AssignContext ctx) {
            String id = ctx.ID().getText();
            Double val = visit(ctx.expr());
            vars.put(id, val);
            return val;
        }
    
        // stmt : expr NEWLINE ;
        @Override
        public Double visitPrintExpr(CalcParser.PrintExprContext ctx) {
            Double value = visit(ctx.expr());
            System.out.println(value);
            return .0;
        }
    
        // expr : INT ;
        @Override
        public Double visitLiteral(CalcParser.LiteralContext ctx) {
            return Double.valueOf(ctx.NUMBER().getText());
        }
    
        // expr : ID ;
        @Override
        public Double visitId(CalcParser.IdContext ctx) {
            String id = ctx.ID().getText();
            if (vars.containsKey(id)) return vars.get(id);
            return .0;
        }
    
        // expr : expr op=('*'|'/') expr ;
        @Override
        public Double visitMulDiv(CalcParser.MulDivContext ctx) {
            double lhs = visit(ctx.expr(0));
            double rhs = visit(ctx.expr(1));
            if (ctx.op.getType() == CalcParser.MUL) return lhs * rhs;
            return lhs / rhs;
        }
    
        // expr : expr op=('+'|'-') expr ;
        @Override
        public Double visitAddSub(CalcParser.AddSubContext ctx) {
            double lhs = visit(ctx.expr(0));
            double rhs = visit(ctx.expr(1));
            if (ctx.op.getType() == CalcParser.ADD) return lhs + rhs;
            return lhs - rhs;
        }
    
        // expr : '(' expr ')' ;
        @Override
        public Double visitParen(CalcParser.ParenContext ctx) {
            return visit(ctx.expr());
        }
    }
    
    

    通过上面的例子,可以看到, ANTLR 4 为每个产生式生成了对应的 visit 函数,并且有各自不同的 Context 对象 ctx。要访问子树需要使用 visit(ctx.<sublabel>());

    • ctx.<nonterminal>() 可以访问语法规则中的 <nonterminal> 部分的 Context
    • ctx.getText() 可以获得在原文中的串

    想知道 Context 对象里面有什么?当然,你可以看 <Grammar>Parser.java 里面写的。但是,如果你有一个带智能提示的 IDE 的话,那就非常舒服了!

    使用 ANTLR 4 中的 Listener 模式

    ANTLR 4 会为产生式生成

    public void enter<Label>(CalcParser.<Label>Context ctx);
    public void exit<Label>(CalcParser.<Label>Context ctx);
    

    这样的事件,类似 Visitor 模式按需填空即可。

    传递参数与返回值

    细心的读者应该注意到了,ANTLR 4 生成的 Visitor 模式中返回类型是统一的,而 Listener 模式直接就是 void ,并且两个模式都没有提供传入参数的地方。那么如果想要手动操纵返回值和参数怎么办呢?

    ANTLR 4 Runtime 提供了一个 ParseTreeProperty<T> ,其实大致就是个 IdentityHashMap。你可以把 Context 当作 key 把相关的东西丢进去。

    Listener 例子

    还是前面的计算器,演示下 Listener 模式以及 ParseTreeProperty 的用法。

    import org.antlr.v4.runtime.tree.ParseTreeProperty;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * Created by abcdabcd987 on 2016-03-23.
     */
    public class Evaluator extends CalcBaseListener {
        public Map<String, Double> vars = new HashMap<>();
        public ParseTreeProperty<Double> values = new ParseTreeProperty<>();
    
        // stmt : ID '=' expr NEWLINE ;
        @Override
        public void exitAssign(CalcParser.AssignContext ctx) {
            String id = ctx.ID().getText();
            Double val = values.get(ctx.expr());
            vars.put(id, val);
        }
    
        // stmt : expr NEWLINE ;
        @Override
        public void exitPrintExpr(CalcParser.PrintExprContext ctx) {
            System.out.println(values.get(ctx.expr()));
        }
    
        // expr : NUMBER ;
        @Override
        public void exitLiteral(CalcParser.LiteralContext ctx) {
            values.put(ctx, Double.valueOf(ctx.NUMBER().getText()));
        }
    
        // expr : ID ;
        @Override
        public void exitId(CalcParser.IdContext ctx) {
            values.put(ctx, vars.containsKey(ctx.ID().getText()) ? vars.get(ctx.ID().getText()) : .0);
        }
    
        // expr : expr op=('*'|'/') expr ;
        @Override
        public void exitMulDiv(CalcParser.MulDivContext ctx) {
            double lhs = values.get(ctx.expr(0));
            double rhs = values.get(ctx.expr(1));
            values.put(ctx, ctx.op.getType() == CalcParser.MUL ? lhs * rhs : lhs / rhs);
        }
    
        // expr : expr op=('+'|'-') expr ;
        @Override
        public void exitAddSub(CalcParser.AddSubContext ctx) {
            double lhs = values.get(ctx.expr(0));
            double rhs = values.get(ctx.expr(1));
            values.put(ctx, ctx.op.getType() == CalcParser.ADD ? lhs + rhs : lhs - rhs);
        }
    
        // expr : '(' expr ')' ;
        @Override
        public void exitParen(CalcParser.ParenContext ctx) {
            values.put(ctx, values.get(ctx.expr()));
        }
    }
    
    

    Listener 模式与 Visitor 模式的比较

    在 Visitor 模式中,树的遍历是需要我们自己手动控制的。这个有好处也有坏处。当你要实现一个树上的解释器的时候,用 Visitor 就很方便,比如你可以只执行 if-else 块中的一个,比如你可以重复执行循环语句的主体部分。当然坏处就是万一意外忘记遍历或者重复遍历就麻烦了。

    在 Listener 模式中, walker 自顾自地走着,按顺序恰好遍历每个节点一次,进入或者退出一个节点的时候调用你的 Listener。因此,如果要实现一个树上解释器的话, Listener 模式就非常蛋疼了。但是,如果想要构建一个 AST ,这种自动帮你一遍的事情就很舒服了。再比如要支持函数的后向调用,可以在第一次遍历中先把所有的函数名称找出来,然后再在第二遍遍历中做类型检查等等。

    添加 ANTLR 4 JAR 到 Intellij Idea 中

    http://stackoverflow.com/questions/21051991/importing-jar-file-into-intellij-idea

    后记

    ANTLR 4 还有很多高级用法,比如 Channel,比如内嵌代码等。前者我觉得用的比较少,所以就不研究了;后者我个人比较反对,我比较支持把语法文件和动作分离,所以也不写出来了。

    后续还有一篇关于ANTLR4实战的文章。

  • 相关阅读:
    C#多线程同步重新梳理
    word打不开,Microsoft Office Word 遇到问题需要关闭。。。总提示进入安全...
    SMTP协议
    【转载】Hadoop集群(第10期)_MySQL关系数据库 天高地厚
    备份与恢复的原理 . 天高地厚
    ubuntu server 使用parted分区 天高地厚
    Flex开发中遇到未整理资源 天高地厚
    Oracle RAC + Data Guard 环境搭建 . 天高地厚
    【转载】oracle事务之oracle读一致性 . 天高地厚
    理解PGA(2)pga_aggregate_target详解 . 天高地厚
  • 原文地址:https://www.cnblogs.com/beilong/p/13124659.html
Copyright © 2011-2022 走看看