技术预计阅读 17 分钟6668 字0 次浏览

Antlr4系列④:支持分支语句

比较运算符、逻辑运算符、if/else if/else、代码块block

目录

在上一篇文章中我们完善了语法,使其支持变量的定义和打印函数,本篇文章我们将继续完善语法,实现if / else if / else分支语句,并为此新增比较运算符和逻辑运算符,让我们的语言真正具备"判断"的能力。

一、语法设计

  1. 比较运算符

    参考主流语言的写法,设计六种比较运算符:> < >= <= == !=,运算符两侧支持数值、变量甚至一个完整的四则运算表达式,例如a + 1 > b * 2

  2. 逻辑运算符

    设计三种逻辑运算符:&&(与)、||(或)、!(非),用来组合或取反条件表达式,同时支持用小括号显式提升优先级。

  3. 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)* ;

为方便和上一篇文章中的语法进行对比,对于本次新增的部分使用注释进行了标注,接下来对本次新增的部分进行解释。

  1. 词法部分

    IFELSE等关键字词法都是固定字符串,必须放在ID词法之前定义,原因和之前BOOLPRINT一样:antlr4词法优先匹配写在前面的规则,如果把它们放在ID后面,ifelse这些字符串就会被当成普通的标识符(变量名)而不是关键字。

  2. 语法部分

    • 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,所以比较运算符两侧可以是常量、变量,也可以是一个完整的四则运算表达式。boolLiteralcondArgValue分别处理直接写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();
}

关于mainprog:细心的读者可能会注意到我们并没有重写visitMainvisitProg方法。这是因为mainprog两个语法都没有使用#定义别名,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命中"

第一段ifa > b为真且flag为真,&&的结果为真,命中第一个分支,打印"a大于b且flag为真";第二段ifa < b为假,!flag也为假(flag为true),两个条件都不命中,最终执行了else,打印"最后的else命中",符合预期。

五、遗留的问题

细心的读者可能已经发现了一个隐患:目前所有变量都保存在MyRuleSetVisitor里唯一的一个paramDefine这个Map中,这意味着在if代码块内部定义的变量,出了这个代码块在外面依然能访问到——这在大多数编程语言里是不允许的,正确的行为应该是代码块内定义的变量只在该代码块内可见。这个问题在只有if语句时还不明显,当下一篇文章引入循环语句后会更容易暴露出来,我们将在实现完循环语句后专门用一篇文章来解决作用域的问题。下一篇文章我们先继续完善语法,实现while循环语句。

花开空白

西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论