Вопрос-ответ

If/else statements in ANTLR using listeners

Операторы If / else в ANTLR с использованием прослушивателей

Я создаю простой язык программирования для школьного проекта. Я использую ANTLR 4 для генерации лексера и синтаксического анализатора на основе моей грамматики. До сих пор я использовал шаблон прослушивателя ANTLRs для применения реальной функциональности языка программирования.

Теперь я хотел бы реализовать операторы if / else, но я не уверен, что они действительно могут быть реализованы при использовании шаблона прослушивателя, поскольку ANTLR решает, в каком порядке проходить дерево синтаксического анализа при использовании прослушивателей, и я полагаю, что реализация операторов if / else потребует перехода по дереву синтаксического анализа в зависимости от того, какое условие в инструкции выполнено.

Кто-нибудь может сказать мне, возможно ли будет реализовать инструкции if / else с использованием ANTLR или мне придется самому реализовать шаблон visitor? Также, кто-нибудь может привести чрезвычайно простой пример реализации инструкций?

Переведено автоматически
Ответ 1

По умолчанию ANTLR 4 генерирует прослушиватели. Но если вы зададите org.antlr.v4.Tool параметр командной строки -visitor, ANTLR сгенерирует классы visitor для вас. Они работают во многом как прослушиватели, но дают вам больше контроля над тем, по каким (вспомогательным) деревьям совершается обход / посещение. Это особенно полезно, если вы хотите исключить определенные (под) деревья (например, блоки else / if, как в вашем случае). Хотя это можно сделать с помощью прослушивателей, гораздо чище делать это с посетителем. Используя прослушиватели, вам нужно будет ввести глобальные переменные, которые отслеживают, нужно ли вычислять (под) дерево, а которые нет.

Так получилось, что я работаю над небольшим руководством по ANTLR 4. Это еще не сделано, но я опубликую небольшой рабочий пример, демонстрирующий использование этих классов visitor и if конструкции оператора.


1. Грамматика

Вот простая грамматика, поддерживающая базовые выражения, if-, while- и 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
: .
;

Теперь предположим, что вы хотели бы проанализировать и оценить входные данные следующим образом:

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. Посетитель, которого я

Начните с создания классов parser и visitor:

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

Приведенная выше команда сгенерировала бы, среди прочего, файл MuBaseVisitor<T>. Это класс, который мы собираемся расширить с помощью нашей собственной логики:

EvalVisitor.java

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

где Value это просто оболочка для любого из типов нашего языка (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() != this.getClass()) {
return false;
}

Value that = (Value)o;

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

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

3. Тест I

Для тестирования классов используйте следующий Main класс:

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

а также скомпилировать и запустить исходные файлы:

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

(в Windows последней командой будет: java -cp .;antlr-4.0-complete.jar Main)

После запуска Main ничего не происходит (конечно?). Это потому, что мы не реализовали ни одно из правил в нашем EvalVisitor классе. Чтобы иметь возможность правильно оценить файл test.mu, нам необходимо обеспечить надлежащую реализацию следующих правил:


  • if_stat

  • andExpr

  • orExpr

  • plusExpr

  • assignment

  • idAtom

  • booleanAtom

  • stringAtom

  • log


# 4. Посетитель II и тест II

Вот реализация этих правил:

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

При повторной компиляции и запуске Main на вашей консоли будет выведено следующее:

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

Для реализации всех других правил смотрите: https://github.com/bkiers/Mu

Редактировать

От @pwwpche, в комментариях:


для тех, кто использует jdk1.8 и encounter IndexOutOfBoundsException, antlr 4.0 каким-то образом несовместим с jdk1.8. Загрузите antlr-4.6-complete.jar, и замена expr POW<assoc=right> expr на <assoc=right>expr POW expr устранит ошибку и предупреждения.


java