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?
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
.
Voici une grammaire simple prenant en charge les expressions de base, les instructions if
-, while
- et log
-:
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:
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!";
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:
public class EvalVisitor extends MuBaseVisitor<Value> {
// ...
}
où Value
est juste un wrapper pour tous les types de notre langage (String
, Boolean
, Double
):
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);
}
}
Pour tester les classes, utilisez la classe Main
suivante:
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
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
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 remplacezexpr POW<assoc=right> expr
avec<assoc=right>expr POW expr
éliminera l'erreur et les avertissements.