Шаблон посетителей ANTLR4 на простом арифметическом примере
Я полный новичок ANTLR4, поэтому, пожалуйста, прости мое невежество. Я наткнулся на эту презентацию, где определена очень простая грамматика арифметического выражения. Это выглядит как:
grammar Expressions;
start : expr ;
expr : left=expr op=('*'|'/') right=expr #opExpr
| left=expr op=('+'|'-') right=expr #opExpr
| atom=INT #atomExpr
;
INT : ('0'..'9')+ ;
WS : [ \t\r\n]+ -> skip ;
Это здорово, потому что оно генерирует очень простое двоичное дерево, которое можно просмотреть с помощью шаблона посетителя, как описано на слайдах, например, вот функция, которая посещает expr
:
public Integer visitOpExpr(OpExprContext ctx) {
int left = visit(ctx.left);
int right = visit(ctx.right);
String op = ctx.op.getText();
switch (op.charAt(0)) {
case '*': return left * right;
case '/': return left / right;
case '+': return left + right;
case '-': return left - right;
default: throw new IllegalArgumentException("Unkown opeator " + op);
}
}
Следующее, что я хотел бы добавить, это поддержка скобок. Поэтому я изменил expr
следующее:
expr : '(' expr ')' #opExpr
| left=expr op=('*'|'/') right=expr #opExpr
| left=expr op=('+'|'-') right=expr #opExpr
| atom=INT #atomExpr
;
К сожалению, приведенный выше код не работает, потому что при обращении к скобкам три атрибута op
,left
а также right
являются нулевыми (терпит неудачу с NPE).
Я думаю, что я мог бы обойти это, определив новый атрибут, например, parenthesized='(' expr ')'
, а затем разобраться с этим в коде посетителя. Однако мне кажется излишним иметь целый дополнительный тип узла для представления выражения в скобках. Более простое, но уродливое решение - добавить следующую строку кода в начале visitOpExpr
метод:
if (ctx.op == null) return visit(ctx.getChild(1)); // 0 and 2 are the parentheses!
Мне совсем не нравится вышесказанное, потому что оно очень хрупкое и сильно зависит от грамматической структуры.
Мне интересно, есть ли способ заставить ANTLR просто "съесть" скобки и относиться к выражению как к ребенку. Есть? Есть лучший способ сделать это?
Примечание. Моя конечная цель - расширить пример, включив в него логические выражения, которые сами могут содержать арифметические выражения, например: (2+4*3)/10 >= 11
то есть отношение (<,>, ==, ~ = и т. д.) между арифметическими выражениями может определять атомное логическое выражение. Это просто, и у меня уже есть набросок грамматики, но у меня та же проблема с круглыми скобками, т.е. мне нужно иметь возможность писать что-то вроде (я также добавлю поддержку переменных):
((2+4*x)/10 >= 11) | ( x>1 & x<3 )
РЕДАКТИРОВАТЬ: Исправлен приоритет выражения в скобках, скобки всегда имеют более высокий приоритет.
2 ответа
Конечно, просто обозначьте это по-другому. Ведь альтернатива '(' expr ')'
не является #opExpr
:
expr : left=expr op=('*'|'/') right=expr #opExpr
| left=expr op=('+'|'-') right=expr #opExpr
| '(' expr ')' #parenExpr
| atom=INT #atomExpr
;
И в вашем посетителе вы бы сделали что-то вроде этого:
public class EvalVisitor extends ExpressionsBaseVisitor<Integer> {
@Override
public Integer visitOpExpr(@NotNull ExpressionsParser.OpExprContext ctx) {
int left = visit(ctx.left);
int right = visit(ctx.right);
String op = ctx.op.getText();
switch (op.charAt(0)) {
case '*': return left * right;
case '/': return left / right;
case '+': return left + right;
case '-': return left - right;
default: throw new IllegalArgumentException("Unknown operator " + op);
}
}
@Override
public Integer visitStart(@NotNull ExpressionsParser.StartContext ctx) {
return this.visit(ctx.expr());
}
@Override
public Integer visitAtomExpr(@NotNull ExpressionsParser.AtomExprContext ctx) {
return Integer.valueOf(ctx.getText());
}
@Override
public Integer visitParenExpr(@NotNull ExpressionsParser.ParenExprContext ctx) {
return this.visit(ctx.expr());
}
public static void main(String[] args) {
String expression = "2 * (3 + 4)";
ExpressionsLexer lexer = new ExpressionsLexer(new ANTLRInputStream(expression));
ExpressionsParser parser = new ExpressionsParser(new CommonTokenStream(lexer));
ParseTree tree = parser.start();
Integer answer = new EvalVisitor().visit(tree);
System.out.printf("%s = %s\n", expression, answer);
}
}
Если вы запустите класс выше, вы увидите следующий вывод:
2 * (3 + 4) = 14
Я перенес выше на Python Visitor и даже Python Listener
Слушатель Python
from antlr4 import *
from arithmeticLexer import arithmeticLexer
from arithmeticListener import arithmeticListener
from arithmeticParser import arithmeticParser
import sys
## grammar arithmetic;
##
## start : expr ;
##
## expr : left=expr op=('*'|'/') right=expr #opExpr
## | left=expr op=('+'|'-') right=expr #opExpr
## | '(' expr ')' #parenExpr
## | atom=INT #atomExpr
## ;
##
## INT : ('0'..'9')+ ;
##
## WS : [ \t\r\n]+ -> skip ;
import codecs
import sys
def dump(obj):
for attr in dir(obj):
print("obj.%s = %r" % (attr, getattr(obj, attr)))
def is_number(s):
try:
float(s)
return True
except ValueError:
return False
class arithmeticPrintListener(arithmeticListener):
def __init__(self):
self.stack = []
# Exit a parse tree produced by arithmeticParser#opExpr.
def exitOpExpr(self, ctx:arithmeticParser.OpExprContext):
print('exitOpExpr INP',ctx.op.text,ctx.left.getText(),ctx.right.getText())
op = ctx.op.text
opchar1=op[0]
right= self.stack.pop()
left= self.stack.pop()
if opchar1 == '*':
val = left * right
elif opchar1 == '/':
val = left / right
elif opchar1 == '+':
val = left + right
elif opchar1 == '-':
val = left - right
else:
raise ValueError("Unknown operator " + op)
print("exitOpExpr OUT",opchar1,left,right,val)
self.stack.append(val)
# Exit a parse tree produced by arithmeticParser#atomExpr.
def exitAtomExpr(self, ctx:arithmeticParser.AtomExprContext):
val=int(ctx.getText())
print('exitAtomExpr',val)
self.stack.append(val)
def main():
#lexer = arithmeticLexer(StdinStream())
expression = "(( 4 - 10 ) * ( 3 + 4 )) / (( 2 - 5 ) * ( 3 + 4 ))"
lexer = arithmeticLexer(InputStream(expression))
stream = CommonTokenStream(lexer)
parser = arithmeticParser(stream)
tree = parser.start()
printer = arithmeticPrintListener()
walker = ParseTreeWalker()
walker.walk(printer, tree)
if __name__ == '__main__':
main()
Посетитель Python
from antlr4 import *
from arithmeticLexer import arithmeticLexer
from arithmeticVisitor import arithmeticVisitor
from arithmeticParser import arithmeticParser
import sys
from pprint import pprint
## grammar arithmetic;
##
## start : expr ;
##
## expr : left=expr op=('*'|'/') right=expr #opExpr
## | left=expr op=('+'|'-') right=expr #opExpr
## | '(' expr ')' #parenExpr
## | atom=INT #atomExpr
## ;
##
## INT : ('0'..'9')+ ;
##
## WS : [ \t\r\n]+ -> skip ;
import codecs
import sys
class EvalVisitor(arithmeticVisitor):
def visitOpExpr(self, ctx):
#print("visitOpExpr",ctx.getText())
left = self.visit(ctx.left)
right = self.visit(ctx.right)
op = ctx.op.text;
# for attr in dir(ctx.op): ########### BEST
# print("ctx.op.%s = %r" % (attr, getattr(ctx.op, attr)))
#print("visitOpExpr",dir(ctx.op),left,right)
opchar1=op[0]
if opchar1 == '*':
val = left * right
elif opchar1 == '/':
val = left / right
elif opchar1 == '+':
val = left + right
elif opchar1 == '-':
val = left - right
else:
raise ValueError("Unknown operator " + op)
print("visitOpExpr",opchar1,left,right,val)
return val
def visitStart(self, ctx):
print("visitStart",ctx.getText())
return self.visit(ctx.expr())
def visitAtomExpr(self, ctx):
print("visitAtomExpr",int(ctx.getText()))
return int(ctx.getText())
def visitParenExpr(self, ctx):
print("visitParenExpr",ctx.getText())
return self.visit(ctx.expr())
def main():
#lexer = arithmeticLexer(StdinStream())
expression = "(( 4 - 10 ) * ( 3 + 4 )) / (( 2 - 5 ) * ( 3 + 4 ))"
lexer = arithmeticLexer(InputStream(expression))
stream = CommonTokenStream(lexer)
parser = arithmeticParser(stream)
tree = parser.start()
answer = EvalVisitor().visit(tree)
print(answer)
if __name__ == '__main__':
main()