antlr4 入門


antlr4

本文包括:

  • antlr4基本操作:下載、安裝、測試
  • Listener模式和Visitor模式比較
  • 通過增加操作修飾文法
  • antlr4 優先級、左遞歸及相關性
  • antlr4 實現的簡單計算器(java版)

基本操作

  1. 下載安裝antlr
sudo curl -O http://www.antlr.org/download/antlr-4.7-complete.jar
alias antlr4='java -jar /usr/local/lib/antlr-4.7-complete.jar'
alias grun='java org.antlr.v4.gui.TestRig'

設置antlr4和grun別名的兩句:直接寫在命令行,重啟就會被抹去,失去效果;推薦寫在用戶配置文件中(Mac OS 下 vi ~/.bash_profile,寫在文件末尾即可)

  1. 編寫一個文法,保存為hello.g4
grammar hello;

//tokens
s: 'hello' + ID;
ID: [a-z]+;
WS: [ \t\n\r]+ -> skip;
  1. 利用antlr生成這個文法的識別器(默認生成的識別器由java編寫)
//進入hello.g4所在文件夾后執行
antlr4 hello.g4

測試字符串是否屬於這個文法

在使用生成的識別器前要先編譯,因為我們生成的識別器是用默認的java語言編寫的,所以我們用javac來編譯:javac -g *.java

如果不編譯就使用這個識別器,會發生Can't load hello as lexer or parser的錯誤,原因顯而易見,不再贅述

以下方式都可以用來識別字符串是否能夠被這個文法識別,敲入下列命令,回車,輸入要識別的字符串,以 EOF(UNIX/Mac OS下Ctrl+D,windows下Ctrl+Z)結尾

grun [文法名] [標識符] [TestRig參數]
  • grun hello s -tokens

  • grun hello s -tree

  • grun hello s -gui

  • 上面列出了常見TestRig參數,其他參數可見antlr官方文檔

Listener & Visitor

  • Visitor和Listener是antlr提供的兩種樹遍歷機制。Listener是默認的機制,可以被antlr提供的遍歷器對象調用;如果要用Visitor機制,在生成識別器時就需要顯式說明 antlr4 -no-listener -visitor Calc.g4,並且必須顯示的調用visitor方法遍歷它們的子節點,在一個節點的子節點上如果忘記調用visit方法,就意味着那些子數沒有得到訪問

Listener

文件結構

hello.tokens
helloBaseListener.java
helloLexer.java
helloLexer.tokens
helloListener.java
helloParser.java

ParserTreeWalker類是ANTLR運行時提供的用於遍歷語法分析樹和觸發Listener中回調方法的樹遍歷器。ANTLR工具根據hello.g4文法自動生成ParserTreeListener接口的子接口helloListener和默認實現helloBaseListener,其中含有針對語法中每個規則的enter和exit方法。

helloListener是語法和Listener對象之間的關鍵接口 public interface helloListener extends ParserTreeListener

helloBaseListener是ANTLR生成的一組空的默認實現。ANTLR內建的樹遍歷器會去觸發在Listener中像enterProg()和exitProg()這樣的一串回調方法

Visitor

文件結構

執行antlr4 -no-listener -visiter hello.g4后,生成以下文件

hello.tokens
helloBaseVisitor.java
helloLexer.java
helloLexer.tokens
helloParser.java
helloVisitor.java

增加操作

@parser

grammer Rows;
@parser::members {
	int col;
	public RowsParser(TokenStream input, int col){
		this(input);
		this.col = col;
	}
}

file: (row NL)+;

row
locals [int i=0]
	: ( STUFF
		{
		$i++;
		if ($i == col) System.out.println($STUFF.text);
		}
	  )+
	;

TAB	: '\t' -> skip;
NL	: '\r'? '\n';
STUFF: ~[\t\r\n]+;
  • 操作時被花括號括起來的代碼段
  • 上例中members操作的代碼將會被注入到生成的語法分析器類中的成員區;
  • 規則row中的操作訪問\(i是由locals子句定義的局部變量,該操作也用\)STUFF.text獲取最近匹配的STUFF記號的文本內容
public class Rows{
	public static void main(String[] args) throws Exception {
		ANTLRInputStream input = new ANTLRInputStream(System.in);
		RowsLexer lexer = new RowsLexer(input);
		CommonTokenStream tokens = new CommonTokenStream(lexer);
		int col = Integer.valueOf(args[0]);
		RowsParser parser = new RowsParser(tokens, col);
		parser.setBuildParserTree(false);
		parser.file();
	}
}

語義謂詞

grammer IData;

file	: group+;
group	: INT sequence[$INT.int];

sequence[int n]
locals [int i = 1]
	: ({$i <= $n}? INT {$i++})*
	;

INT	:[0-9]+;
WS	:[ \t\n\r]+ -> skip;

