ANTLR(或其他):解析与评估解耦

我有一个相对简单的DSL,我希望比一些手动编写的java.util.regex.Pattern语句+解析逻辑更强大。

最引用的工具似乎是ANTLR。 我不熟悉它,并且愿意试一试。 然而,当我看到这些例子时(例如ANTLR表达式求值器例子,或者Martin Fowler的HelloAntlr,或者这个在stackoverflow上的其他Q),我会有点茫然。 原因在于语法文件看起来像是散布着语言定义的大杂烩,其中散布着实际语言中必不可少的实现语言(例如Java)片段。

我真正喜欢的是将解析器的命令/评估部分分开。 有没有一种方法可以使用ANTLR(或其他工具)来定义语法并生成一组Java源文件,以便将它编译成可用于将输入解析为无结构操作的结构的类?

例如,如果我想仅使用+*()运算符来使用表达式评估,并且我有输入

3 *(4 + 7 * 6)*(3 + 7 *(4 + 2))

那么我想要做的就是编写一个语法来将其转换为类似的层次结构

Product
  Term(3)
  Sum
     Term(4)
     Product
        Term(7)
        Term(6)
  Sum
     Term(3)
     Product
        Term(7)
        Sum
            Term(4)
            Term(2)

我可以在哪里使用类

interface Expression<T> {
    public T evaluate();
}

class Term implements Expression<Double> {
    final private double value;
    @Override public Double evaluate() { return value; }
}

class Product implements Expression<Double> {
    final private List<Expression<Double>> terms;
    @Override public Double evaluate() {
        double result = 1;
        for (Expression<Double> ex : terms)
            result *= ex.evaluate();
        return result;
    }
}

class Sum implements Expression<Double> {
    final private List<Expression<Double>> terms;
    @Override public Double evaluate() {
        double result = 0;
        for (Expression<Double> ex : terms)
            result += ex.evaluate();
        return result;
    }
}

并使用ANTLR来构建结构。 有没有办法做到这一点? 我真的更愿意采用这种方法,因为它可以让我(和其他软件工程师)编辑和可视化完整的Java类,而不必将这些类碎片化成ANTLR语法文件中的奇怪部分。

有没有办法做到这一点?


澄清:我想以两种方式尽可能地花费我的努力:定义语法本身,以及ANTLR独立的Java(例如我的Product / Sum / Term类)。 我想尽量减少学习ANTLR语法,怪癖和API的时间/经验。 我不知道如何从ANTLR语法创建和操作AST。 因为这只是大型Java项目的一小部分,所以不仅仅是我,我的团队中的任何人都必须检查或维护我的代码。

(我的意思不是听起来不合适:我愿意花时间和精力投入使用工具,但前提是工具成为一种有用的工具,不会成为绊脚石。)


Jason S写道:

有没有办法做到这一点?

是。

首先定义你的语法(我只用+*()运算符来表达你的表达式解析器的例子):

grammar Exp;

// parser rules
parse
  :  additionExp
  ;

additionExp
  :  multiplyExp (Add multiplyExp)*
  ;

multiplyExp
  :  atomExp (Mult atomExp)* 
  ;

atomExp
  :  Number
  |  LParen additionExp RParen
  ;

// lexer rules
Add    : '+' ;
Mult   : '*' ;
LParen : '(' ;
RParen : ')' ;   
Number : ('0'..'9')+ ('.' ('0'..'9')+)? ;
Spaces : (' ' | 't' | 'r'| 'n') {$channel=HIDDEN;} ;

如果你想让ANTLR从上面的语法中产生一个合适的AST,你必须在语法顶部(在语法声明下)放置以下内容:

options { 
  output=AST; 
}

