Demander à ANTLR de générer un interpréteur de script?

disons que j'ai L'API Java suivante que tous les paquets sontblocks.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());
    }
}

encore une fois,ne vous inquiétez pas des mathématiques et du fait que les coordonnées (x, y) ne représentent pas avec précision les blocs dans l'espace 3D. Le fait est que nous avons du code Java, compilé comme un JAR, qui effectue des opérations sur des blocs. Je veux maintenant construire un langage de script léger qui permet à un non-programmeur d'invoquer les différentes méthodes D'API de blocs et de manipuler les blocs, et je veux mettre en œuvre son interprète avec ANTLR (la dernière version est 4.3).

Le langage de script, nous allons l'appeler BlockSpeak pourrait ressembler à cela:

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

cela peut être équivalent au code Java suivant:

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

donc l'idée est que L'interpréteur généré par ANTLR prendrait un *.blockspeak script comme input, et utilisez les commandes de ce script pour invoquer blocks.jar opérations API. J'ai lu l'excellent Exemple Simple qui crée une calculatrice simple utilisant ANTLR. Néanmoins, dans ce lien, il y a un ExpParser classe eval() méthode:

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

Le problème ici est que, dans le cas de la calculatrice, le tokens représentent une expression mathématique pour évaluer et eval() renvoie l'évaluation de l'expression. Dans le cas d'un interprète, le tokens représente mon script BlockSpeak, mais appelle eval() ne devrait pas évaluer quoi que ce soit, il faut savoir comment pour associer les différentes commandes BlockSpeak au code 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);

Donc ma question est, où puis-je effectuer cette "cartographie"? En d'autres termes, comment puis-je demander à ANTLR d'appeler différents morceaux de code (empaqueté à l'intérieur blocks.jar) quand il rencontre des grammaires particulières dans le script BlockSpeak? plus important encore, quelqu'un peut-il me donner un exemple de pseudo-code?

11
demandé sur Community 2014-07-15 22:55:26

3 réponses

je voudrais simplement évaluer le script à la volée, pas générer des fichiers source Java qui doivent être compilés eux-mêmes à nouveau.

avec ANTLR 4 il est fortement recommandé de garder la grammaire et le code cible spécifique séparés les uns des autres et de mettre tout code cible spécifique à l'intérieur d'un arbre-auditeur ou-visiteur.

je vais donner une démonstration rapide comment utiliser un écouteur.

Une grammaire pour votre exemple d'entrée pourrait ressembler à ceci:

le Fichier: 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
 ;

à l'appui des classes Java:

Fichier: 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
    }
}

Cette classe est assez explicite avec les commentaires en ligne. La partie délicate est ce à quoi l'auditeur est censé ressembler. Eh bien, ici, c'est:

Fichier: 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);
        }
    }
}

L'auditeur ci-dessus a 2 méthodes définies qui correspondent aux suivantes analyseur de règles:

create_block
 : 'block' NAME 'at' position
 ;

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

Chaque fois que l'analyseur "entre" un tel analyseur règle générale, la méthode correspondante à l'intérieur de l'écouteur sera appelé. Donc, chaque fois que enterCreate_block (l'analyseur entre dans le create_block rule) est appelé, nous créons (et sauvegardons) un bloc, et quand enterStack_block est appelé, nous récupérons le bloc 2 impliqué dans l'opération, et empilons l'un sur l'autre.

pour voir les 3 classes ci-dessus en action, Téléchargez ANTLR 4.4 dans le répertoire qui contient le blockspeak/ répertoire avec le .g4 et .java fichier.

Ouvrez une console et effectuez les 3 étapes suivantes:

1. générer les fichiers ANTLR:

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

2. compiler tous les fichiers Java sources:

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

3. Exécuter la classe principale:

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

voici un exemple de session d'exécution du Main catégorie:

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$ 

