Antlr4系列③:支持变量定义和打印函数
number/string/boolean变量定义、print函数、VisitorResult统一返回类型
在上一篇文章中我们完善了四则运算使其支持连续混合运算,并解决了连续运算时包含乘除法的优先级问题,本篇文章我们将继续完善语法,使其支持变量的定义,并在四则混合运算中支持变量的使用。
一、语法设计
-
变量定义语法
在java语言中变量的定义是这样的
int a = 1;,可以看出变量的定义通常包含变量类型、变量名称以及变量值,参考java语言变量的定义方式,在我们的语法中变量的定义设计如下:- 支持三种变量类型
number、string、boolean,分别代表数值型、字符型和布尔型。 - 变量名称设计为:变量名称以大小写字母或下划线开头,可以包含数字。
number作为数值型的关键字,其定义的变量值可以为整型或者浮点型。string作为字符型的关键字,其定义的变量值为用双引号包裹的任意字符。boolean作为布尔型的关键字,其定义的变量值为true或false。(后续再支持逻辑运算)=作为赋值符号- 变量定义语句的结尾以分号结尾,分号可以省略。
- 支持三种变量类型
-
打印函数语法
为了方便验证计算结果,我们再设计一种打印函数,设计原则如下:
- 使用print关键字作为打印函数名称
- print后面使用小括号将要打印的信息进行包裹
- 支持打印字符串
- 支持打印boolean型
- 支持打印变量
-
四则运算语法完善
在四则运算语法中引入变量,使其支持使用变量进行计算。
二、语法定义
根据以上设计原则对之前的语法定义进行完善,完善后的结果如下:
grammar RuleSet; //程序名称和.g4名称一致即可
prog: main+ // 新增
;
main: setArg SEMI? // 新增
| print SEMI? // 新增
;
setArg :'number' id = ID '=' calcu #numarg // 新增
|'string' id = ID '=' STRING #chararg // 新增
|'boolean' id = ID '=' BOOL #boolarg // 新增
;
calcu: calcu opt=(MUL|DIV) calcu # mulAndDiv
| calcu opt=(ADD|SUB) calcu # addAndSub
| '(' calcu ')' # parens
| NUMBER # number
| ID # argValue // 新增
;
print: PRINT '(' value = ID ')' # printArg // 新增
| PRINT '(' value = BOOL ')' # printBoolean // 新增
| PRINT '(' value = STRING ')' # printString // 新增
;
WS : [ \t\n\r]+ -> skip ; // ->skip表示antlr4在分析语言的文本时,符合这个规则的词法将被无视
ADD : '+' ;
SUB : '-' ;
MUL : '*' ;
DIV : '/' ;
NUMBER : '-'? [0-9]+('.'([0-9]+)?)? ; // 数字正则
//以下为本次新增词法
BOOL : 'true' | 'false' ;
STRING : '"' .*? '"';
PRINT : 'print';
SEMI: ';';
fragment ID_LETTER : [a-z] | [A-Z] | '_' ;
fragment DIGIT : [0-9] ;
ID : ID_LETTER (ID_LETTER | DIGIT)* ;
为方便和上一篇文章中的语法进行对比,对于本次新增的部分使用注释进行了标注,接下来对本次新增的部分进行解释。
-
词法部分
对于词法部分主要是一些正则表达式的定义,需要理解的地方如下:
?: 表示匹配或忽略*: 表示匹配0次或者多次+: 表示匹配1次或者多次fragment: 表示一个词法片段,可以被词法引用,但是不能在语法中使用,如DIGIT可以在词法ID中使用,但是不能在calcu语法中使用,而未被fragment标识的ID是可以在calcu语法中使用的。
-
语法部分
各个语法的解释如下:
-
setArg语法表示变量定义语法,其包含三种规则,分别用来定义number、string、boolean三种类型的变量,其中的词法ID是变量的名称=作为赋值符号将右边的值赋给变量,string、boolean的变量值都是固定的词法,number的值为calcu语法,而calcu语法的规则中包含了NUMBER词法,因此number变量定义时等号右侧可以是数值也可以是四则运算。 -
calcu语法中新增了一个ID词法,因为ID词法是作为变量定义时的变量的名称的,所以calcu就支持了使用变量进行计算。 -
print有三种规则,语法的括号内按照上文的设计原则分别匹配了变量、字符串和布尔型。 -
main增加此语法是因为print和setArg之间不存在引用关系,是两种独立的语法,是两个独立的语法树,而在antlr4中想要将所有代码完整执行就只能存在一棵语法树,因此需要一个新的语法将两种语法关联到一起为最终形成一棵语法树做好准备�
-
H���:+�y��y.+yc깧"xXZ[�9. 9��z+�y��{�#9/a��+�g*XZ[�9�9d#�gh��"x可表示匹配一个或多个,因此当代码中同时出现setArg和print时可以多次匹配,最终形成一棵完整的语法树。
对于`main`和`prog`两种语法可能相对比较难理解,简单的来说如果没有`prog`和`mian`时`setArg`和`calcu`只能独立使用,同时出现在代码中时语法检查无法通过。如果没有`prog`只有`mian`时,根据上一篇文章所讲的语法规则优先级,`setArg`和`calcu`同时出现在代码中时,写在后面的代码会被忽略掉,无法完整执行。
## 三、语法实现
接下来使用idea插件将定义好的.g4文件生成对应的java文件,使用新生成的java文件覆盖之前的文件,至此我们需要思考一个问题之前的文章中`MyRuleSetVisitor`继承`RuleSetBaseVisitor`后因为四则运算的返回值为数值型将泛型指定为了Double,在本次新增的语法中打印函数和变量定义均无返回值类型,该如何兼容这种情况呢?这就需要再定义一种类型,来兼容各种返回值类型,方便后续返回不同类型的值。
定义一个`VisitorResult`类如下:
/**
* visitor访问语法树每个节点的返回结果
* 返回结果包含 三种类型 数值型(全部转为double)、字符串、布尔型,每次只可能返回三种类型中的一种,其他类型将抛出运行时异常
*
* @author dongxingli
* createTime 2023/5/11
* @version V1.0
*/
public class VisitorResult {
private Object value;
private VisitorResult() {
}
private VisitorResult(Object value) {
if (value instanceof String
|| value instanceof Double
|| value instanceof Boolean
|| value == null) {
this.value = value;
} else {
throw new RuntimeException("不支持的参数类型" + value);
}
}
public Boolean getBool() {
if (isBoolean()) {
return (Boolean) value;
}
throw new RuntimeException("非Boolean类型");
}
public Double getNumber() {
if (isNumber()) {
return (Double) value;
}
throw new RuntimeException("非Number类型");
}
public String getString() {
if (isVarchar()) {
return (String) value;
}
throw new RuntimeException("非String类型");
}
public static VisitorResult value(Object value) {
return new VisitorResult(value);
}
/**
* 返回NULL
*
* @return 结果类
*/
public static VisitorResult nil() {
return new VisitorResult();
}
/**
* 返回结果是否数值型
*
* @return 是或否
*/
public boolean isNumber() {
return this.value instanceof Number;
}
/**
* 返回结果是否字符串
*
* @return 是或否
*/
public boolean isVarchar() {
return this.value instanceof String;
}
/**
* 返回结果是否布尔型
*
* @return 是或否
*/
public boolean isBoolean() {
return this.value instanceof Boolean;
}
/**
* 返回结果是否为null
*
* @return 是或否
*/
public boolean isNull() {
return value == null;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
if (isNumber()) {
return Double.toString((Double) value);
} else if (isVarchar()) {
return (String) value;
} else if (isBoolean()) {
return Boolean.toString((Boolean) value);
} else {
throw new RuntimeException("未知类型");
}
}
}
接下来使用VisitorResult类作为RuleSetBaseVisitor的泛型对之前的四则运算代码进行修改,并实现赋值与打印函数的功能,修改后的代码如下:
public class MyRuleSetVisitor extends RuleSetBaseVisitor<VisitorResult> { //修改泛型
/**
* 语法中定义的参数,key为变量名称
*/
private final Map<String, Object> paramDefine = new HashMap<>(); //新增
@Override
public VisitorResult visitAddAndSub(RuleSetParser.AddAndSubContext ctx) {
// 符号左边的calcu语法,
// 它可能是一组带括号或不带括号甲减乘除运算,也可能是一个数字
// 直接visit即可访问它的处理方法拿到最终返回结果
VisitorResult visit = visit(ctx.calcu(0)); //修改
double left = visit.getNumber(); //修改
// 符号右边的calcu语法
VisitorResult visit2 = visit(ctx.calcu(1)); //修改
double right = visit2.getNumber(); //修改
//通过别名获取opt类型判断是加还是减
if (ctx.opt.getType() == RuleSetLexer.ADD) {
return VisitorResult.value(left + right); //修改
} else {
return VisitorResult.value(left - right); //修改
}
}
@Override
public VisitorResult visitMulAndDiv(RuleSetParser.MulAndDivContext ctx) {
VisitorResult visit = visit(ctx.calcu(0)); //修改
double left = visit.getNumber(); //修改
VisitorResult visit2 = visit(ctx.calcu(1)); //修改
double right = visit2.getNumber(); //修改
if (ctx.opt.getType() == RuleSetLexer.MUL) {
return VisitorResult.value(left * right); //修改
} else {
return VisitorResult.value(left / right); //修改
}
}
@Override
public VisitorResult visitNumber(RuleSetParser.NumberContext ctx) {
// NUMBER词法直接转为double
return VisitorResult.value(Double.parseDouble(ctx.getText())); //修改
}
// ---------------------------以下函数为本次新增----------------------------
/**
*定义数值类型变量,调用calcu进行计算完成后将变量值保存到map中
*/
@Override
public VisitorResult visitNumarg(RuleSetParser.NumargContext ctx) {
VisitorResult visit = visit(ctx.calcu());
paramDefine.put(ctx.id.getText(), visit.getNumber());
return VisitorResult.nil();
}
/**
*定义字符型变量,获取到字符串值后将变量值保存到map中
*/
@Override
public VisitorResult visitChararg(RuleSetParser.CharargContext ctx) {
paramDefine.put(ctx.id.getText(), ctx.STRING().getText());
return VisitorResult.nil();
}
/**
*定义布尔型变量,获取到布尔值后将变量值保存到map中
*/
@Override
public VisitorResult visitBoolarg(RuleSetParser.BoolargContext ctx) {
paramDefine.put(ctx.id.getText(), ctx.BOOL().getText());
return VisitorResult.nil();
}
/**
*获取所定义的变量值,根据变量名称从保存变量值的map中查找
*/
@Override
public VisitorResult visitArgValue(RuleSetParser.ArgValueContext ctx) {
Object value = paramDefine.get(ctx.getText());
if (value instanceof Double) {
return VisitorResult.value(value);
}
throw new RuntimeException("未定义的参数:" + ctx.getText() + " 或参数" + ctx.getText() + "定义为非数值型");
}
/**
*打印变量值,根据变量名称从保存变量值的map中查找后使用java打印方法进行打印
*/
@Override
public VisitorResult visitPrintArg(RuleSetParser.PrintArgContext ctx) {
String argName = ctx.value.getText();
Object value = paramDefine.get(argName);
if (value == null) {
throw new RuntimeException("未定义的变量:" + argName);
}
System.out.println(value);
return super.visitPrintArg(ctx);
}
/**
*打印变量值,获取到布尔变量值后使用java打印方法进行打印
*/
@Override
public VisitorResult visitPrintBoolean(RuleSetParser.PrintBooleanContext ctx) {
System.out.println(ctx.value.getText());
return VisitorResult.nil();
}
/**
*打印变量值,获取到字符串后使用java打印方法进行打印
*/
@Override
public VisitorResult visitPrintString(RuleSetParser.PrintStringContext ctx) {
System.out.println(ctx.value.getText());
return VisitorResult.nil();
}
}
接下来编写测试代码,如下
public class Test {
public static void main(String[] args) {
String expression =
"number a = 1 \n" +
"number b = 2 \n" +
"number c = a + b * 3 \n" +
"string d = \"字符串\" \n" +
"boolean e = true \n" +
"print(c) \n" +
"print(d) \n" +
"print(e)";
calcute(expression);
}
private static void calcute(String expression) {
System.out.println("执行:");
System.out.println(expression);
System.out.println();
MyRuleSetVisitor visitor = new MyRuleSetVisitor();
RuleSetLexer lexer = new RuleSetLexer(CharStreams.fromString(expression));
lexer.removeErrorListeners();
LexicalErrorListener lexicalListener = new LexicalErrorListener();
//添加词法错误监听
lexer.addErrorListener(lexicalListener);
RuleSetParser parser = new RuleSetParser(new CommonTokenStream(lexer));
parser.removeErrorListeners();
GrammarErrorListener errorListener = new GrammarErrorListener();
//添加语法法错误监听
parser.addErrorListener(errorListener);
RuleSetParser.ProgContext calcu = parser.prog(); //此处由calcu()改为prog()
List<String> lexicalError = lexicalListener.getErrMessage();
List<String> grammarError = errorListener.getErrMessage();
if (!lexicalError.isEmpty()) {
System.out.println(lexicalError);
return;
}
if (!grammarError.isEmpty()) {
System.out.println(grammarError);
return;
}
VisitorResult result = visitor.visit(calcu);
}
}
执行结果如下:
> Task :Test.main()
执行:
number a = 1
number b = 2
number c = a + b * 3
string d = "字符串"
boolean e = true
print(c)
print(d)
print(e)
7.0
"字符串"
true
根据以上执行结果可以看出,语法已经支持变量的定义及使用且计算结果正确,同时打印函数也可以正常使用,下一章节将继续完善语法,使其支持分支语句。