被稱為語義謂詞的布爾值操作:{$i <= $n},當謂詞計算結果為true時,語法分析器匹配整數直到超過序列規則參數n要求的數量;當謂詞計算結果為false時,謂詞讓相關的選項從生成的語法分析器中“消失”。在這里,值為false的謂詞讓(...)*循環從規則序列里終止而退出。

注意:一定不能寫成以下的語法,語義偏差很大,因為\(INT總是返回最近匹配的INT,在下面的錯誤代碼中,匹配sequence時的第一個\)INT確實是sequence之前的INT,但之后都將變成上一個sequence操作中匹配的INT

grammer IData;

file	: group+;
group	: INT sequence;

sequence
locals [int i = 1]
	: ({$i <= $INT.int}? INT {$i++})*
	;

INT	:[0-9]+;
WS	:[ \t\n\r]+ -> skip;

詞法模型

同一文件的不同格式

基本思路:當詞法分析器看到特殊的哨兵字符序列時,讓它在不同模式之間切換

lexer grammar XMLLexer;

//默認模式
OPEN			:	'<'					-> pushMode(INSIDE);
COMMEND		:	'<!--' .* '-->'	-> skip;
EntityRef		:	'&' [a-z] ';';
TEXT			:	~('<'|'&')+;

//INSIDE模式
mode INSIDE;

CLOSE			:	'>'					-> popMode;
SLASH_CLOSE	:	'/>'				-> popMode;
EQUALS			:	'='	;
STRING			:	'"' .* '"';
SlashName		:	'/' Name;
Name			:	ALPHA (ALPHA|DIGIT)*;
S				:	[ \t\r\n]			-> skip;

fragment
ALPHA			:	[a-zA-Z];

fragment
DIGIT			:	[0-9];

重寫輸入流

import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.TokenStreamRewriter;

public class RewriteListener extends IDataBaseListener{
	TokenStreamRewrite rewriter;
	
	public RewriteListener(TokenStream tokens) {
		rewriter = new TokentreamRewriter(tokens);
	}
	
	@Override
	public void enterGroup(IDataParser.GroupContext ctx){
		rewriter.insertAfter(ctx.stop, '\n');
	}
}

以上代碼實現在捕獲到group的時候把換行符插到它末尾。

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
import java.io.FileInputStream;
import java.io.InputStream;

public class IData {
	InputStream is = args.length > 0 ? new FileInputStream(args[0]):System.in;
	
	ANTLRInputStream input = new ANTLRInputStream(is);   //CharStream
	IDataLexer lexer = new IDataLexer(input);
	CommonTokenStream tokens = new CommonTokenStream(lexer);
	IDataParser parser = new IDataParser(tokens);
	ParserTree tree = parser.file();
	
	RewriterListener listener = new RewriteListener(tokens);
	
	System.out.println("Before Rewriting");
	System.out.println(listener.reewriter.getText());
	
	ParserTreeWalker walker = new ParserTreeWalker();
	walker.walk(listener, tree);
	
	System.out.println("After rewriting");
	System.out.println(listener.rewriter.getText());
}

發送記號到不同的通道

COMMENT
	: '/*' .*? '*/'		-> channel(HIDDEN)
	;

WS	: [ \r\n\t]+			-> channel(HIDDEN)
	;

優先級,左遞歸以及相關性

相關性

默認情況下,ANTLR從左到右結合運算符。使用assoc手動指定運算符記號上的相關性。

expr	: <assoc=right>expr '^' expr
		| INT
		;

//指數表達式選項放在其他表達式選項之前,因為它的運算符比乘法和加法都有更高的優先級
expr	: <assco=right>expr '^' expr
		| expr '*' expr
		| expr '+' expr
		| INT
		;

左遞歸

左遞歸規則是指直接或者間接調用在選項左邊緣的自身的規則。

ANTLR可以處理直接左遞歸,不能處理間接左遞歸

優先級

ANTLR詞法分析器通過偏愛首先指定的規則來解決詞法規則間的二義性,這意味着ID規則應該定義在所有的關鍵詞規則之后。

比如,乘除比加減優先級高,寫在加減的前面

expr: expr op=(MUL|DIV) expr    # md_expr
    | expr op=(ADD|SUB) expr    # as_expr
    | sign=(ADD|SUB)? NUM       # number
    | ID                        # id
    | '(' expr ')'              # parens
    ;

ANTLR把隱式的為字面量生成的詞法規則放在顯式的詞法規則之前,因此它們總是具有更高的優先級。

簡單計算器案例

  • 用 visitor 模式實現的簡單計算器
  • 實現支持加、減、乘、除、取余、冪運算的浮點數計算器,其中規定取余只能對有整數意義的對象操作,優先級及結合律的實現無誤
  • 支持print語句、行注釋、塊注釋
grammar Calc;

prog: stat+;

