ANTLR4入门学习(三)

一、ANTLR快速指南

1.1 匹配算数表达式的语言

  1. 新建文档t.expr
193
a = 5
b = 6
a+b*2
(1+2)*3
  1. 创建g4语法文件 Expr.g4
grammar Expr;

/**
 * 起始规则,语法分析的起点
 */
prog : stat+ ;
stat : expr NEWLINE
 	  | ID '=' expr NEWLINE
 	  | NEWLINE
 	  ;

expr : expr ('*'|'/') expr
 	  | expr ('+'/'-') expr
 	  | INT
 	  | ID
 	  | '(' expr ')'
 	  ;
	
ID : [a-zA-Z]+ ; // 匹配标识符
INT : [0-9]+ ;	 // 匹配整数
NEWLINE : '\r' ? '\n' ; // 告诉语法分析器一个新行的开始(即语句终止标志)
WS : [ \t]+ -> skip ; // 丢弃空白字符

语法包含一系列描述语言结构的规则。这些规则既包含类似stat和expr的描述语法结构的规则,也包括描述标识符和整数之类的词汇符号(词法符号)的规则。

  • 语法分析器的规则以小写字母开头
  • 词法分析器的规则以大写字母开头
  • 使用|来分隔同一个语言规则的若干备选分支,使用圆括号把一些符号组合成子规则。例如,子规则(‘*’|‘/’)匹配一个乘法符号或者一个除法符号。
antlr4 Expr.g4
javac *.java
grun Expr prog -tree t.expr

可得出对应数据信息
在这里插入图片描述

1.2 java测试代码

public class ExprTest {
    public static void main(String[] args) throws Exception {
        String path = ExprTest.class.getResource("/cn/liulin/algorithm/antlr4/t.expr").getFile();
        ANTLRInputStream input = new ANTLRInputStream(new FileInputStream(path));
        ExprLexer lexer = new ExprLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        ExprParser parser = new ExprParser(tokens);
        ParseTree tree = parser.prog();
        System.out.println(tree.toStringTree(parser));
    }
}

1.3 增加通用模块

  1. 新建通用模块语法规则
vim CommonLexerRules.g4
输入以下内容
lexer grammar CommonLexerRules; // 注意区别,是"lexer grammar"

ID : [a-zA-Z]+ ; // 匹配标识符
INT : [0-9]+ ; // 匹配整数
NEWLINE : '\r' ? '\n' ; // 告诉语法分析器一个新行的开始(即语句终止标志)
WS : [ \T]+ -> skip ; //丢弃空白字符
  1. 引入通用模块
vim LibExpr.g4
输入以下内容
grammar LibExpr;
import CommonLexerRules; // 引入CommonLexerRules.g4中的全部规则
/**
 1. 起始规则,语法分析的七点
 */
prog : stat+ ;
stat : expr NEWLINE
     | ID '=' expr NEWLINE
     | NEWLINE
     ;
expr : expr ('*'|'/') expr
     | expr ('+'|'-') expr
     | INT
     | ID
     | '(' expr ')'
     ;

1.4 使用访问者模式实现计算器

  1. 使用访问者模式,给备选分支加上标签(这些标签可以是任意标识符,只要它们不与规则名冲突)。如果备选分支上面没有标签,ANTLR就只为每条规则生成一个方法。标签以#开头,放置在一个备选分支的右侧。
vim LabeledExpr.g4
输入以下内容
grammar LabeledExpr;

prog : stat+;
stat : expr NEWLINE # printExpr
	 | ID '=' expr NEWLINE # assign
	 | NEWLINE # blank
	 ;

expr : expr op=('*'|'/') expr # MulDiv
	 | expr op=('+'|'-') expr # AddSub
	 | INT # int
	 | ID # id
	 | '(' expr ')' # parents
	 ;

MUL : '*' ; // 为上述语法中使用的'*' 命名
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
  1. 运行编译命令
 antlr4 -no-listener -visitor LabeledExpr.g4
  1. 编写EvalVisitor继承LabeledExprBaseVisitor java文件
public class EvalVisitor extends LabeledExprBaseVisitor<Integer> {

    // 计算机的"内存",存放变量名和变量值的对应关系

    Map<String, Integer> memory = new HashMap<>();

