Antlr4: 为parser rule添加label

1. parser rule中的label

1.1 简介

  • Antrl4语法文件Calculator.g4,stat和expr两个parser rule含有多个rule element,我们这两个parse rule的每个rule element添加了Alternative labels(简称label)
  • 按照Antlr4的语法规则:
    • label一般位于每个rule element的行尾,以#开头。
    • 同时,要么为parser rule的每个rule element添加label,要么一个都不添加
      // 定义stat,不添加label
      stat: expr 
          | ID '=' expr 
          ;
      
      // 定义expre,每个rule element都添加label
      expr: expr op=(MUL|DIV) expr
          | expr op=(ADD|SUB) expr 
          | INT 
          | ID 
          | '(' expr ')' 
          ;
      

1.2 label对parse tree的影响

  • 使用IDEA的Antlr4插件,测试语法规则prog
    • 为stat的rule element添加label后,解析出的stat将使用label进行标识
    • 去除rule element的label后,只能使用序号进行标识
  • 可以说,使用label标识rule element,可以为语法解析带来意想不到的便利

1.3 label的作用

  • Antlr4官网是这样介绍label的:
    1. Labeling Rule Alternatives for Precise Event Methods, we can get more precise parse-tree listener events by labeling the outermost alternatives of a rule using the # operator.
    2. All alternatives within a rule must be labeled, or none of them. Here are two rules with labeled alternatives.
    3. Alternative labels do not have to be at the end of the line and there does not have to be a space after the # symbol. ANTLR generates a rule context class definition for each label.

总结起来如下:

  1. 添加label,可以为每个rule element生成对应的ParserRuleContext,从而快速访问各rule element

    • 由于没有给stat的rule element添加label,只能通过CalculatorParser.StatContext的getter方法获取rule element
      public static class StatContext extends ParserRuleContext {
              
              
      	// getter方法
      	public ExprContext expr() {
              
              
      		return getRuleContext(ExprContext.class,0);
      	}
      	public TerminalNode ID() {
              
               return getToken(CalculatorNoLabelParser.ID, 0); }
      	... // 其他代码省略
      }
      
    • 添加了label,将基于StatContext创建更加具体的Context,这有利于访问parse tree中的各节点
      public static class StatContext extends ParserRuleContext {
              
              
      	public StatContext(ParserRuleContext parent, int invokingState) {
              
              
      		super(parent, invokingState);
      	}
      	@Override public int getRuleIndex() {
              
               return RULE_stat; }
       
      	public StatContext() {
              
               }
      	public void copyFrom(StatContext ctx) {
              
              
      		super.copyFrom(ctx);
      	}
      }
      public static class AssignContext extends StatContext {
              
              
      	public TerminalNode ID() {
              
               return getToken(CalculatorParser.ID, 0); }
      	public ExprContext expr() {
              
              
      		return getRuleContext(ExprContext.class,0);
      	}
      	... // 其他代码省略
      }
      public static class PrintExprContext extends StatContext {
              
              
      	public ExprContext expr() {
              
              
      		return getRuleContext(ExprContext.class,0);
      	
      	... // 其他代码省略
      }
      
  2. 同时,CalculatorListener接口中的监听器方法也更加丰富,以便更加精确地监听parse tree中的节点访问事件

    // 未添加label,只能监听stat节点。具体是赋值节点还是打印节点,需要在代码中区分
    void enterStat(CalculatorNoLabelParser.StatContext ctx);
    void exitStat(CalculatorNoLabelParser.StatContext ctx);
    
    // 添加label后的监听器方法更加有针对性
    void enterPrintExpr(CalculatorParser.PrintExprContext ctx);
    void exitPrintExpr(CalculatorParser.PrintExprContext ctx);
    void enterAssign(CalculatorParser.AssignContext ctx);
    void exitAssign(CalculatorParser.AssignContext ctx);
    
  3. 自己的补充: CalculatorVisitor接口中的visitXxx()方法也将变得更加丰富,以便更加精确地访问rule element,从而访问parse tree中的各节点

    // 未给添加label,只能访问stat节点。具体是赋值节点还是打印节点,需要在代码中区分
    T visitStat(CalculatorNoLabelParser.StatContext ctx);
    
    // 添加label后,visitStat()方法被以下有针对性的方法替代
    T visitPrintExpr(CalculatorParser.PrintExprContext ctx);
    T visitAssign(CalculatorParser.AssignContext ctx);
    

2. 小建议

  • 建议: 为rule element都加上label,这样获取具体的节点将会更加方便

  • 以visitor模式实现计算器为例,若不为stat的rule element添加label,在CalculatorVisitorImpl中重写CalculatorVisitor.visitStat()方法时,需要自主判断节点的类型

    @Override
    public Integer visitStat(CalculatorNoLabelParser.StatContext ctx) {
          
          
        if (ctx.ID() != null) {
          
           // 存在ID说明是赋值语句
            String variable = ctx.ID().getText();
            Integer value = visit(ctx.expr());
            variables.put(variable, value);
        } else {
          
           // 否则是打印语句
            if (ctx.expr() instanceof CalculatorNoLabelParser.ConstantContext) {
          
          
                System.out.printf("%d\n", visit(ctx.expr()));
            } else {
          
          
                System.out.printf("%s = %d\n", ctx.expr().getText(), visit(ctx.expr()));
            }
        }
        return 0; // 打印语句统一返回0
    }
    
  • 如果定义了label,无需自主判断节点的类型,可以直接访问具体的节点

    @Override
    public Integer visitPrintExpr(CalculatorParser.PrintExprContext ctx) {
          
          
        Integer result = visit(ctx.expr());
        if (ctx.expr() instanceof CalculatorParser.ConstantContext) {
          
          
            System.out.println("打印常量的值: " +result);
        } else {
          
          
            System.out.printf("打印计算结果: %s = %d\n", ctx.expr().getText(), result);
        }
        return result;
    }
    
    @Override
    public Integer visitAssign(CalculatorParser.AssignContext ctx) {
          
          
        String variable = ctx.ID().getText();
        Integer value = visit(ctx.expr());
        variables.put(variable, value);
        return value;
    }
    

猜你喜欢

转载自blog.csdn.net/u014454538/article/details/129349625