并且您必须指出每个解析规则的根应该是什么。 这可以通过两种方式完成:

  • 通过使用重写规则;
  • 或者放置其中一个“内嵌树运算符” ^! 令牌之后:
  • ^意味着:使这个标记成为根;
  • ! 意思是:从AST中排除这个标记。
  • 现在你的语法看起来像这样:

    grammar Exp;
    
    options { 
      output=AST; 
    }
    
    // parser rules
    parse
      :  additionExp
      ;
    
    additionExp
      :  multiplyExp (Add^ multiplyExp)*
      ;
    
    multiplyExp
      :  atomExp (Mult^ atomExp)* 
      ;
    
    atomExp
      :  Number
      |  LParen! additionExp RParen!
      ;
    
    // lexer rules
    Add    : '+' ;
    Mult   : '*' ;
    LParen : '(' ;
    RParen : ')' ;   
    Number : ('0'..'9')+ ('.' ('0'..'9')+)? ;
    Spaces : (' ' | 't' | 'r'| 'n') {$channel=HIDDEN;} ;
    

    正如你所看到的,我创建了AddMult根, Mult除了括号。

    现在从语法中生成一个词法分析器和解析器:

    java -cp antlr-3.2.jar org.antlr.Tool Exp.g 
    

    创建一个小测试用具:

    import org.antlr.runtime.*;
    import org.antlr.runtime.tree.*;
    import java.util.*;
    
    public class Main {
    
        private static void preOrder(CommonTree tree, int depth) {
            for(int i = 0; i < depth; i++) {
                System.out.print("- ");
            }
            System.out.println("> "+tree + " :: " + ExpParser.tokenNames[tree.getType()]);
            List children = tree.getChildren();
            if(children == null) return;
            for(Object o : children) {
                preOrder((CommonTree)o, depth+1);
            }
        }
    
        public static void main(String[] args) throws Exception {
            ANTLRStringStream in = new ANTLRStringStream("3 * (4 + 7 * 6) * (3 + 7 * (4 + 2))");
            ExpLexer lexer = new ExpLexer(in);
            CommonTokenStream tokens = new CommonTokenStream(lexer);
            ExpParser parser = new ExpParser(tokens);
            CommonTree tree = (CommonTree)parser.parse().getTree();
            preOrder(tree, 0);
        }
    }
    

    编译一切:

    javac -cp antlr-3.2.jar *.java
    

    并运行Main类:

    // *nix/Mac OS
    java -cp .:antlr-3.2.jar Main
    
    // Windows
    java -cp .;antlr-3.2.jar Main
    

    它产生以下内容:

    > * :: Mult
    - > * :: Mult
    - - > 3 :: Number
    - - > + :: Add
    - - - > 4 :: Number
    - - - > * :: Mult
    - - - - > 7 :: Number
    - - - - > 6 :: Number
    - > + :: Add
    - - > 3 :: Number
    - - > * :: Mult
    - - - > 7 :: Number
    - - - > + :: Add
    - - - - > 4 :: Number
    - - - - > 2 :: Number
    

    正如你所看到的, parse规则(方法)返回一个CommonTree对象,你可以使用它来创建你自己的walker / visitor,使语法保持原样。

    HTH


    如何使用ANTLR AST(抽象语法树)并通过访问每个树节点为您的类构建一个镜像树。


    @Giuseppe Cardone添加了一些很棒的链接,我在这里发布:

    http://www.antlr.org/article/1100569809276/use.tree.grammars.tml

    http://www.antlr.org/article/1170602723163/treewalkers.html

    一个例子可以在下面找到:

    http://sagarsunkle.spaces.live.com/blog/cns!E07F3B561597E4EE!664.entry?sa=97619042


    为了简洁起见,您提到的示例在语法中嵌入了解析器操作。 这适用于小型项目。 对于大一点的人,你最好先做一个AST,然后做任何你想做的事情。 通过嵌入创建树的动作,你可以做到这一点,但是antlr提供了一个更好的声明方式:

    http://www.antlr.org/wiki/display/ANTLR3/Tree+construction

    然后可以使用树语法生成代码,例如使用StringTemplate。 我用这个工具链作为我的论文,它的工作就像一个魅力。 但我敢打赌,如果没有Anlr3参考书(http://pragprog.com/titles/tpantlr/the-definitive-antlr-reference)

    我还发现在antlr页面上链接的讲义非常有用:http://www.antlr.org/wiki/display/CS652/CS652+Home

    另外,请使用AntlrWorks来测试您的语法。 还有一个可用的语法单元测试套件。 另外,antlr邮件列表非常活跃,Terence Parr积极响应大多数帖子。 另外,这很有趣。

    链接地址: http://www.djcxy.com/p/43699.html

    上一篇: ANTLR (or alternative): decoupling parsing from evaluation

    下一篇: Google Closure Compiler Includes