solidity-antlr4

 

ANTLR4 筆記

ANTLR4 是一個非常厲害的程序/庫,可以用來生成 Lexer 和 Parser,而且生成的接口非常易用。

安裝

$ 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: '\r'? '\n'
WS  : [ \t]+ -> skip

 常見 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"\\] ;  // \b, \t, \n, ...

//------ Comments
LINE_COMMENT  : '//' .*? '\n' -> skip;
BLOCK_COMMENT : '/*' .*? '*/' -> skip;

//------ Whitespace
WS : [ \t\n\r]+ -> skip

 

整合到自己的程序中

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 : '\r'? '\n';
WS      : [ \t]+ -> 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

參考:https://abcdabcd987.com/notes-on-antlr4/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章