stat: expr ';'                      # cal_stat
    | ID '=' expr ';'               # assign
    | 'print' '(' expr ')' ';'      # print
    ;

expr: <assco=right>expr POW expr    # pow_expr
    | expr op=(MUL|DIV|MOD) expr    # md_expr
    | expr op=(ADD|SUB) expr        # as_expr
    | sign=(ADD|SUB)? NUM           # number
    | ID                            # id
    | '(' expr ')'                  # parens
    ;

NUM: INT
   | FLOAT
   ;

MUL: '*';
DIV: '/';
ADD: '+';
SUB: '-';
POW: '^';
MOD: '%';

ID: [a-zA-Z]+[0-9a-zA-Z]*;

ZERO: '0';

INT: [1-9][0-9]*
    | ZERO
    ;

FLOAT: INT '.' [0-9]+
    ;

COMMENT_LINE: '//' .*? '\r'? '\n' -> skip;
COMMENT_BLOCK: '/*' .*? '*/'     -> skip;

WS: [ \t\r\n] -> skip;

具體實現

  • 浮點型的比較用BigDecimal提高精確度
  • 浮點型格式化輸出用DecimalFormat實現
  • 對除零、浮點數取余做了錯誤檢查
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by wangqingyue on 2017/9/21.
 */
public class Calc extends CalcBaseVisitor<Double>{
    Map<String, Double> memory = new HashMap<String, Double>();

    // ID '=' expr ';'
    @Override
    public Double visitAssign(CalcParser.AssignContext ctx){
        String id = ctx.ID().getText();
        Double value = visit(ctx.expr());
        memory.put(id, value);
        return value;
    }

    // expr ';'
    @Override
    public Double visitCal_stat(CalcParser.Cal_statContext ctx){
        Double value = visit(ctx.expr());
        return value;
    }

    // 'print' '(' expr ')' ';'
    @Override
    public Double visitPrint(CalcParser.PrintContext ctx){
        Double value = visit(ctx.expr());
        DecimalFormat df = new DecimalFormat("#.###");
        System.out.println(df.format(value));
        return value;
    }

    // <assco=right>expr POW expr
    @Override
    public Double visitPow_expr(CalcParser.Pow_exprContext ctx){
        Double truth = visit(ctx.expr(0));
        Double power = visit(ctx.expr(1));
        return Math.pow(truth, power);
    }

    // expr op=(MUL|DIV) expr
    @Override
    public Double visitMd_expr(CalcParser.Md_exprContext ctx){
        Double left = visit(ctx.expr(0));
        Double right = visit(ctx.expr(1));
        if (ctx.op.getType() == CalcParser.MUL) return left * right;
        else if (ctx.op.getType() == CalcParser.DIV){
            if (new BigDecimal(right).compareTo(new BigDecimal(0.0)) == 0){
                return 0.0;
            }
            return left / right;
        }
        else {
            int left_int = (new Double(left)).intValue();
            int right_int = (new Double(right).intValue());
            if (new BigDecimal(right_int).compareTo(new BigDecimal(right)) == 0
                    && new BigDecimal(left_int).compareTo(new BigDecimal(left_int)) == 0){
                return Double.valueOf(left_int % right_int);
            }
            System.out.println("Mod\'%\' operations should be integer.");
            return 0.0;
        }
    }

    // expr op=(ADD|SUB) expr
    @Override
    public Double visitAs_expr(CalcParser.As_exprContext ctx){
        Double left = visit(ctx.expr(0));
        Double right = visit(ctx.expr(1));
        if (ctx.op.getType() == CalcParser.ADD) return left + right;
        else return left - right;
    }

    // sign=(ADD|SUB)? NUM
    @Override
    public Double visitNumber(CalcParser.NumberContext ctx){
        int child = ctx.getChildCount();
        Double value = Double.valueOf(ctx.NUM().getText());
        if (child == 2 && ctx.sign.getType() == CalcParser.SUB){
                return 0 - value;
        }
        return value;
    }

    // ID
    @Override
    public Double visitId(CalcParser.IdContext ctx){
        String id = ctx.ID().getText();
        if (memory.containsKey(id)) {
            return memory.get(id);
        }
        System.out.println("undefined identifier \"" + id + "\".");
        return 0.0;
    }

    //'(' expr ')'
    @Override
    public Double visitParens(CalcParser.ParensContext ctx){
        Double value = visit(ctx.expr());
        return value;
    }
}
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;

/**
 * Created by wangqingyue on 2017/9/21.
 */
public class Main {
    public static void main(String[] args) throws Exception{
        CharStream input = args.length > 0? CharStreams.fromFileName(args[0]): CharStreams.fromStream(System.in);

        CalcLexer lexer = new CalcLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        CalcParser parser = new CalcParser(tokens);
        ParseTree tree = parser.prog();

        Calc calculator = new Calc();
        calculator.visit(tree);
    }
}

update at 2017/9/22
by 一顆球


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM