【问题标题】:ANTLR (or alternative): decoupling parsing from evaluationANTLR(或替代方案):将解析与评估解耦
【发布时间】:2010-10-11 21:04:00
【问题描述】:

我有一个相对简单的 DSL,我想比一堆手动编码的 java.util.regex.Pattern 语句 + 解析逻辑更健壮地处理它。

引用最多的工具似乎是 ANTLR。我不熟悉它,并愿意尝试一下。但是,当我查看示例时(例如 ANTLR expression evaluator example,或 Martin Fowler 的 HelloAntlr,或 this other Q on stackoverflow),我有点怀疑。原因是语法文件看起来像是一个大杂烩,散布着本质上是命令式的实现语言(例如 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 项目的一小部分,所以不仅是我,还有我团队中的任何人都必须审查或维护我的代码。

(我并不是说无礼:我愿意投入时间和精力来使用工具,但前提是该工具成为有用的工具并且不会继续成为绊脚石。)

【问题讨论】:

    标签: java parsing antlr


    【解决方案1】:

    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; 
    }
    

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

    1. 通过使用rewrite rules;
    2. 或通过在标记后放置“内联树运算符”^! 之一:
      • ^ 表示:将此令牌设为根
      • ! 表示:从 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 根,并排除了括号。

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

    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

    【讨论】:

    • +1。感谢您发布分步示例,这或多或少是我需要的。我查看的所有其他示例在 .g 文件中都有命令式操作,我无法弄清楚 ANTLR-ism 与可以删除的 Java 片段的语法是什么。
    • 附注您使用的是 org.antlr.stringtemplate 的哪一部分?它们是推荐/必要的吗?
    • 嗯。您的每个树节点 prints 作为字符串...但是节点是什么?我如何确定一个节点是 multiplyExp 还是 additionExp 或其他?当我有机会时,我会通过调试器试一试,但如果有一个简短+明显的答案,我将不胜感激。
    • @Jason,CommonTree 的内容在其toString(): String 方法中使用。您可以通过其getType(): int 方法获取类型(我稍微更改了我的示例)。不,不需要 StringTemplate 导入:我发布的其他演示中意外出现了(我也删除了导入)。
    • @Jason,是的,这是正确的。要么用你想要的令牌重新构建 AST,要么创建一个扩展 CommonTree 的类,比如 ExpTree,在其中添加额外的功能(如(抽象)evaluate() 方法)并从中创建一些子类ExpTree(如 TermSumProduct)。关于后者的更多信息可以在这里找到:Using custom AST node types
    【解决方案2】:

    如何使用 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

    【讨论】:

    • 嗯。我是 ANTLR 的新手,所以我不熟悉如何去做你的建议,或者它可能有什么优点/缺点。
    • 我想进一步阐述这个问题,但基本上我同意@smink:构建一个 AST(可能使用output=AST ANTLR 选项),然后使用 tree walker 检查/评估/编译它是将语法与代码解耦的最简单方法。
    • +1 @Giuseppe Cardone 获取链接。我在以前的项目中使用过这种技术,效果很好。
    • 感谢您的详细说明。我并不是要以非建设性的方式批评,但我认为“use.tree.grammars.tml”或“treewalkers.html”链接都没有帮助。他们似乎是关于 ANTLR 本身是否采取正确方法的争论:第一个说“访问者不是一个好的解决方案”和“我喜欢 ANTLR 的语法+动作策略”(这正是我遇到的问题) ;第二个似乎是反驳,并建议应该有替代 ANTLR 做事的方式,但不建议这样做。
    • 示例链接(在 sagarsunkle.spaces.live.com)很有用......我有点明白......但我有点不明白。 ://
    【解决方案3】:

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

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

    然后您可以使用树语法来生成代码,例如与字符串模板。 我在我的论文中使用了这个工具链,它就像一个魅力。但我敢打赌,如果没有 Anlr3 参考书(http://pragprog.com/titles/tpantlr/the-definitive-antlr-reference),我会遭受很多痛苦

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-05-31
      • 2014-07-26
      • 2013-08-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多