    /**
     * expr NEWLINE
     */
    @Override
    public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
        // 计算expr子节点的值
        Integer value = visit(ctx.expr());
        // 打印结果
        System.out.println(value);
        // 上面已经直接打印出了结果,因此在这里返回一个假值即可
        return 0;
    }

    /**
     * ID '=' expr NEWLINE
     */
    @Override
    public Integer visitAssign(LabeledExprParser.AssignContext ctx) {
        // id 在 '=' 左侧
        String id = ctx.ID().getText();
        // 计算右侧表达式的值
        Integer value = visit(ctx.expr());
        memory.put(id, value);
        return value;
    }

    /**
     * expr ('*'|'/') expr
     */
    @Override
    public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {
        // 计算左侧子表达式的值
        Integer left = visit(ctx.expr(0));
        // 计算右侧子表达式的值
        Integer right = visit(ctx.expr(1));
        if (LabeledExprLexer.MUL == ctx.op.getType()) {
            return left * right;
        }
        return left / right;
    }

    /**
     * expr ('+'|'-') expr
     */
    @Override
    public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {
        // 计算左侧子表达式的值
        Integer left = visit(ctx.expr(0));
        // 计算右侧子表达式的值
        Integer right = visit(ctx.expr(1));
        if (LabeledExprLexer.ADD == ctx.op.getType()) {
            return left + right;
        }
        return left - right;
    }

    /**
     * ID
     */
    @Override
    public Integer visitId(LabeledExprParser.IdContext ctx) {
        String id = ctx.ID().getText();
        return memory.getOrDefault(id, 0);
    }

    /**
     * INT
     */
    @Override
    public Integer visitInt(LabeledExprParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }

    @Override
    public Integer visitParents(LabeledExprParser.ParentsContext ctx) {
        return visit(ctx.expr());
    }
}

1.5 实现带有清除内存的计算器功能

vim LabeledClear.g4
输入以下内容
grammar LabeledClear;

prog : stat+ ;

stat : expr NEWLINE  # printExpr
     | ID '=' expr NEWLINE  # assign
     | 'clear' ID NEWLINE # clearMemory
     | NEWLINE  # blank
     ;

expr : expr op=('*'|'/') expr  # MulDiv
     | expr op=('+'|'-') expr  # AddSub
     | ID  # id
     | INT  # int
     | '(' expr ')' # parents
     ;

MUL : '*';
DIV : '/';
ADD : '+';
SUB : '-';

ID : [a-zA-Z]+;
INT : [0-9]+;
NEWLINE : '\r' ? '\n';
WS : [ \t] -> skip;

public class ExprClearVisitor extends LabeledClearBaseVisitor<Integer> {

    Map<String, Integer> memory = new HashMap<>();

    @Override
    public Integer visitPrintExpr(LabeledClearParser.PrintExprContext ctx) {
        Integer value = visit(ctx.expr());
        System.out.println(value);
        return 0;
    }

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

    @Override
    public Integer visitClearMemory(LabeledClearParser.ClearMemoryContext ctx) {
        String id = ctx.ID().getText();
        Integer remove = memory.remove(id);
        System.out.printf("remove id : %s, value : %s", id, remove);
        System.out.println();
        return remove;
    }

    @Override
    public Integer visitMulDiv(LabeledClearParser.MulDivContext ctx) {
        Integer left = visit(ctx.expr(0));
        Integer right = visit(ctx.expr(1));
        if (ctx.op.getType() == LabeledClearParser.MUL) {
            return left * right;
        }
        return left / right;
    }

    @Override
    public Integer visitAddSub(LabeledClearParser.AddSubContext ctx) {
        Integer left = visit(ctx.expr(0));
        Integer right = visit(ctx.expr(1));
        if (ctx.op.getType() == LabeledClearParser.ADD) {
            return left + right;
        }
        return left - right;
    }

    @Override
    public Integer visitId(LabeledClearParser.IdContext ctx) {
        String id = ctx.ID().getText();
        return memory.getOrDefault(id, 0);
    }

    @Override
    public Integer visitInt(LabeledClearParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }

    @Override
    public Integer visitParents(LabeledClearParser.ParentsContext ctx) {
        return visit(ctx.expr());
    }
}
public class LabeledClearTest {
    public static void main(String[] args) throws Exception {
        ANTLRInputStream input = new ANTLRInputStream(System.in);
        LabeledClearLexer lexer = new LabeledClearLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        LabeledClearParser parser = new LabeledClearParser(tokens);
        ParseTree tree = parser.prog();
        ExprClearVisitor visitor = new ExprClearVisitor();
        visitor.visit(tree);
    }
}

1.6 使用监听器构建一个翻译程序(暂无)

访问器机制和监听器机制最大的区别在于,监听器的方法会被ANTLR提供的遍历器对象自动调用,而在访问器的方法中,必须显示调用visit方法来访问子节点。

二、定制语法分析过程

  1. 监视器和访问器机制使得自定义的程序代码和语法本身分离开来,让语法更具可读性,避免了将语法和特定的程序混杂在一起;
  2. 可以直接将代码片段(动作)嵌入语法中,这些动作将被拷贝到ANTLR自动生成的递归下降语法分析器的代码中。
  3. 语义判定(semantic predicate)动态地开启或者关闭部分语法,看到如何实现特殊的动作

2.1 在语法中嵌入任意动作

  1. 建立识别文件t.rows

在这里插入图片描述

  1. 建立语法文件,加入动作,传入希望提取的列号(从1开始计数)
vim Rows.g4
输入以下内容
grammar Rows;
@parser::members { // 在生成的RowsParser中添加一些成员
	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]+ ; // 匹配除tab符和换行符之外的任何字符
  1. STUFF词法规则匹配除tab符和换行符之外的任何字符,这意味着数据中可以包含空格。
  2. 动作就是花括号包未的一些代码片段。members动作可以将代码注入到生成的语法分析器类中,使之成为该类的成员,在row规则中动作访问了 i ,它是一个使用 l o c a l s 子句定义的局部变量。 r o w 规则也使用了 i,它是一个使用locals子句定义的局部变量。row规则也使用了 i,它是一个使用locals子句定义的局部变量。row规则也使用了STUFF.text来获得刚刚匹配的STUFF词法符号中包含的文本。
public class SemanticTest {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        int i = sc.nextInt();
        sc.close();
        String path = SemanticTest.class.getResource("/cn/liulin/algorithm/antlr4/t.rows").getFile();
        ANTLRInputStream input = new ANTLRInputStream(new FileInputStream(path));
        RowsLexer lexer = new RowsLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        // 传递列号作为参数
        RowsParser parser = new RowsParser(tokens, i);
        // 不需要浪费时间建立语法分析树
        parser.setBuildParseTree(false);
        // 开始语法分析
        parser.file();

    }
}

2.2 使用语义判定改变语法分析过程

  1. 创建数据源
    在这里插入图片描述
  2. 创建语法分析文件
vim Data.g4
输入以下内容
grammar Data;

file : group+ ;
group : INT sequence[$INT.int] ;
sequence[int n]
locals [int i = 1;]
	: ({$i<=$n} ? INT {$i++;})* // 匹配n个整数
	;

INT : [0-9]+ ; //匹配整数
WS : [ \t\r\n]+ -> skip ; //丢弃所有的空白字符

  1. {$i<=$n} ? 被称为一个语义判定,值为布尔类型,在匹配到n个输入整数之前保持为true,其中n是sequence语法中的参数,当语义判定的值为false时,对应的备选分支就从语法中“消失”,即代码终止。
  2. 运行命令
    在这里插入图片描述

2.3 神奇的语法分析特性

  1. 孤岛语法:处理相同文件中的不同格式
    例如一个XML解析器将除了标签和实体转义(例如£)之外的东西全部当作普通文本,当看到<时,词法分析器会切换到“标签内部”模式;当看到>或者/>时,它就切换回默认模式。
vim XMLLexer.g4
输入以下内容
lexer grammar XMLLexer;

// 默认的“模式”:所有在标签之外的东西
OPEN : '<' -> pushMode(INSIDE) ;
COMMENT : '<!--' .*? '-->' -> skip;
EntityRef : '&' [a-z]+ ';' ;
TEXT : ~('<'|'&')+ ; //匹配任意除<&之外的16位字符

// --------------------所有在标签之内的东西 ----------------------------
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] ;

测试XML

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

运行命令

grun XMLLexer tokens -tokens t,xml
  1. 重写输入流(暂无)
  2. 将词法符号送入不同通道(暂无)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值