ANTLR4入门学习(三)
一、ANTLR快速指南
1.1 匹配算数表达式的语言
- 新建文档t.expr
193
a = 5
b = 6
a+b*2
(1+2)*3
- 创建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 增加通用模块
- 新建通用模块语法规则
vim CommonLexerRules.g4
输入以下内容
lexer grammar CommonLexerRules; // 注意区别,是"lexer grammar"
ID : [a-zA-Z]+ ; // 匹配标识符
INT : [0-9]+ ; // 匹配整数
NEWLINE : '\r' ? '\n' ; // 告诉语法分析器一个新行的开始(即语句终止标志)
WS : [ \T]+ -> skip ; //丢弃空白字符
- 引入通用模块
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 使用访问者模式实现计算器
- 使用访问者模式,给备选分支加上标签(这些标签可以是任意标识符,只要它们不与规则名冲突)。如果备选分支上面没有标签,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 : '-' ;
- 运行编译命令
antlr4 -no-listener -visitor LabeledExpr.g4
- 编写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方法来访问子节点。
二、定制语法分析过程
- 监视器和访问器机制使得自定义的程序代码和语法本身分离开来,让语法更具可读性,避免了将语法和特定的程序混杂在一起;
- 可以直接将代码片段(动作)嵌入语法中,这些动作将被拷贝到ANTLR自动生成的递归下降语法分析器的代码中。
- 语义判定(semantic predicate)动态地开启或者关闭部分语法,看到如何实现特殊的动作
2.1 在语法中嵌入任意动作
- 建立识别文件t.rows
- 建立语法文件,加入动作,传入希望提取的列号(从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符和换行符之外的任何字符
- STUFF词法规则匹配除tab符和换行符之外的任何字符,这意味着数据中可以包含空格。
- 动作就是花括号包未的一些代码片段。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 使用语义判定改变语法分析过程
- 创建数据源
- 创建语法分析文件
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 ; //丢弃所有的空白字符
- {$i<=$n} ? 被称为一个语义判定,值为布尔类型,在匹配到n个输入整数之前保持为true,其中n是sequence语法中的参数,当语义判定的值为false时,对应的备选分支就从语法中“消失”,即代码终止。
- 运行命令
2.3 神奇的语法分析特性
- 孤岛语法:处理相同文件中的不同格式
例如一个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
- 重写输入流(暂无)
- 将词法符号送入不同通道(暂无)