web-dev-qa-db-fra.com

Instructions if / else dans ANTLR à l'aide d'écouteurs

Je crée un langage de programmation simple pour un projet scolaire. J'utilise ANTLR 4 pour générer un lexer et un analyseur à partir de ma grammaire. Jusqu'à présent, j'utilisais le modèle d'écoute ANTLR pour appliquer les fonctionnalités réelles du langage de programmation.

Maintenant, je voudrais implémenter les instructions if/else, mais je ne suis pas sûr que celles-ci puissent réellement être implémentées lors de l'utilisation du modèle d'écoute, car ANTLR décide dans quel ordre parcourir l'arbre d'analyse lors de l'utilisation des écouteurs et j'imagine que la mise en œuvre de if/Sinon, les instructions nécessiteront de sauter autour de l'arbre d'analyse en fonction de la condition remplie dans l'instruction.

Quelqu'un peut-il me dire s'il sera possible d'implémenter des instructions if/else en utilisant ANTLR ou si je devrai implémenter le modèle de visiteur moi-même? En outre, quelqu'un peut-il donner un exemple extrêmement simple de la mise en œuvre des déclarations?

43
simonbs

Par défaut, ANTLR 4 génère des écouteurs. Mais si vous donnez org.antlr.v4.Tool le paramètre de ligne de commande -visitor, ANTLR génère pour vous des classes de visiteurs. Ceux-ci fonctionnent un peu comme les auditeurs, mais vous donnent plus de contrôle sur les (sous) arbres qui sont parcourus/visités. Ceci est particulièrement utile si vous souhaitez exclure certains (sous) arbres (comme les blocs else/if, comme dans votre cas). Bien que cela puisse être fait en utilisant des écouteurs, il est beaucoup plus propre de le faire avec un visiteur. À l'aide d'écouteurs, vous devrez introduire des variables globales qui gardent une trace si une (sous) arborescence doit être évaluée, et qui ne le font pas.

En fait, je travaille sur un petit tutoriel ANTLR 4. Ce n'est pas encore fait, mais je posterai un petit exemple de travail qui montre l'utilisation de ces classes de visiteurs et une construction d'instruction if.


1. Grammaire

Voici une grammaire simple prenant en charge les expressions de base, les instructions if-, while- et log-:

Mu.g4

grammar Mu;

parse
 : block EOF
 ;

block
 : stat*
 ;

stat
 : assignment
 | if_stat
 | while_stat
 | log
 | OTHER {System.err.println("unknown char: " + $OTHER.text);}
 ;

assignment
 : ID ASSIGN expr SCOL
 ;

if_stat
 : IF condition_block (ELSE IF condition_block)* (ELSE stat_block)?
 ;

condition_block
 : expr stat_block
 ;

stat_block
 : OBRACE block CBRACE
 | stat
 ;

while_stat
 : WHILE expr stat_block
 ;

log
 : LOG expr SCOL
 ;

expr
 : expr POW<assoc=right> expr           #powExpr
 | MINUS expr                           #unaryMinusExpr
 | NOT expr                             #notExpr
 | expr op=(MULT | DIV | MOD) expr      #multiplicationExpr
 | expr op=(PLUS | MINUS) expr          #additiveExpr
 | expr op=(LTEQ | GTEQ | LT | GT) expr #relationalExpr
 | expr op=(EQ | NEQ) expr              #equalityExpr
 | expr AND expr                        #andExpr
 | expr OR expr                         #orExpr
 | atom                                 #atomExpr
 ;

atom
 : OPAR expr CPAR #parExpr
 | (INT | FLOAT)  #numberAtom
 | (TRUE | FALSE) #booleanAtom
 | ID             #idAtom
 | STRING         #stringAtom
 | NIL            #nilAtom
 ;

OR : '||';
AND : '&&';
EQ : '==';
NEQ : '!=';
GT : '>';
LT : '<';
GTEQ : '>=';
LTEQ : '<=';
PLUS : '+';
MINUS : '-';
MULT : '*';
DIV : '/';
MOD : '%';
POW : '^';
NOT : '!';

SCOL : ';';
ASSIGN : '=';
OPAR : '(';
CPAR : ')';
OBRACE : '{';
CBRACE : '}';

TRUE : 'true';
FALSE : 'false';
NIL : 'nil';
IF : 'if';
ELSE : 'else';
WHILE : 'while';
LOG : 'log';

ID
 : [a-zA-Z_] [a-zA-Z_0-9]*
 ;

INT
 : [0-9]+
 ;

FLOAT
 : [0-9]+ '.' [0-9]* 
 | '.' [0-9]+
 ;

