【问题标题】:Getting ANTLR to generate a script interpreter?让 ANTLR 生成脚本解释器?
【发布时间】:2014-07-15 18:55:26
【问题描述】:

假设我有以下所有打包为 blocks.jar 的 Java API:

public class Block {
    private Sting name;
    private int xCoord;
    private int yCoord;

    // Getters, setters, ctors, etc.

    public void setCoords(int x, int y) {
        setXCoord(x);
        setYCoord(y);
    }
}

public BlockController {
    public static moveBlock(Block block, int newXCoord, int newYCoord) {
        block.setCooords(newXCoord, newYCoord);
    }

    public static stackBlocks(Block under, Block onTop) {
        // Stack "onTop" on top of "under".
        // Don't worry about the math here, this is just for an example.
        onTop.setCoords(under.getXCoord() + onTop.getXCoord(), under.getYCoord());
    }
}

同样,不必担心数学问题以及 (x,y) 坐标无法准确表示 3D 空间中的块这一事实。关键是我们有 Java 代码,编译为 JAR,对块执行操作。我现在想构建一个轻量级脚本语言,允许非程序员调用各种块 API 方法和操作块,我想用 ANTLR 实现它的解释器(最新版本是 4.3)。

脚本语言,我们称之为 BlockSpeak,可能如下所示:

block A at (0, 10)   # Create block "A" at coordinates (0, 10)
block B at (0, 20)   # Create block "B" at coordinates (0, 20)
stack A on B         # Stack block A on top of block B

这可能等同于以下 Java 代码:

Block A, B;
A = new Block(0, 10);
B = new Block(0, 20);
BlockController.stackBlocks(B, A);

因此想法是 ANTLR 生成的解释器将 *.blockspeak 脚本作为输入,并使用此脚本中的命令调用 blocks.jar API 操作。我阅读了优秀的Simple Example,它使用 ANTLR 创建了一个简单的计算器。然而,在该链接中,有一个 ExpParser 类具有 eval() 方法:

ExpParser parser = new ExpParser(tokens);
parser.eval();

这里的问题是,在计算器的情况下,tokens 表示要计算的数学表达式,eval() 返回表达式的计算结果。在解释器的情况下,tokens 将代表我的 BlockSpeak 脚本,但调用 eval() 不应该评估任何东西,它应该知道如何将各种 BlockSpeak 命令映射到 Java 代码:

BlockSpeak Command:             Java code:
==========================================
block A at (0, 10)      ==>     Block A = new Block(0, 10);
block B at (0, 20)      ==>     Block B = new Block(0, 20);
stack A on B            ==>     BlockController.stackBlocks(B, A);

所以我的问题是,我在哪里执行这个“映射”?换句话说,当 ANTLR 在 BlockSpeak 脚本中遇到特定语法时,如何指示 ANTLR 调用各种代码(封装在 blocks.jar 中)? 更重要的是,谁能给我一个伪代码示例?

【问题讨论】:

  • 你考虑过 Xtext 吗?那会给你一个很好的编辑器,等等。在使用 Xtext 和 Xtend 实现特定领域的语言中,作者在第 8 章展示了如何实现解释器。(虽然如果你不习惯使用 eclipse 作为依赖,你也可以创建独立的应用程序。)
  • 谢谢@GáborBakos (+1) - 我很欣赏这个建议,但我正在寻找一个基于 ANTLR 的解决方案,原因超出了这个问题的范围!
  • 没问题。 (虽然Xtext也是基于antlr的。)

标签: java antlr grammar interpreter scripting-language


【解决方案1】:

我只是在运行中评估脚本,而不是生成需要自己再次编译的 Java 源文件。

对于 ANTLR 4,强烈建议将语法和目标特定代码彼此分开,并将任何目标特定代码放在树侦听器或访问者中。

我将快速演示如何使用监听器。

示例输入的语法可能如下所示:

文件:blockspeak/BlockSpeak.g4

grammar BlockSpeak;

parse
 : instruction* EOF
 ;

instruction
 : create_block
 | stack_block
 ;

