使用 ANTLR(或任何其他工具)编写术语重写 parser/expression 评估程序

Code a term-rewriting parser/expression evaluator with ANTLR (or any other tool)

我正在尝试编写一个软件,它应该只执行具有这些功能的基本编程语言的指令:

它应该一次一步显示代码 'reduced' 或 'simplified',让我展示一个示例输出示例:

迭代 1:

a=3;
b=2;
c=true;
if(c && (a < 3 * (5 -2) ) || b >= 3 * (5 -2))){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 2:

if(true && (a < 3 * (5 -2) ) || b >= 3 * (5 -2))){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 3:

if(true && (3 < 3 * (5 -2) ) || 2 >= 3 * (5 -2))){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 4:

if(true && (3 < 9 ) || 2 >= 3 * (5 -2))){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 5:

if(true && (3 < 9 ) || 2 >= 3 * 3)){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 6:

if(true && (3 < 9 ) || 2 >= 9)){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 7:

if(true && true || 2 >= 9)){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 8:

if(true && (true || false)){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 9:

if(true && false){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 10:

if(false){
    System.out.println("going through if");
}else{
    System.out.println("going through else");
}

迭代 11:

System.out.println("going through else");

所以它应该在每次迭代时同时解析输入代码并执行,一次只做一个基本操作,替换操作结果并在不需要更多简化步骤时完成循环.任何人都知道如何做到这一点,例如使用 ANTLR 工具?我一直在检查 ANTLR 的 Tree Listener 功能,因为它似乎是可行的方法,但我不清楚如何实现它。

另一个是最佳的是它在 Java 脚本中实现,以便能够在网络浏览器中执行,但是 Java 代码就足够了(即作为Java小程序)。

语法:

program
    :   (variable | function)*
        statement*
    ;


variable
    :   IDENT ('=' expression)? ';'
    ;

type
    :   'int'
    |   'boolean'
    |   'String'
    |   'char'
    ;


statement
    :   assignmentStatement
    |   ifStatement
    ;


ifStatement
    :   'if' '(' expression ')' '{' statement+ '}'
        ('else if' '(' expression ')' '{' statement+)* '}'
        ('else' '{' statement+)? '}'
    ;

assignmentStatement
    :   IDENT '=' expression ';'
    ;


returnStatement
    :   'return' expression ';'
    ;


function
    :   'function' IDENT '(' parameters? ')' '{'
        (statement|returnStatement)*
        '}'
    ;   

parameters
    :   IDENT (',' IDENT)*
    ;

term
    :   IDENT
    |   '(' expression ')'
    |   INTEGER
    ;

negation
    :   '-' -> NEGATION
    ;

unary
    :   ('+'! | negation^)* term
    ;

mult
    :   unary (('*' | '/' | 'mod') unary)*
    ;

add
    :   mult (('+' | '-') mult)*
    ;

relation
    :   add (('=' | '/=' | '<' | '<=' | '>=' | '>') add)*
    ;

expression
    :   relation (('and' | 'or') relation)*
    ;


fragment LETTER : ('a'..'z' | 'A'..'Z') ;
fragment DIGIT : '0'..'9';
INTEGER : DIGIT+ ;
IDENT : LETTER (LETTER | DIGIT)*;
WS : (' ' | '\t' | '\n' | '\r' | '\f')+ {$channel = HIDDEN;};
COMMENT : '//' .* ('\n'|'\r') {$channel = HIDDEN;};

[OP: ...(或任何其他工具)]

您使用短语 term rewriting and then show an interesting example of incrementally processing the source code for a program by substituting values and doing constant folding 生成最终程序答案。

抽象地说,您希望术语重写系统能够从一组 "term rewrite" 规则开始,这些规则本质上指定

if you see this, replace it by that

例如

" if (true) then ... "  ==>   " ... "

并以有组织的方式重复应用这些规则,直到达到某些停止条件(通常是 "no more rules apply")。

有两种方法可以实现术语重写,都以术语(在您的情况下是程序的 AST)开始,并产生相同的结果。区别在于重写规则的实际实现方式。

程序树重写

第一种方式按程序指定重写步骤。也就是说,构造一个访问者来遍历 AST,它在程序上检查节点类型,以及感兴趣的节点类型的子树("match"),并在找到匹配项的地方根据所需的效果修改树的规则。

对于上面的 "if" 示例,访问者会找到 "if" 语句子树根,检查 left/condition 子树以查看它是否是 "true" 子树,并且如果是这样,用产生修改树的右子树替换 "if" 子树。

常量折叠有点特殊:访问者检查运算符节点,检查其所有 children 是否为常量值,计算运算符对这些常量的结果,然后将运算符替换为包含计算结果的节点。

要实现一套规则的改写,首先要写下所有的抽象规则,然后对这个访问者进行编码,结合所有规则的所有测试。这会产生一段非常混乱的代码,它正在检查节点类型并在子树上上下移动以进行进一步检查。 (你也可以按照规则将其实现为一个访问者,这使得它们更容易编写,但是现在你有一大堆访问者,你必须 运行 他们在树上重复......这最终变得非常慢).

访问者有点聪明:你不希望它处理子树"before their time"考虑这段代码,由重写器处理:

  if (x) then y = 3/ 0; else y = 3;

您不希望在 x 被求值之前不断折叠“3/0”

您可以从任何解析器生成器(包括 ANTLR)的 AST 开始实现过程重写;写访客只是汗水。也许很多。

由于将所有规则匹配组合到访问者中的问题,过程重写很难实现。如果你有几十条规则,这很快就会变得难以管理。 (如果您要使用规则来处理完整的计算机语言,则每个语法位至少有一个规则;在这种情况下很容易获得数十个规则,如果不是数百个的话)。

要获得 OP 所需的 "incremental" 显示方面,您必须在每个 match/replace 步骤后停止,然后漂亮地打印 AST,例如,从 AST 重新生成表面语法文本。修改访问者以在每次树修改后调用 prettyprint 并不难。

不过,生成 AST 的解析器生成器通常不会为漂亮的打印步骤提供很好的帮助。做漂亮的打印比看起来更难。细节太复杂了,不能放在这里;有关如何执行此操作的详细信息,请参阅我的 SO answer on how to prettyprint

下一个复杂问题:当遇到正在评估的程序中的变量时,应该在树中替换什么值?为此,语言需要一个符号 table,并且必须使该符号 table 保持最新,变量赋值发生。

没有讨论如果程序是 ill-formed 会发生什么。它们将是:-{ rwrites 可能需要大量 "error checking" 来防止无意义的计算(例如,"x / y",其中 y 是一个字符串)。

直接树重写

理想情况下,您想要的是一个直接接受显式术语重写规则并可以应用它们的引擎。

Program Transformation Systems (PTS) 这样做。具体系统包括 Mathematica(现在叫 "Wolfram language"?)、DMS、TXL、Stratego/XT。 (OP 正在寻找在 Java 中实现的一个:Stratego 有一个 Java 版本,我认为,其他版本绝对没有)。

这些工具接受使用目标语言的表层语法编写的重写规则,将规则本质上转换为成对的模式树(一个"match"具有变量的树占位符)和具有(相同)变量占位符的 "replace" 树。这些工具中的重写引擎将采用指定规则的任意子集,并将它们应用于树,通过将 "match" 树与目标树进行比较来检查所有匹配项,并替换替换树,其占位符已填充从比赛中,当找到比赛时。这是编写复杂的重写集的主要便利。 (如果你想想我,这仍然是程序重写......只是引擎在做而不是你rule-specifier。尽管如此方便。

这样的 PTS 包括构建 AST 的解析器生成器(Mathematica 没有)和一个完整的漂亮打印机(或者至少允许您方便地定义一个)。

对于DMS,你可以这样写规则:

 rule fold_true_ifTE(s: statement, t:statement): statement->statement =
 "  if (true) then \s else \t " ->  " \s ";

 rule fold_false_ifTE(s: statement, t:statement): statement->statement =
 "  if (false) then \s else \t " ->  " \t ";

 rule fold_constants_add(x:NUMBER,y:NUMBER):sum -> sum =
 " \x + \y " -> " \Add\(\x\,\y\)";

前两条规则实现了我们之前勾勒的"if"语句重写;您还需要 "if-then" 语句的规则。引号是 metaquotes;它们将规则规范语言 (RSL) 的文本与规则所操纵的语言的文本分开。 metaescaped 字母 (s, t, x, y) 是元变量,代表规则匹配的子树。这些元变量必须具有由规则参数列表指定的 AST-type(例如,s:语句 表示 s 是 "any statement node")。

第三条规则为"addition"实现常量折叠。该模式寻找“+”运算符;只有当它找到一个同时具有 children 的数字常量时,它才会获得匹配。它通过在其运算符上调用外部过程“\Add”来工作;添加 returns 一棵包含总和的树,重写引擎将其拼接到位。

在 DMS 的情况下,重写机制上有一个挂钩,在每次重写尝试(失败和成功)后都会调用该挂钩来跟踪重写结果。这个钩子将是 OP 调用 prettyprinter 以显示树在每一步后如何变化的地方。

有关如何编写规则来评估代数表达式的详细示例,请参阅 how to implement Algebra with rewrite rules

有关 DMS 重写规则如何工作的更详细说明,以及将它们应用于 "simplifying"(评估)Nikolas Wirth 的编程语言 Oberon 的示例,请参阅 DMS Rewrite Rules

在这两种情况下都没有显示控制规则应用顺序的方法。因为排序约束可以是任意的,所以必须介入并指导重写引擎。如果需要,DMS 可提供对规则排序的完整程序控制。通常可以将规则分成不同的集合:可以不加区别地应用的规则和需要排序的规则(例如 if-then 简化规则)。

PTS 不会使符号 table 问题消失; OP仍然需要一个。大多数 PTS 不为此提供任何支持。 DMS 对此提供了明确的支持(它需要一些努力来配置,但比没有任何努力要少得多!)以及构建静态类型检查器以帮助在程序开始执行之前验证程序的格式是否正确。实际上,在准备执行程序时需要解决很多问题(例如,可能需要构建标签到源代码点的映射以实现高效的 GOTO 模拟)。参见 Life After Parsing

在下面这个 Symja 示例片段中,您可以尝试内置的 Java 术语重写引擎。

术语重写和模式匹配引擎的主要部分在包中实现:GIT: org.matheclipse.core.patternmatching

通过实现 IEvalStepListener 接口,您可以看到引擎在内部运行的步骤:

package org.matheclipse.core.examples;

import org.matheclipse.core.eval.EvalEngine;
import org.matheclipse.core.eval.ExprEvaluator;
import org.matheclipse.core.interfaces.AbstractEvalStepListener;
import org.matheclipse.core.interfaces.IExpr;
import org.matheclipse.parser.client.SyntaxError;
import org.matheclipse.parser.client.math.MathException;

public class SO_StepListenerExample {
    private static class StepListener extends AbstractEvalStepListener {
        /** Listens to the evaluation step in the evaluation engine. */
        @Override
        public void add(IExpr inputExpr, IExpr resultExpr, int recursionDepth, long iterationCounter) {
            System.out.println("Depth " + recursionDepth + " Iteration " + iterationCounter + ": " + inputExpr.toString() + " ==> "
                    + resultExpr.toString());
        }
    }

    public static void main(String[] args) {
        try {
            ExprEvaluator util = new ExprEvaluator( );
            EvalEngine engine = util.getEvalEngine();
            engine.setStepListener(new StepListener());

            IExpr result = util.evaluate(
                    "a=3;b=2;c=True;If(c && (a < 3 * (5 -2) ) || ( b >= 3 * (5 -2)),"
                    + "GOINGTHROUGHIF,"
                    + "GOINGTHROUGHELSE )");
            System.out.println("Result: " + result.toString());
            // disable trace mode if the step listener isn't necessary anymore
            engine.setTraceMode(false);
        } catch (SyntaxError e) {
            // catch Symja parser errors here
            System.out.println(e.getMessage());
        } catch (MathException me) {
            // catch Symja math errors here
            System.out.println(me.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

该示例生成以下输出:

Depth 4 Iteration 0: a=3 ==> 3
Depth 4 Iteration 0: b=2 ==> 2
Depth 3 Iteration 0: a=3;b=2 ==> 2
Depth 3 Iteration 0: c=True ==> True
Depth 2 Iteration 0: a=3;b=2;c=True ==> True
Depth 5 Iteration 0: c ==> True
Depth 6 Iteration 0: a ==> 3
Depth 8 Iteration 0: (-1)*2 ==> -2
Depth 7 Iteration 0: -2+5 ==> -2+5
Depth 7 Iteration 1: 5-2 ==> 3
Depth 6 Iteration 0: 3*(-2+5) ==> 3*3
Depth 6 Iteration 1: 3*3 ==> 9
Depth 5 Iteration 0: a<3*(-2+5) ==> 3<9
Depth 5 Iteration 1: 3<9 ==> True
Depth 4 Iteration 0: c&&a<3*(-2+5) ==> True
Depth 3 Iteration 0: c&&a<3*(-2+5)||b>=3*(-2+5) ==> True
Depth 2 Iteration 0: If(c&&a<3*(-2+5)||b>=3*(-2+5),goingthroughif,goingthroughelse) ==> goingthroughif
Depth 1 Iteration 0: a=3;b=2;c=True;If(c&&a<3*(-2+5)||b>=3*(-2+5),goingthroughif,goingthroughelse) ==> goingthroughif
Result: goingthroughif