ANTLR4(三) 语义判定 词法符号特性

400 阅读3分钟

定制嵌入式语法

我们可以在语法规则的定义中,加上嵌入式的动作。

我们将从一个文本文件中,根据列号,取出该列每行的值。

预期效果

输入 文件t.rows:

parrt Terence parr 101 tombu Tom Burns 020 bke Kevin Edgar 008

输出

20200829133228715.png

语法文件

观察以下代码:

我们在该语法分析器中加入了新成员:col(也就是列号)、一个自定义的RowsParser(获取输入的文件及列号);

row规则中,我们引入了一个locals本地变量i,在STUFF读取完之前,i会++多次,并判断是否与col相同以决定是否要打印STUFF的内容。

//Rows.g4

grammar Rows;

@parser::members { // add members to generated RowsParser
    int col;
    public RowsParser(TokenStream input, int col) { // custom constructor
        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 ;   // match but don't pass to the parser
NL   :  '\r'? '\n' ;     // match and pass to the parser
STUFF:  ~[\t\r\n]+ ;     // match any chars except tab, newline

运行效果

观察以下代码:

注意自定义的RowsParser的实例获取了输入文件以及列号,并且关掉了语法分析树的构建

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;

import java.io.FileInputStream;
import java.io.InputStream;

public class Col {
    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); // pass column number!
        parser.setBuildParseTree(false); // don't waste time bulding a tree
        parser.file(); // parse
    }
}

java Col 1 < t.rows

java Col 2 < t.rows

java Col 3 < t.rows

得到结果

202008291342497.png

词法分析特性

接下来将介绍三个词法分析的特性。

通过模式切换处理相同文件中的不同格式

以xml为例,当xml文件看到<时,词法分析器会切换到标签内部模式;而看到>时,会切换到默认方式。

我们可以通过设置一个哨兵,让语法分析器进行这种模式的切换,来应对不同格式,进行处理。

语法文件

观察以下代码:

我们发现当检查的词法OPEN时,会跳转到模式INSIDE

而匹配到CLOSE或者SLASH_CLOSE时,会跳转出当前模式,回到默认模式。

//XMLLexer.g4
lexer grammar XMLLexer;

// Default "mode": Everything OUTSIDE of a tag
OPEN        :   '<'                 -> pushMode(INSIDE) ;
COMMENT     :   '<!--' .*? '-->'    -> skip ;
EntityRef   :   '&' [a-z]+ ';' ;
TEXT        :   ~('<'|'&')+ ;           // match any 16 bit char minus < and &

// ----------------- Everything INSIDE of a tag ---------------------
mode INSIDE;

CLOSE       :   '>'                 -> popMode ; // back to default mode
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] ;

运行词法分析器

输入 t.xml:

<tools>
	<tool name="ANTLR">A parser generator</tool>
</tools>

运行词法分析器

antlr4 XMLLexer.g4

javac XML*.java

grun XML tokens -tokens t.xml

结果
从左至右分别是 token序号 字符起始位置 字符终止位置 内容文本 规则序号 行号 行内位置。

20200829135523643.png

重写输入流

我们想实现这样一件事:读取输入,稍微修改以后输出。

这样的小动作我们不想通过Listener去更改每个方法,比如以下例子

输入

//Demo.java
import java.util.List;
import java.util.Map;
public class Demo {
	void f(int x, String y) { }
	int[ ] g(/*no args*/) { return null; }
	List<Map<String, Integer>>[] h() { return null; }
}

输出

20200829140556389.png

即:在进入classBody时,将这个serialVersionUID的常量字段打印出来。

重写Listener

实际上我们只需要重写enterClassBody就足够了。

首先我们创建一个重写器,并让这个rewriter获取tokens。

并在enterClassBody方法内嵌入这个rewriter,赋给它常量字段。

但注意,现在并没有触发重写

//InsertSerialIDListener.java
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.TokenStreamRewriter;

public class InsertSerialIDListener extends JavaBaseListener {
    TokenStreamRewriter rewriter;
    public InsertSerialIDListener(TokenStream tokens) {
        rewriter = new TokenStreamRewriter(tokens);
    }

    @Override
    public void enterClassBody(JavaParser.ClassBodyContext ctx) {
        String field = "\n\tpublic static final long serialVersionUID = 1L;";
        rewriter.insertAfter(ctx.start, field);
    }
}

运行结果

//InsertSerialID.java

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.*;

import java.io.FileInputStream;
import java.io.InputStream;

public class InsertSerialID {
    public static void main(String[] args) throws Exception {
        String inputFile = null;
        if ( args.length>0 ) inputFile = args[0];
        InputStream is = System.in;
        if ( inputFile!=null ) {
            is = new FileInputStream(inputFile);
        }
        ANTLRInputStream input = new ANTLRInputStream(is);

        JavaLexer lexer = new JavaLexer(input);
		CommonTokenStream tokens = new CommonTokenStream(lexer);
        JavaParser parser = new JavaParser(tokens);
        ParseTree tree = parser.compilationUnit(); // parse

        ParseTreeWalker walker = new ParseTreeWalker(); // create standard walker
        InsertSerialIDListener extractor = new InsertSerialIDListener(tokens);
        walker.walk(extractor, tree); // initiate walk of tree with listener

        // print back ALTERED stream
        System.out.println(extractor.rewriter.getText());
    }
}

注意最后一行我们才通过rewriter将常量字段打印出来,打印的位置就是rewriter嵌入的位置。

因此rewriter更像是一个随取随用的工具

将词法符号送入不同通道

在之前通过Listener模式识别Java的例子中,我们曾经保留了Java签名的空白字符和注释

但通常语法需要让我们忽略它们。如果我们通过->skip的方式忽略,那么它们直接被丢弃了,我们无法再获取它们进行处理。

这里我们通过改变channel的方式,让空白字符和注释进入隐藏的channel。即:虽然忽略,但是不丢弃

COMMENT
    :   '/*' .*? '*/'    -> channel(HIDDEN) // match anything between /* and */
    ;
WS  :   [ \r\t\u000C\n]+ -> channel(HIDDEN)
    ;

LINE_COMMENT
    : '//' ~[\r\n]* '\r'? '\n' -> channel(HIDDEN)
    ;