STRING
 : '"' (~["\r\n] | '""')* '"'
 ;

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

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

OTHER
 : . 
 ;

Supposons maintenant que vous souhaitiez analyser et évaluer des entrées comme ceci:

test.mu

a = true;
b = false;

if a && b {
  log "1 :: a=" + a +", b=" + b;
}
else if a || b {
  log "2 :: a=" + a +", b=" + b;
}
else {
  log "3 :: a=" + a +", b=" + b;
}

log "Done!";

2. Visiteur I

Commencez par générer les classes d'analyseur et de visiteur:

Java -cp antlr-4.0-complete.jar org.antlr.v4.Tool Mu.g4 -visitor

La commande ci-dessus aurait généré, entre autres, le fichier MuBaseVisitor<T>. C'est la classe que nous allons étendre sans notre propre logique:

EvalVisitor.Java

public class EvalVisitor extends MuBaseVisitor<Value> {
    // ...
}

Value est juste un wrapper pour tous les types de notre langage (String, Boolean, Double):

Value.Java

public class Value {

    public static Value VOID = new Value(new Object());

    final Object value;

    public Value(Object value) {
        this.value = value;
    }

    public Boolean asBoolean() {
        return (Boolean)value;
    }

    public Double asDouble() {
        return (Double)value;
    }

    public String asString() {
        return String.valueOf(value);
    }

    public boolean isDouble() {
        return value instanceof Double;
    }

    @Override
    public int hashCode() {

        if(value == null) {
            return 0;
        }

        return this.value.hashCode();
    }

    @Override
    public boolean equals(Object o) {

        if(value == o) {
            return true;
        }

        if(value == null || o == null || o.getClass() != value.getClass()) {
            return false;
        }

        Value that = (Value)o;

        return this.value.equals(that.value);
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

3. Test I

Pour tester les classes, utilisez la classe Main suivante:

Main.Java

import org.antlr.v4.runtime.ANTLRFileStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;

public class Main {
    public static void main(String[] args) throws Exception {
        MuLexer lexer = new MuLexer(new ANTLRFileStream("test.mu"));
        MuParser parser = new MuParser(new CommonTokenStream(lexer));
        ParseTree tree = parser.parse();
        EvalVisitor visitor = new EvalVisitor();
        visitor.visit(tree);
    }
}

et compiler et exécuter les fichiers source:

javac -cp antlr-4.0-complete.jar *.Java
java -cp .:antlr-4.0-complete.jar Main

(sous Windows, la dernière commande serait: Java -cp .;antlr-4.0-complete.jar Main)

Après avoir exécuté Main, rien ne se passe (bien sûr?). C'est parce que nous n'avons implémenté aucune des règles de notre classe EvalVisitor. Pour pouvoir évaluer le fichier test.mu correctement, nous devons fournir une implémentation appropriée pour les règles suivantes:

  • if_stat
  • andExpr
  • orExpr
  • plusExpr
  • assignment
  • idAtom
  • booleanAtom
  • stringAtom
  • log

4. Visiteur II et Test II

Voici une implémentation de ces règles:

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

import Java.util.HashMap;
import Java.util.List;
import Java.util.Map;

public class EvalVisitor extends MuBaseVisitor<Value> {

    // used to compare floating point numbers
    public static final double SMALL_VALUE = 0.00000000001;

    // store variables (there's only one global scope!)
    private Map<String, Value> memory = new HashMap<String, Value>();

    // assignment/id overrides
    @Override
    public Value visitAssignment(MuParser.AssignmentContext ctx) {
        String id = ctx.ID().getText();
        Value value = this.visit(ctx.expr());
        return memory.put(id, value);
    }

    @Override
    public Value visitIdAtom(MuParser.IdAtomContext ctx) {
        String id = ctx.getText();
        Value value = memory.get(id);
        if(value == null) {
            throw new RuntimeException("no such variable: " + id);
        }
        return value;
    }

    // atom overrides
    @Override
    public Value visitStringAtom(MuParser.StringAtomContext ctx) {
        String str = ctx.getText();
        // strip quotes
        str = str.substring(1, str.length() - 1).replace("\"\"", "\"");
        return new Value(str);
    }

    @Override
    public Value visitNumberAtom(MuParser.NumberAtomContext ctx) {
        return new Value(Double.valueOf(ctx.getText()));
    }

    @Override
    public Value visitBooleanAtom(MuParser.BooleanAtomContext ctx) {
        return new Value(Boolean.valueOf(ctx.getText()));
    }

    @Override
    public Value visitNilAtom(MuParser.NilAtomContext ctx) {
        return new Value(null);
    }

    // expr overrides
    @Override
    public Value visitParExpr(MuParser.ParExprContext ctx) {
        return this.visit(ctx.expr());
    }

    @Override
    public Value visitPowExpr(MuParser.PowExprContext ctx) {
        Value left = this.visit(ctx.expr(0));
        Value right = this.visit(ctx.expr(1));
        return new Value(Math.pow(left.asDouble(), right.asDouble()));
    }

    @Override
    public Value visitUnaryMinusExpr(MuParser.UnaryMinusExprContext ctx) {
        Value value = this.visit(ctx.expr());
        return new Value(-value.asDouble());
    }

    @Override
    public Value visitNotExpr(MuParser.NotExprContext ctx) {
        Value value = this.visit(ctx.expr());
        return new Value(!value.asBoolean());
    }

    @Override
    public Value visitMultiplicationExpr(@NotNull MuParser.MultiplicationExprContext ctx) {

        Value left = this.visit(ctx.expr(0));
        Value right = this.visit(ctx.expr(1));

        switch (ctx.op.getType()) {
            case MuParser.MULT:
                return new Value(left.asDouble() * right.asDouble());
            case MuParser.DIV:
                return new Value(left.asDouble() / right.asDouble());
            case MuParser.MOD:
                return new Value(left.asDouble() % right.asDouble());
            default:
                throw new RuntimeException("unknown operator: " + MuParser.tokenNames[ctx.op.getType()]);
        }
    }

    @Override
    public Value visitAdditiveExpr(@NotNull MuParser.AdditiveExprContext ctx) {

        Value left = this.visit(ctx.expr(0));
        Value right = this.visit(ctx.expr(1));

        switch (ctx.op.getType()) {
            case MuParser.PLUS:
                return left.isDouble() && right.isDouble() ?
                        new Value(left.asDouble() + right.asDouble()) :
                        new Value(left.asString() + right.asString());
            case MuParser.MINUS:
                return new Value(left.asDouble() - right.asDouble());
            default:
                throw new RuntimeException("unknown operator: " + MuParser.tokenNames[ctx.op.getType()]);
        }
    }

    @Override
    public Value visitRelationalExpr(@NotNull MuParser.RelationalExprContext ctx) {

        Value left = this.visit(ctx.expr(0));
        Value right = this.visit(ctx.expr(1));

        switch (ctx.op.getType()) {
            case MuParser.LT:
                return new Value(left.asDouble() < right.asDouble());
            case MuParser.LTEQ:
                return new Value(left.asDouble() <= right.asDouble());
            case MuParser.GT:
                return new Value(left.asDouble() > right.asDouble());
            case MuParser.GTEQ:
                return new Value(left.asDouble() >= right.asDouble());
            default:
                throw new RuntimeException("unknown operator: " + MuParser.tokenNames[ctx.op.getType()]);
        }
    }

    @Override
    public Value visitEqualityExpr(@NotNull MuParser.EqualityExprContext ctx) {

        Value left = this.visit(ctx.expr(0));
        Value right = this.visit(ctx.expr(1));

        switch (ctx.op.getType()) {
            case MuParser.EQ:
                return left.isDouble() && right.isDouble() ?
                        new Value(Math.abs(left.asDouble() - right.asDouble()) < SMALL_VALUE) :
                        new Value(left.equals(right));
            case MuParser.NEQ:
                return left.isDouble() && right.isDouble() ?
                        new Value(Math.abs(left.asDouble() - right.asDouble()) >= SMALL_VALUE) :
                        new Value(!left.equals(right));
            default:
                throw new RuntimeException("unknown operator: " + MuParser.tokenNames[ctx.op.getType()]);
        }
    }

    @Override
    public Value visitAndExpr(MuParser.AndExprContext ctx) {
        Value left = this.visit(ctx.expr(0));
        Value right = this.visit(ctx.expr(1));
        return new Value(left.asBoolean() && right.asBoolean());
    }

    @Override
    public Value visitOrExpr(MuParser.OrExprContext ctx) {
        Value left = this.visit(ctx.expr(0));
        Value right = this.visit(ctx.expr(1));
        return new Value(left.asBoolean() || right.asBoolean());
    }

    // log override
    @Override
    public Value visitLog(MuParser.LogContext ctx) {
        Value value = this.visit(ctx.expr());
        System.out.println(value);
        return value;
    }

    // if override
    @Override
    public Value visitIf_stat(MuParser.If_statContext ctx) {

        List<MuParser.Condition_blockContext> conditions =  ctx.condition_block();

        boolean evaluatedBlock = false;

        for(MuParser.Condition_blockContext condition : conditions) {

            Value evaluated = this.visit(condition.expr());

            if(evaluated.asBoolean()) {
                evaluatedBlock = true;
                // evaluate this block whose expr==true
                this.visit(condition.stat_block());
                break;
            }
        }

        if(!evaluatedBlock && ctx.stat_block() != null) {
            // evaluate the else-stat_block (if present == not null)
            this.visit(ctx.stat_block());
        }

        return Value.VOID;
    }

    // while override
    @Override
    public Value visitWhile_stat(MuParser.While_statContext ctx) {

        Value value = this.visit(ctx.expr());

        while(value.asBoolean()) {

            // evaluate the code block
            this.visit(ctx.stat_block());

            // evaluate the expression
            value = this.visit(ctx.expr());
        }

        return Value.VOID;
    }
}

Lorsque vous recompilez et exécutez Main, les éléments suivants sont imprimés sur votre console:

2 :: a=true, b=false
Done!

Pour une implémentation de toutes les autres règles, voir: https://github.com/bkiers/M

ÉDITER

De @pwwpche, dans les commentaires:

pour ceux qui utilisent jdk1.8 et rencontrent IndexOutOfBoundsException, antlr 4.0 n'est pas compatible avec jdk1.8. Téléchargez antlr-4.6-complete.jar et remplacez expr POW<assoc=right> expr avec <assoc=right>expr POW expr éliminera l'erreur et les avertissements.

97
Bart Kiers