ANTLR使⽤访问器遍历语法树
计算器
语法⽂件
实现⼀个简单的计算器,可以对如下表达式进⾏识别
193
a = 5
b = 6
a+b*2
(1+2)*3牙齿英文怎么读
如下为匹配规则的语法⽂件Expr.g4
追悼会悼词
grammar Expr;
/
** 起始规则,语法分析的起点 */
prog: stat+;
stat: expr NEWLINE //匹配expr表达式 + 换⾏
| 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 ;// 匹配空⽩,按->skip命令跳过
左递归规则:例如在语法规则expr: expr ('*'|'/') expr中,expr在备选分⽀的起始位置对⾃⾝进⾏了递归调⽤使⽤antlr运⾏编译,然后利⽤TestBig⼯具进⾏测试
D:\Code\antlr\demo\chapter4>antlr4 Expr.g4 # ⽣成语法、词法分析器
D:\Code\antlr\demo\chapter4>javac *.java # 编译相关⽂件
# 对语法Expr进⾏测试,初始规则prog,输⼊⽂件t.expr,并以可视化的⽅式输出结果
D:\Code\antlr\demo\chapter4>grun Expr prog -pr
⽣成的语法分析树如下所⽰
⽂件引⼊
在⼀个庞⼤的项⽬中,通常将语法⽂件拆分为语法规则⽂件和词法规则⽂件,这样有⼀些重复的词法规则就可以放在⼀个⽂件中以实现重⽤,当需要使⽤的时候再将⽂件引⼊。
如下所⽰,将所有词法规则放到⽂件CommonLexerRules.g4中
lexer grammar CommonLexerRules;// 词法⽂件以关键字"lexer grammar"开头
ID :[a-zA-Z]+;
INT :[0-9]+;
NEWLINE:'\r'?'\n';
WS :[ \t]+-> skip ;
然后在语法规则⽂件LibExpr.g4中引⼊所需的⽂件,之后我们只需要对LibExpr运⾏antlr构建⼯具即可,不需要再⼿动操作导⼊的⽂件
grammar LibExpr;
import CommonLexerRules;// 引⼊词法⽂件
prog: stat+;
stat: expr NEWLINE
| ID '=' expr NEWLINE
| NEWLINE
;
expr: expr ('*'|'/') expr
| expr ('+'|'-') expr
| INT
| ID
|'(' expr ')'
;
错误处理
ANTLR语法分析器能够⾃动识别语法报告中的错误并从错误中恢复。
如下所⽰,在输⼊中少了⼀个括号,语法分析树会输出提⽰信息,并且会继续向后匹配
D:\Code\antlr\demo\chapter4>grun LibExpr prog -tree
(1+2
3+4
^Z
line 1:4 mismatched input '\r\n' expecting {'*', '/', '+', '-', ')'}# 提⽰缺失
(prog (stat (expr ((expr (expr 1) + (expr 2))<missing ')'>) \r\n)(stat (expr (expr 3) + (expr 4)) \r\n))\ # 不影响继续匹配
如果使⽤gui的⽅式,会在确实的节点显⽰为红⾊
使⽤访问器遍历树
在构建了语法分析树后,使⽤访问器对节点进⾏遍历从⽽得出计算结果。访问器机制和监听器机制最⼤的区别在于,监听器⽅法会被遍历器⾃动调⽤,⽽访问器必须⼿动调⽤visit()⽅法实现对⼦节点的访问。
在使⽤访问器之前,需要对语法⽂件的每个分⽀添加标签,ANTLR会为每个标签⽣成相应的⽅法,否则只会默认为每个规则⽣成⼀个⽅法。如下所⽰为语法规则⽂件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 ')' # parens
;
//在语法⽂件中,为词法符号命名,这样在Java中就可以当作常量来访问了
MUL :'*';// 将 '*' 命名为MUL
儿童急走追黄蝶的下一句DIV :'/';
ADD :'+';
SUB :'-';
//词法规则
ID :[a-zA-Z]+;// match identifiers
INT :[0-9]+;// match integers
NEWLINE:'\r'?'\n';// return newlines to parr (is end-statement signal)
WS :[ \t]+-> skip ;
对上述语法⽂件运⾏ANTLR构建⼯具,通过命令参数-visitor指定⽣成包含访问器的代码
antlr4 -no-listener -visitor LabeledExpr.g4
⾃动⽣成访问器接⼝类LabeledExprVisitor,之前在语法⽂件中定义的标签都会⽣成对应的⽅法,传⼊相应的上下⽂作为参数,并且以泛型的⽅式定义接⼝类,我们可以根据需要⾃定义返回值类型。同时⽣成了接⼝的默认实现类LabeledExprBaVisitor
public interface LabeledExprVisitor<T>extends ParTreeVisitor<T>{
T visitProg(LabeledExprParr.ProgContext ctx);//访问Prog标签
T visitPrintExpr(LabeledExprParr.PrintExprContext ctx);//访问PrintExpr标签
T visitAssign(LabeledExprParr.AssignContext ctx);//访问Assign标签
......
}
public class LabeledExprBaVisitor<T>extends AbstractParTreeVisitor<T>implements LabeledExprVisitor<T>{
@Override public T visitProg(LabeledExprParr.ProgContext ctx){return visitChildren(ctx);}
@Override public T visitPrintExpr(LabeledExprParr.PrintExprContext ctx){return visitChildren(ctx);}
@Override public T visitAssign(LabeledExprParr.AssignContext ctx){return visitChildren(ctx);}
......
}
通过继承LabeledExprBaVisitor,实现⾃定义访问器类EvalVisitor ,在其中实现具体访问节点的代码,完成计算器的运算操作。注意在每个visitXxx()⽅法中都通过visit()⽅法⼿动对⼦节点进⾏访问
import java.util.HashMap;
import java.util.Map;
public class EvalVisitor extends LabeledExprBaVisitor<Integer>{
/** 计算器的“内存”,存放<;变量名, 变量值> */
Map<String, Integer> memory =new HashMap<String, Integer>();
/** ID '=' expr NEWLINE */
@Override
public Integer visitAssign(LabeledExprParr.AssignContext ctx){
String id = ctx.ID().getText();// 获取=左边的变量
int value =pr());// 计算右侧表达式的值
memory.put(id, value);// 将计算结果储存到“内存”中
return value;
}
/
** expr NEWLINE */
@Override
public Integer visitPrintExpr(LabeledExprParr.PrintExprContext ctx){
Integer value =pr());// 计算⼦节点的值
System.out.println(value);// 打印结果
return0;// 返回虚值
}
搭石教学反思
/** INT */
@Override
public Integer visitInt(LabeledExprParr.IntContext ctx){
return Integer.valueOf(ctx.INT().getText());
}
/** ID */
@Override
public Integer visitId(LabeledExprParr.IdContext ctx){
String id = ctx.ID().getText();
if( ainsKey(id))(id);
return0;
}
/** expr op=('*'|'/') expr */
@Override
public Integer visitMulDiv(LabeledExprParr.MulDivContext ctx){
int left =pr(0));// 递归计算左侧表达式的值
int right =pr(1));// 计算右侧表达式的值
非线性是什么意思
玛卡的功效与作用吃法if( Type()== LabeledExprParr.MUL )return left * right;//两值相乘return left / right;// 或者相除
}
/** expr op=('+'|'-') expr */
@Override
public Integer visitAddSub(LabeledExprParr.AddSubContext ctx){
int left =pr(0));// get value of left subexpression
int right =pr(1));// get value of right subexpression
if( Type()== LabeledExprParr.ADD )return left + right;
return left - right;// must be SUB
}
/** '(' expr ')' */
@Override
自贡景点
public Integer visitParens(LabeledExprParr.ParensContext ctx){
return pr());// 返回⼦表达式的值
}
}
最后实现⼀个主程序Calc.java对语法分析树进⾏遍历,计算结果