create_block
 : 'block' NAME 'at' position
 ;

stack_block
 : 'stack' top=NAME 'on' bottom=NAME
 ;

position
 : '(' x=INT ',' y=INT ')'
 ;

COMMENT
 : '#' ~[\r\n]* -> skip
 ;

INT
 : [0-9]+
 ;

NAME
 : [a-zA-Z]+
 ;

SPACES
 : [ \t\r\n] -> skip
 ;

一些支持 Java 的类:

文件:blockspeak/Main.java

package blockspeak;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

import java.util.Scanner;

public class Main {

    public static void main(String[] args) throws Exception {

        Scanner keyboard = new Scanner(System.in);

        // Some initial input to let the parser have a go at.
        String input = "block A at (0, 10)   # Create block \"A\" at coordinates (0, 10)\n" +
                "block B at (0, 20)   # Create block \"B\" at coordinates (0, 20)\n" +
                "stack A on B         # Stack block A on top of block B";

        EvalBlockSpeakListener listener = new EvalBlockSpeakListener();

        // Keep asking for input until the user presses 'q'.
        while(!input.equals("q")) {

            // Create a lexer and parser for `input`.
            BlockSpeakLexer lexer = new BlockSpeakLexer(new ANTLRInputStream(input));
            BlockSpeakParser parser = new BlockSpeakParser(new CommonTokenStream(lexer));

            // Now parse the `input` and attach our listener to it. We want to reuse 
            // the same listener because it will hold out Blocks-map.
            ParseTreeWalker.DEFAULT.walk(listener, parser.parse());

            // Let's see if the user wants to continue.
            System.out.print("Type a command and press return (q to quit) $ ");
            input = keyboard.nextLine();
        }

        System.out.println("Bye!");
    }
}

// You can place this Block class inside Main.java as well.
class Block {

    final String name;
    int x;
    int y;

    Block(String name, int x, int y) {
        this.name = name;
        this.x = x;
        this.y = y;
    }

    void onTopOf(Block that) {
        // TODO
    }
}

这个主类使用内联 cmets 很容易解释。棘手的部分是听众应该是什么样子。嗯,就是这样:

文件:blockspeak/EvalBlockSpeakListener.java

package blockspeak;

import org.antlr.v4.runtime.misc.NotNull;

import java.util.HashMap;
import java.util.Map;

/**
 * A class extending the `BlockSpeakBaseListener` (which will be generated
 * by ANTLR) in which we override the methods in which to create blocks, and
 * in which to stack blocks.
 */
public class EvalBlockSpeakListener extends BlockSpeakBaseListener {

    // A map that keeps track of our Blocks.
    private final Map<String, Block> blocks = new HashMap<String, Block>();

    @Override
    public void enterCreate_block(@NotNull BlockSpeakParser.Create_blockContext ctx) {

        String name = ctx.NAME().getText();
        Integer x = Integer.valueOf(ctx.position().x.getText());
        Integer y = Integer.valueOf(ctx.position().y.getText());

        Block block = new Block(name, x, y);

        System.out.printf("creating block: %s\n", name);

        blocks.put(block.name, block);
    }

    @Override
    public void enterStack_block(@NotNull BlockSpeakParser.Stack_blockContext ctx) {

        Block bottom = this.blocks.get(ctx.bottom.getText());
        Block top = this.blocks.get(ctx.top.getText());

        if (bottom == null) {
            System.out.printf("no such block: %s\n", ctx.bottom.getText());
        }
        else if (top == null) {
            System.out.printf("no such block: %s\n", ctx.top.getText());
        }
        else {
            System.out.printf("putting %s on top of %s\n", top.name, bottom.name);
            top.onTopOf(bottom);
        }
    }
}

上面的监听器定义了 2 个映射到以下解析器规则的方法:

create_block
 : 'block' NAME 'at' position
 ;

stack_block
 : 'stack' top=NAME 'on' bottom=NAME
 ;

每当解析器“进入”这样的解析器规则时,就会调用监听器内部的相应方法。所以,每当enterCreate_block(解析器进入create_block规则)被调用时,我们创建(并保存)一个块,当enterStack_block被调用时,我们检索操作中涉及的第2个块,并在上面堆叠一个最重要的。

