【问题标题】:Leveraging ANTLR 4's left recursion disambiguation利用 ANTLR 4 的左递归消歧
【发布时间】:2015-01-09 21:18:15
【问题描述】:

我想要一个仅包含二进制非终结符的语法和求值器(ANTLR parse tree walker),而无需在访问表达式节点时打开运算符来确定要执行的操作(例如 pre-左因子语法,因为访问者会访问一个“additionNode”,所以访问者可以静态假设它必须进行一个加法)。

相当直接的问题。 ANTLR 支持左递归,所以这是一个有效的语法

expr :
    | expr ('+'|'-') expr
    | expr ('*'|'/') expr
    | '(' expr ')'
    | literal
    ;

很不错,但是任何为此的 walker/visitor/compiler-back-end 现在都必须对类型进行自己的调度,这很糟糕:

onVisitExit(ExprContext ctx){
    left = compiledMap.get(ctx.getChild(0));
    right = compiledMap.get(ctx.getChild(2));
    operator = ctx.getChild(1);
    switch(operator.getToken()){
        case "+": compiledMap.put(ctx, left + right);
        case "-": compiledMap.put(ctx, left - right);
        case "*": compiledMap.put(ctx, left * right);
        case "/": compiledMap.put(ctx, left / right);
    }
}

这种策略的优缺点:

  • antlr 为我构建了一个二叉树,其中(二进制)每个规则都有一个左右参数,这意味着我不必担心用于 kleene 闭包的 while 循环。我真的很喜欢这个
  • 我必须手动调度(切换)令牌,而不是节点的类型。

使用更传统的左因子语法

expr                : addOrSub ;
addOrSub            : multOrDiv (('+'/'-') multOrDiv)* ;
multOrDiv           : bracks (('*'/'/') backs)* ;
bracks              : '(' expr ')' | literal ;
literal             : TOKEN ;

对此的相应访问者与上面的两种语法具有相反的优点和缺点:ANTLR 将为我执行此类型的分派——嗯,大多数情况下,仍然需要区分“+”和“-”——但是现在我必须为那些 kleene 闭包包含 while 循环,因为我不再有严格的二叉树,这很烦人。

我想我理想的语法应该是这样的

expression : expr ;

fragment expr : 
    (addition | subtraction) 
    | (multiplication | division)
    | brackets
    | literal
    ;

addition        : expr '+' expr ;
subtraction     : expr '-' expr ;
multiplication  : expr '*' expr ;
division        : expr '/' expr ;
brackets        : '(' expr ')' ;
literal         : TOKEN ; 

解决我所有的问题,当然这在 ANTLR 中是非法的

【问题讨论】:

    标签: parsing antlr grammar visitor-pattern left-recursion


    【解决方案1】:

    写出这个问题后,我开始思考更多功能

    长话短说,我正在使用语法

    expr : 
        | expr (plus|minus) expr
        | expr (multi|div) expr
        | '(' expr ')'
        | literal
        ;
    
    plus   : '+' ;
    minus  : '-' ;
    multi  : '*' ;
    div    : '/' ;
    

    这给了我们倾听者:

    onVisitExit(ExprContext ctx){
        left = values.get(ctx.child(0));
        right = values.get(ctx.child(2));
        operator = binaryOperators.get(ctx.child(1));
    
        result = operator.doUsing(left, right);
        values.put(ctx, result);
    }
    
    onVisitExit(PlusContext ctx){
        binaryOperators.put(ctx, (left, right) -> left + right);
    }
    
    onVisitExit(MinusContext ctx){
        binaryOperators.put(ctx, (left, right) -> left - right);
    }
    
    //...
    

    它解决了我所有的问题——事实上,第一个访问者实现很可能没有一个 iffor 语句,这真的很漂亮。

    但是为了长篇大论:

    标准多态性技术会让您尝试将开关中的功能推送到您打开的变量中。所以代码

    switch(operator.getToken()){
        case "+": do(left + right);
        //...
    

    变成

    operator.doOperationUsing(left, right);
    

    然后操作符的各种实现会做不同的事情。问题是为操作员生成不同的实现。对于典型的多态性,如果您只使用枚举,那么每个枚举实例具有自定义实现的枚举实际上并不比仅仅切换更好。但是在这里,我们可以使用访问过的非终结规则来生成该实现,带有 lambda :)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-07-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-05
      • 2014-01-14
      相关资源
      最近更新 更多