plus d'informations sur les écouteurs d'arbres: https://theantlrguy.atlassian.net/wiki/display/ANTLR4/Parse+Tree+Listeners

13
répondu Bart Kiers 2014-07-20 18:51:37

j'écrirais personnellement une grammaire pour générer un programme Java pour chaque script que vous pourriez ensuite compiler (avec votre jar) et exécuter indépendamment... c'est à dire, un procédé en 2 étapes.

par exemple, avec quelque chose comme la grammaire simple suivante (que je n'ai pas testé et je suis sûr que vous auriez besoin d'étendre et d'adapter), vous pourriez remplacer le parser.eval() dans cet exemple,parser.program(); (aussi en remplaçant "BlockSpeak" par" Exp " dans l'ensemble) et il devrait recracher Java code qui correspond au script stdout, que vous pouvez rediriger vers A.java file, Compiler (avec le jar) et exécuter.

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

(notez que pour des raisons de simplicité, cette grammaire exige des demi-colonnes pour séparer chaque instruction.)

Il y a bien sûr d'autres façons de faire ce genre de chose, mais cela semble le plus simple pour moi.

bon la chance!


mise à Jour

alors je suis allé de l'avant et j'ai "fini" mon post original (en corrigeant quelques bugs dans la grammaire ci-dessus) et en le testant sur un script simple.

Voici la .le fichier java que j'ai utilisé pour tester la grammaire ci-dessus (tiré des talons de code que vous avez affichés ci-dessus). Notez que dans votre situation, vous voudriez probablement faire le nom du fichier script (dans mon code "script.blockspeak") dans un paramètre de ligne de commande. Aussi, bien sûr, le Block et BlockController les cours viendraient plutôt de votre pot.

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

Et voici les lignes de commandes que j'ai utilisé (sur mon 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

C'est le script d'entrée:

script.blockspeak:

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

Et c'était la sortie:

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

vous devrez alors compiler et exécuter BlockProgram.java pour chaque script.


en réponse à l'une des questions de votre commentaire (#3), Il y a quelques options plus complexes que j'ai d'abord envisagées qui pourraient rationaliser votre "expérience utilisateur".

(A) au lieu d'utiliser la grammaire pour générer un programme java que vous devez ensuite compiler et exécuter, vous pouvez intégrer les appels à BlockController directement dans les actions ANTLR. Où j'ai créé des cordes et les ai passées d'un non-terminal à ensuite, vous pouvez avoir du code java directement en faisant vos commandes de bloc à chaque fois qu'un instruction la règle est reconnu. Cela nécessiterait un peu plus de complexité en ce qui concerne la grammaire ANTLR et les importations, mais c'est techniquement faisable.

(B) Si vous deviez faire l'option A, vous pourriez alors aller plus loin et créer un interactive interpréteur ("shell"), où l'utilisateur est présenté avec un prompt et tape juste les commandes "blockspeak" au prompt, qui sont alors interprétée et exécutée directement, l'affichage des résultats à l'utilisateur.

aucune de ces options n'est beaucoup plus difficile à accomplir en termes de complexité, mais ils nécessitent chacun de faire beaucoup plus de codage qui serait au-delà de la portée d'une réponse de débordement de pile. C'est pourquoi j'ai choisi de présenter un "simple" solution ici.

3
répondu Turix 2014-07-18 18:30:52

eval()ExpParser est implémenté par des appels de méthode; c'est juste que les appels ont une syntaxe de raccourci sous la forme d'opérateurs.

comme exercice, changez ExpParser ajout d'un Calculator classe avec (non implémenté) les méthodes d'opérateurs mathématiques, add(),multiply(),divide(), et ainsi de suite, et puis changer les règles d'utilisation de ces méthodes à la place des opérateurs. Ainsi, vous comprendrez la base de ce que vous devez faire pour votre BlockSpeak interprète.

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);}
         )* 
    ;
1
répondu Apalala 2014-07-17 22:37:33