Antlr4系列④:支持分支语句
比较运算符、逻辑运算符、if/else if/else、代码块block
在上一篇文章中我们完善了语法,使其支持变量的定义和打印函数,本篇文章我们将继续完善语法,实现if / else if / else分支语句,并为此新增比较运算符和逻辑运算符,让我们的语言真正具备"判断"的能力。
一、语法设计
-
比较运算符
参考主流语言的写法,设计六种比较运算符:
> < >= <= == !=,运算符两侧支持数值、变量甚至一个完整的四则运算表达式,例如a + 1 > b * 2。 -
逻辑运算符
设计三种逻辑运算符:
&&(与)、||(或)、!(非),用来组合或取反条件表达式,同时支持用小括号显式提升优先级。 -
if语句
参考java语言if语句的写法设计如下:
- 使用
if (条件) { 语句 }作为基本结构。 - 支持任意个
else if (条件) { 语句 }。 - 支持一个可选的
else { 语句 },必须放在最后。 { }包裹的语句块内可以是之前已经支持的变量定义、打印函数,也可以是if语句本身,即支持嵌套。
- 使用
二、语法定义
根据以上设计原则对之前的语法定义进行完善,完善后的结果如下:
grammar RuleSet; //程序名称和.g4名称一致即可
prog: main+
;
main: setArg SEMI?
| print SEMI?
| ifStmt // 新增
;
// 新增:代码块,用花括号包裹0个或多个main,支持if语句内部嵌套if
block: '{' main* '}' // 新增
;
// 新增:if / else if / else,else if可以出现0次或多次,else可选
ifStmt: IF '(' cond ')' block
(ELSE IF '(' cond ')' block)*
(ELSE block)?
; // 新增
// 新增:条件表达式,支持比较运算、逻辑运算以及布尔字面量/布尔变量
cond: NOT cond # notCond
| cond AND cond # andCond
| cond OR cond # orCond
| calcu opt=(GT|LT|GE|LE|EQ|NE) calcu # compare
| '(' cond ')' # condParens
| BOOL # boolLiteral
| ID # condArgValue
; // 新增
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: ';';
//以下为本次新增词法
IF : 'if' ;
ELSE : 'else' ;
AND : '&&' ;
OR : '||' ;
NOT : '!' ;
GT : '>' ;
LT : '<' ;
GE : '>=' ;
LE : '<=' ;
EQ : '==' ;
NE : '!=' ;
fragment ID_LETTER : [a-z] | [A-Z] | '_' ;
fragment DIGIT : [0-9] ;
ID : ID_LETTER (ID_LETTER | DIGIT)* ;
为方便和上一篇文章中的语法进行对比,对于本次新增的部分使用注释进行了标注,接下来对本次新增的部分进行解释。
-
词法部分
IF、ELSE等关键字词法都是固定字符串,必须放在ID词法之前定义,原因和之前BOOL、PRINT一样:antlr4词法优先匹配写在前面的规则,如果把它们放在ID后面,if、else这些字符串就会被当成普通的标识符(变量名)而不是关键字。 -
语法部分
-
block语法表示一个代码块,内容就是复用已有的main语法,用*表示可以包含0条或多条语句。这里没有重新设计一套语句集合,而是直接复用main,是因为代码块内部理应能写和最外层一样的语句,包括后续文章会讲到的循环语句,复用可以让新语句自动获得"可以写在代码块里"的能力,不需要每次新增语句都去修改block。 -
ifStmt语法中,(ELSE IF '(' cond ')' block)*表示else if可以出现0次或多次,(ELSE block)?表示最后可以有一个可选的else。这里有一个关键特点:生成的IfStmtContext中,cond()方法返回的是一个List<CondContext>,个数等于1(if本身)加上else if的个数;block()方法返回的也是一个List<BlockContext>,如果最后跟了单独的else,那么block的个数会比cond多一个。这个"数量差"就是后面实现时判断是否命中最终else的依据。 -
cond语法使用了和calcu一样的左递归写法。NOT写在最前面表示优先级最高,其次是AND,最后是OR,这和calcu语法中乘除优先于加减是同样的道理——写在上面的规则优先级更高。compare两侧使用calcu,所以比较运算符两侧可以是常量、变量,也可以是一个完整的四则运算表达式。boolLiteral和condArgValue分别处理直接写true/false字面量,或者写一个布尔型变量作为条件的情况。
-
三、语法实现
接下来使用idea插件将定义好的.g4文件重新生成java文件覆盖之前的文件。在开始实现ifStmt之前,需要先修正上一篇文章遗留的一个小问题:visitBoolarg当时保存的是BOOL词法的原始文本(字符串"true"/"false"),仅仅够打印用。而本篇的条件表达式需要拿到真正的Boolean类型参与逻辑运算,所以这里顺便修正为保存真正的Boolean:
/**
* 定义布尔型变量,获取到布尔值后将变量值保存到map中
* 这里存的是真正的Boolean而不是文本,是为了本篇新增的条件表达式中可以直接取出参与逻辑运算
*/
@Override
public VisitorResult visitBoolarg(RuleSetParser.BoolargContext ctx) {
paramDefine.put(ctx.id.getText(), Boolean.parseBoolean(ctx.BOOL().getText())); //修改
return VisitorResult.nil();
}
接着在MyRuleSetVisitor中新增本篇涉及到的各个方法:
// ---------------------------以下函数为本次新增----------------------------
/**
* 比较运算,比较符两侧都是calcu语法,先算出数值再根据操作符类型比较
*/
@Override
public VisitorResult visitCompare(RuleSetParser.CompareContext ctx) {
double left = visit(ctx.calcu(0)).getNumber();
double right = visit(ctx.calcu(1)).getNumber();
switch (ctx.opt.getType()) {
case RuleSetLexer.GT:
return VisitorResult.value(left > right);
case RuleSetLexer.LT:
return VisitorResult.value(left < right);
case RuleSetLexer.GE:
return VisitorResult.value(left >= right);
case RuleSetLexer.LE:
return VisitorResult.value(left <= right);
case RuleSetLexer.EQ:
return VisitorResult.value(left == right);
default:
return VisitorResult.value(left != right);
}
}
/**
* 逻辑非,取反cond的布尔结果
*/
@Override
public VisitorResult visitNotCond(RuleSetParser.NotCondContext ctx) {
boolean value = visit(ctx.cond()).getBool();
return VisitorResult.value(!value);
}
/**
* 逻辑与,两个cond都为真才为真
*/
@Override
public VisitorResult visitAndCond(RuleSetParser.AndCondContext ctx) {
boolean left = visit(ctx.cond(0)).getBool();
boolean right = visit(ctx.cond(1)).getBool();
return VisitorResult.value(left && right);
}
/**
* 逻辑或,两个cond有一个为真即为真
*/
@Override
public VisitorResult visitOrCond(RuleSetParser.OrCondContext ctx) {
boolean left = visit(ctx.cond(0)).getBool();
boolean right = visit(ctx.cond(1)).getBool();
return VisitorResult.value(left || right);
}
/**
* 带括号的条件表达式,直接访问括号内的cond
*/
@Override
public VisitorResult visitCondParens(RuleSetParser.CondParensContext ctx) {
return visit(ctx.cond());
}
/**
* 布尔字面量true/false直接转为Boolean
*/
@Override
public VisitorResult visitBoolLiteral(RuleSetParser.BoolLiteralContext ctx) {
return VisitorResult.value(Boolean.parseBoolean(ctx.getText()));
}
/**
* 条件表达式中使用布尔变量,根据变量名称从保存变量值的map中查找
*/
@Override
public VisitorResult visitCondArgValue(RuleSetParser.CondArgValueContext ctx) {
Object value = paramDefine.get(ctx.getText());
if (value instanceof Boolean) {
return VisitorResult.value(value);
}
throw new RuntimeException("未定义的参数:" + ctx.getText() + " 或参数" + ctx.getText() + "定义为非布尔型");
}
/**
* if/else if/else语句
* ctx.cond()按顺序依次是if的条件、每个else if的条件
* ctx.block()按顺序依次是if的代码块、每个else if的代码块,如果最后跟了单独的else,则block数量会比cond多一个
* 依次判断每个条件,命中哪个条件就执行对应的代码块,如果都不命中且存在多出来的最后一个block,则执行它
*/
@Override
public VisitorResult visitIfStmt(RuleSetParser.IfStmtContext ctx) {
List<RuleSetParser.CondContext> conds = ctx.cond();
List<RuleSetParser.BlockContext> blocks = ctx.block();
for (int i = 0; i < conds.size(); i++) {
if (visit(conds.get(i)).getBool()) {
return visit(blocks.get(i));
}
}
if (blocks.size() > conds.size()) {
return visit(blocks.get(blocks.size() - 1));
}
return VisitorResult.nil();
}
/**
* 代码块,依次执行块内的每一条main语句
*/
@Override
public VisitorResult visitBlock(RuleSetParser.BlockContext ctx) {
for (RuleSetParser.MainContext mainCtx : ctx.main()) {
visit(mainCtx);
}
return VisitorResult.nil();
}
关于
main和prog:细心的读者可能会注意到我们并没有重写visitMain和visitProg方法。这是因为main、prog两个语法都没有使用#定义别名,antlr4会生成默认的visitChildren实现,按顺序访问每个子节点,这一点在上一篇文章中就已经是这样,本篇的ifStmt作为main新增的一种子节点,同样会被默认实现自动访问到,不需要额外处理。
四、测试代码与执行结果
接下来编写测试代码,验证if / else if / else以及比较运算符、逻辑运算符是否都符合预期:
public class Test {
public static void main(String[] args) {
String expression =
"number a = 10 \n" +
"number b = 3 \n" +
"boolean flag = true \n" +
"if (a > b && flag) { \n" +
" print(\"a大于b且flag为真\") \n" +
"} else if (a == b) { \n" +
" print(\"a等于b\") \n" +
"} else { \n" +
" print(\"a不大于b\") \n" +
"} \n" +
"if (a < b) { \n" +
" print(\"不会执行\") \n" +
"} else if (!flag) { \n" +
" print(\"不会执行\") \n" +
"} else { \n" +
" print(\"最后的else命中\") \n" +
"}";
calcute(expression);
}
// calcute方法与上一篇文章保持一致,这里不再重复贴出
}
执行结果如下:
执行:
number a = 10
number b = 3
boolean flag = true
if (a > b && flag) {
print("a大于b且flag为真")
} else if (a == b) {
print("a等于b")
} else {
print("a不大于b")
}
if (a < b) {
print("不会执行")
} else if (!flag) {
print("不会执行")
} else {
print("最后的else命中")
}
"a大于b且flag为真"
"最后的else命中"
第一段if中a > b为真且flag为真,&&的结果为真,命中第一个分支,打印"a大于b且flag为真";第二段if中a < b为假,!flag也为假(flag为true),两个条件都不命中,最终执行了else,打印"最后的else命中",符合预期。
五、遗留的问题
细心的读者可能已经发现了一个隐患:目前所有变量都保存在MyRuleSetVisitor里唯一的一个paramDefine这个Map中,这意味着在if代码块内部定义的变量,出了这个代码块在外面依然能访问到——这在大多数编程语言里是不允许的,正确的行为应该是代码块内定义的变量只在该代码块内可见。这个问题在只有if语句时还不明显,当下一篇文章引入循环语句后会更容易暴露出来,我们将在实现完循环语句后专门用一篇文章来解决作用域的问题。下一篇文章我们先继续完善语法,实现while循环语句。