Получение ANTLR для создания интерпретатора script?

Скажем, у меня есть следующий Java API, который все пакеты имеют blocks.jar:

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) координаты не точно представляют блоки в трехмерном пространстве. Дело в том, что у нас есть 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 script, а команды <0 → будут использовать команды blocks.jar API. Я прочитал отличный Простой пример, который создает простой калькулятор с использованием ANTLR. Однако в этой ссылке существует класс ExpParser с методом eval():

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

Проблема заключается в том, что в случае калькулятора tokens представляет математическое выражение для оценки, а eval() возвращает оценку выражения. В случае интерпретатора tokens будет представлять мой BlockSpeak script, но вызов 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 для вызова различных фрагментов кода (упакованных внутри blocks.jar), когда он сталкивается с определенными грамматиками в BlockSpeak script? Что еще важнее, может ли кто-нибудь дать мне пример псевдокода?

Ответ 1

Я бы просто оценил script на лету, а не генерировал исходные файлы 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 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
    }
}

Этот основной класс довольно понятен с помощью встроенных комментариев. Трудная часть - это то, на что должен выглядеть слушатель. Ну вот, вот:

Файл: 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 класса выше в действии, загрузите ANTLR 4.4 в каталог, в котором находится каталог blockspeak/ с .g4 и .java.

Откройте консоль и выполните следующие три шага:

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. Windows
java -cp .;antlr-4.4-complete.jar blockspeak.Main

Ниже приведен пример сеанса работы класса Main:

[email protected]:~/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!
[email protected]:~/Temp/demo$ 

Дополнительная информация о прослушивателях деревьев: https://theantlrguy.atlassian.net/wiki/display/ANTLR4/Parse+Tree+Listeners

Ответ 2

Я лично написал грамматику для создания Java-программы для каждого script, которую вы могли бы скомпилировать (вместе с вашей банкой) и запустить независимо... т.е. двухэтапный процесс.

Например, с помощью следующей простой грамматики (которую я не тестировал, и я уверен, что вам нужно будет расширять и адаптировать), вы можете заменить оператор parser.eval() в этом примере на parser.program(); (также заменяя "BlockSpeak" для "Exp" ), и он должен выплевывать Java-код, который соответствует script to stdout, который можно перенаправить в файл .java, скомпилировать (вместе с банкой) и запустить.

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

(Заметим, что для простоты эта грамматика требует, чтобы полуколоны разделяли каждую инструкцию.)

Конечно, есть и другие способы сделать такие вещи, но это кажется мне самым простым.

Удачи!


Обновление

Итак, я пошел вперед и "закончил" свое оригинальное сообщение (исправление нескольких ошибок в вышеупомянутой грамматике) и проверил его на простой script.

Вот файл .java, который я использовал для проверки вышеуказанной грамматики (взятой из выпадающих надписей кода выше). Обратите внимание, что в вашей ситуации вы, вероятно, захотите сделать имя файла script (в моем коде "script.blockspeak") параметром командной строки. Кроме того, конечно, классы Block и BlockController были бы выбраны из вашей банки.

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:

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 для каждого script.


В ответ на один из вопросов в вашем комментарии (№ 3) есть несколько более сложных вариантов, которые я впервые рассмотрел, которые могут оптимизировать ваш "пользовательский опыт".

(A) Вместо того, чтобы использовать грамматику для создания Java-программы, которую вы затем должны компилировать и запускать, вы можете встраивать вызовы в BlockController непосредственно в действия ANTLR. Там, где я создал строки и передал их из одного нетерминального в следующий, вы могли бы иметь Java-код, непосредственно выполняющий свои команды блока, когда распознается правило instruction. Это потребует немного большей сложности в отношении грамматики и импорта ANTLR, но это технически выполнимо.

(B) Если вам нужно было сделать вариант A, вы могли бы сделать еще один шаг и создать интерактивный интерпретатор ( "оболочка" ), где пользователю будет предложено приглашение и просто введите команды "blockspeak" на приглашения, которые затем анализируются и выполняются напрямую, показывая результаты обратно пользователю.

Ни один из этих вариантов не сложнее выполнить с точки зрения сложности, но каждый из них требует гораздо большего количества кодирования, которое выходит за рамки ответа на переполнение стека. Вот почему я решил представить здесь "более простое" решение.

Ответ 3

eval() в ExpParser реализуется через вызовы методов; это просто, что вызовы имеют ярлык синтаксиса в виде операторов.

В качестве упражнения измените 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);}
         )* 
    ;