要查看上述 3 个类的实际效果,请在包含 blockspeak/ 目录以及 .g4.java 文件的目录中下载 ANTLR 4.4

打开控制台并执行以下 3 个步骤:

1。生成 ANTLR 文件:

java -cp antlr-4.4-complete.jar org.antlr.v4.Tool blockspeak/BlockSpeak.g4 -package blockspeak

2。编译所有 Java 源文件:

javac -cp ./antlr-4.4-complete.jar blockspeak/*.java

3。运行主类:

3.1。 Linux/Mac
java -cp .:antlr-4.4-complete.jar blockspeak.Main
3.2.视窗
java -cp .;antlr-4.4-complete.jar blockspeak.Main

这是运行Main 类的示例会话:

bart@hades:~/Temp/demo$ java -cp .:antlr-4.4-complete.jar blockspeak.Main
creating block: A
creating block: B
putting A on top of B
Type a command and press return (q to quit) $ block X at (0,0)
creating block: X
Type a command and press return (q to quit) $ stack Y on X
no such block: Y
Type a command and press return (q to quit) $ stack A on X 
putting A on top of X
Type a command and press return (q to quit) $ q
Bye!
bart@hades:~/Temp/demo$ 

有关树侦听器的更多信息:https://theantlrguy.atlassian.net/wiki/display/ANTLR4/Parse+Tree+Listeners

【讨论】:

    【解决方案2】:

    我会亲自编写一个语法来为每个脚本生成一个 Java 程序,然后您可以编译(连同您的 jar)并独立运行......即,一个两步过程。

    例如,使用类似于以下简单语法的内容(我尚未测试,我相信您需要扩展和调整),您可以将该示例中的 parser.eval() 语句替换为 parser.program();(也用“BlockSpeak”替换“Exp”),它应该会输出与stdout 匹配的脚本的Java 代码,您可以将其重定向到一个.java 文件中,编译(与jar 一起)并运行。

    BlockSpeak.g

    grammar BlockSpeak;
    
    program 
        @init { System.out.println("//import com.whatever.stuff;\n\npublic class BlockProgram {\n    public static void main(String[] args) {\n\n"); }
        @after { System.out.println("\n    } // main()\n} // class BlockProgram\n\n"); }
        : inss=instructions                         { if (null != $inss.insList) for (String ins : $inss.insList) { System.out.println(ins); } }
        ;
    
    instructions returns [ArrayList<String> insList]
        @init { $insList = new ArrayList<String>(); }
        : (instruction { $insList.add($instruction.ins); })* 
        ;
    
    instruction returns [String ins]
        :  ( create { $ins = $create.ins; } | move  { $ins = $move.ins; } | stack { $ins = $stack.ins; } ) ';' 
        ;
    
    create returns [String ins]
        :  'block' id=BlockId 'at' c=coordinates    { $ins = "        Block " + $id.text + " = new Block(" + $c.coords + ");\n"; }
        ;
    
    move returns [String ins]
        :  'move' id=BlockId 'to' c=coordinates     { $ins = "        BlockController.moveBlock(" + $id.text + ", " + $c.coords + ");\n"; }
        ;
    
    stack returns [String ins]
        :  'stack' id1=BlockId 'on' id2=BlockId     { $ins = "        BlockController.stackBlocks(" + $id1.text + ", " + $id2.text + ");\n"; }
        ;
    
    coordinates returns [String coords]
        :    '(' x=PosInt ',' y=PosInt ')'          { $coords = $x.text + ", " + $y.text; }
        ;
    
    BlockId
        :    ('A'..'Z')+
        ;
    
    PosInt
        :    ('0'..'9') ('0'..'9')* 
        ;
    
    WS  
        :   (' ' | '\t' | '\r'| '\n')               -> channel(HIDDEN)
        ;
    

    (请注意,为简单起见,此语法要求用分号分隔每条指令。)

    当然还有其他方法可以做这种事情,但这对我来说似乎是最简单的。

    祝你好运!


    更新

    所以我继续“完成”了我的原始帖子(修复了上述语法中的一些错误)并在一个简单的脚本上对其进行了测试。

    这是我用来测试上述语法的 .java 文件(取自您在上面发布的代码存根)。请注意,在您的情况下,您可能希望将脚本文件名(在我的代码 "script.blockspeak" 中)设置为命令行参数。此外,BlockBlockController 类当然会来自您的 jar。

    BlockTest.java

    import org.antlr.v4.runtime.*;
    
    class Block {
        private String name;
        private int xCoord;
        private int yCoord;
    
        // Other Getters, setters, ctors, etc.
        public Block(int x, int y) { xCoord = x; yCoord = y; }
    
        public int getXCoord() { return xCoord; }
        public int getYCoord() { return yCoord; }
    
        public void setXCoord(int x) { xCoord = x; }
        public void setYCoord(int y) { yCoord = y; }
    
        public void setCoords(int x, int y) {
            setXCoord(x);
            setYCoord(y);
        }
    }
    
    class BlockController {
        public static void moveBlock(Block block, int newXCoord, int newYCoord) {
            block.setCoords(newXCoord, newYCoord);
        }
    
        public static void stackBlocks(Block under, Block onTop) {
            // Stack "onTop" on top of "under".
            // Don't worry about the math here, this is just for an example.
            onTop.setCoords(under.getXCoord() + onTop.getXCoord(), under.getYCoord());
        }
    }
    
    public class BlocksTest {
        public static void main(String[] args) throws Exception {
            ANTLRFileStream in = new ANTLRFileStream("script.blockspeak");
            BlockSpeakLexer lexer = new BlockSpeakLexer(in);
            CommonTokenStream tokens = new CommonTokenStream(lexer);
            BlockSpeakParser parser = new BlockSpeakParser(tokens);
            parser.program();
        }
    }
    

    这是我使用的命令行(在我的 MacBook Pro 上):

    > java -jar antlr-4.4-complete.jar BlockSpeak.g
    > javac -cp .:antlr-4.4-complete.jar *.java
    > java -cp .:antlr-4.4-complete.jar BlocksTest > BlockProgram.java
    

    这是输入脚本:

    script.blockspeak

    block A at (0, 10);                                                                                                                                            
    block B at (0, 20);
    stack A on B;
    

    这是输出:

    BlockProgram.java

    //import com.whatever.stuff;
    
    public class BlockProgram {
        public static void main(String[] args) {
    
    
            Block A = new Block(0, 10);
    
            Block B = new Block(0, 20);
    
            BlockController.stackBlocks(A, B);
    
    
        } // main()
    } // class BlockProgram
    

    您当然必须为每个脚本编译并运行 BlockProgram.java。


    在回答您评论中的一个问题(#3)时,我首先考虑了几个更复杂的选项,它们可能会简化您的“用户体验”。

    (A) 您可以将对BlockController 的调用直接嵌入到ANTLR 操作中,而不是使用语法生成然后必须编译和运行的java 程序。在我创建字符串并将它们从一个非终端传递到下一个非终端的地方,只要识别到 instruction 规则,您就可以在那里直接使用 java 代码执行您的 Block 命令。这在 ANTLR 语法和导入方面需要更复杂一些,但在技术上是可行的。

    (B) 如果您要执行选项 A,那么您可以更进一步并创建一个 interactive 解释器(“shell”),其中向用户显示提示并只需键入在提示符下的“blockspeak”命令中,然后直接解析并执行,将结果显示给用户。

    复杂性而言,这些选项都不太难完成,但它们都需要做更多的编码,这超出了 Stack Overflow 答案的范围。这就是为什么我选择在这里提出一个“更简单”的解决方案。

    【讨论】:

    • 优秀的答案@Turix (+1) - 不过还有一些后续问题! (1) 在您的示例语法中,您引用了 SystemBlockBlockController 类... ANTLR 如何在运行时链接到这些,因为它们不是“导入”带有完整的包名?例如,它如何知道您调用的是java.lang.System 而不是com.foo.bar.System(2) parser 是什么类类型?我查看了 ANTLR 4.3 javadocs,找不到像 ParserAbstractParser 这样有 eval()program() 方法的东西。
    • (3) 你的回答让我很好奇;你开始说“我会亲自编写一个语法来生成一个 Java 程序......”,然后你说“当然还有其他方法可以做到这一点东西……”。这让我觉得这只是完成我需要的许多策略之一。如果是这样,你能详细说明一下吗?您可以提供任何链接,以便我可以阅读它们吗?并不是我不喜欢你的方法,它可能绝对是最好的,我只是想尽可能地了解情况。再次感谢!
    • @IAmYourFaja 我刚刚更新了我的帖子,提供了更多详细信息。 (这一切都如所介绍的那样工作。)关于(1),ANTLR 在这方面非常聪明。对于System,不需要导入任何东西,但是对于我使用的ArrayList,ANTLR 知道导入java.util.ArrayList。如果您想执行我的选项 (A),那么您需要在语法的开头添加一个 @header 块以包含您的自定义导入。关于 (2),它是一个 BlockSpeakParser,由 ANTLR 生成以扩展其 parser 类。 program() 匹配语法中顶部非终结符的名称。关于 qn (3),我将其添加到我的更新中。祝你好运!
    • @IamYourFaja 没问题。让我知道您是否决定走选项(A)或(B)的路线。也不是那么难。 (A) 需要更多地了解您的BlockController 将如何处理每条指令(例如,更新数据库模型?),尤其是关于指令的验证(例如,您是试图移动一个未知的块?操作合法吗?等等),如果它失败了会发生什么。 (B) 需要一个输入循环,为每一行用户输入重复调用parser.instruction(),并处理来自BlockController 的任何(错误)响应。
    • 抱歉听起来有点刺耳,我知道你的意思很好。但建议的解决方案不是我建议实施(和维护)的东西。
    【解决方案3】:

    ExpParser中的eval()是通过方法调用实现的;只是调用具有运算符形式的快捷语法。

    作为练习,更改ExpParser 添加一个Calculator 类,其中包含数学运算符的(未实现的)方法、add()multiply()divide() 等,然后更改规则以使用这些方法而不是运算符。因此,您将了解您需要为 BlockSpeak 解释器做什么的基础。

    additionExp returns [double value]
        :    m1=multiplyExp       {$value =  $m1.value;} 
             ( '+' m2=multiplyExp {$value = Calculator.add($value, $m2.value);} 
             | '-' m2=multiplyExp {$value = Calculator.subtract($value, $m2.value);}
             )* 
        ;
    

    【讨论】:

    • 感谢@Apalala (+1) - 但这里有几个问题! (1) 你是说我应该修改 ExpParser 类,还是根本不使用它?如果您说的是前者,这似乎不是一个非常可维护的过程,因为每次语法规则因任何原因发生变化时,我都需要修改 ANTLR 给我的输出ExpParser。 (2) 如何“连接”语法文件中的Calculator,比如com.me.myorg.Calculator
    • 并且 (3) 您能否发布一个完整的代码示例以及您对 ExpParser 和 Calculator 的建议更改?我想我仍然无法通过这里的树木看到森林,再次感谢!
    • 您可以使用 ExpParser 作为模板。我认为您会将BlockSpeakBuilder bscBlock block 参数传递给语法中的规则,因此您的规则可以执行{$value = bsc.newBlock();}{block.setSomeFeature($var);} 之类的东西。
    • 为了给你一个更具体的例子,我需要一个 BlockSpeak 的语法,无论如何这应该是你的起点。尝试在没有语义操作的情况下编写和测试语法,并用结果更新您的问题。
    • 谢谢@Apalala(两者都+2) - 请给我大约 24 小时,我会用 BlockSpeak 的特定语法更新我的问题!再次感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-09-03
    • 2013-12-28
    • 2012-08-05
    • 2010-10-15
    • 2023-03-07
    • 2013-02-20
    • 2011-09-06
    相关资源
    最近更新 更多