Reputation: 834
In our software we can click together a query to select a subset of elements. In memory, the finished query is a construct of lists and objects (CLO) representing filter criteria. Now I am about to implement a query language to create the same thing by parsing it with ANTLR.
The language has to cover only simple expressions and their logical connections 'AND' and 'OR'.
name LIKE 'foo'
name LIKE 'bar' AND size > 42 OR comment LIKE 'ignore'
The words 'name', 'size' and 'comment' are attributes. Every attribute connected by 'AND' will be an object A in a list. Every attribute connected with 'OR' will be an object B and goes into a list of A.
My grammar (excerpts):
expr
: expr AND expr # andExpr
| expr OR expr # orExpr
| IDENTIFIER numOp INT # numExpr
| IDENTIFIER strOp STR # strExpr
;
numOp : (GT | LT | EQ);
strOp : (EQ | LIKE);
Using the visitor pattern with ANTLR, I override the methods for numExpr and strExpr and return the A object. But I also need to connect those object in respect to the logical operators 'AND'|'OR'. When I override the logicExpr method and call the visit() method, both logical operators are evaluated first, then the basic expressions in order. This feels a bit awkward as the expressions are rather separated from the logical operators.
OR
AND
name
size
comment
Q: How can I process the respective expression based on the logical operator that came before it? What would be a good approach?
Q: Furthermore, is the logicExpr statement at the correct position in the grammar rule 'expr' or should the basic expressions come first?
Upvotes: 1
Views: 567
Reputation: 170308
I'd just go for a visitor that evaluates things on the fly (not create custom objects and store those). A quick demo of such a visitor would look like this:
public class EvalVisitor extends ExprBaseVisitor<Object> {
private final Map<String, Object> attributes;
public EvalVisitor(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public Boolean visitEval(ExprParser.EvalContext ctx) {
return (Boolean) visit(ctx.expr());
}
@Override
public Boolean visitStrExpr(ExprParser.StrExprContext ctx) {
String lhs = this.attributes.get(ctx.IDENTIFIER().getText()).toString();
String rhs = ctx.STR().getText().substring(1, ctx.STR().getText().length() - 1);
int op = ctx.strOp().start.getType();
switch (op) {
case ExprLexer.EQ:
return lhs.equals(rhs);
case ExprLexer.LIKE:
return lhs.toLowerCase().contains(rhs.toLowerCase());
default:
throw new RuntimeException("unknown operator: " + ctx.strOp().getText());
}
}
@Override
public Object visitAndExpr(ExprParser.AndExprContext ctx) {
Boolean lhs = (Boolean) this.visit(ctx.expr(0));
Boolean rhs = (Boolean) this.visit(ctx.expr(1));
return lhs && rhs;
}
@Override
public Object visitOrExpr(ExprParser.OrExprContext ctx) {
Boolean lhs = (Boolean) this.visit(ctx.expr(0));
Boolean rhs = (Boolean) this.visit(ctx.expr(1));
return lhs || rhs;
}
@Override
public Boolean visitNumExpr(ExprParser.NumExprContext ctx) {
Integer lhs = (Integer) this.attributes.get(ctx.IDENTIFIER().getText());
Integer rhs = Integer.valueOf(ctx.INT().getText());
int op = ctx.numOp().start.getType();
switch (op) {
case ExprLexer.GT:
return lhs > rhs;
case ExprLexer.LT:
return lhs < rhs;
case ExprLexer.EQ:
return lhs.equals(rhs);
default:
throw new RuntimeException("unknown operator: " + ctx.numOp().getText());
}
}
}
which can be tested like this:
String source = "name LIKE 'bar' AND size > 42 OR comment LIKE 'ignore'";
ExprLexer lexer = new ExprLexer(CharStreams.fromString(source));
ExprParser parser = new ExprParser(new CommonTokenStream(lexer));
Map<String, Object> attributes = new HashMap<>();
attributes.put("name", "Barista");
attributes.put("size", 40);
attributes.put("comment", "Ignore this please");
Object result = new EvalVisitor(attributes).visit(parser.eval());
System.out.printf("source:\n %s\n\nresult:\n %s\n", source, result);
resulting int:
source:
name LIKE 'bar' AND size > 42 OR comment LIKE 'ignore'
result:
true
Upvotes: 3
Reputation: 6785
I think you have a bit of a misconception about when things are happening.
When you parse your input, a parse tree is constructed as a result of evaluating your parse rules.
You can then use visitors or listeners as convenience classes that help you to navigate the parse tree you got back from the parse.
It might help, if you set up the "grun" ability outlined in the ANTLR book, and then use the "-gui" option to get a visual presentation of the parse tree that the visitor or listener will process.
Often, the listener interface is easier to use, as it will navigate the parse tree for you and just call back to your methods as it enters or exits a node (you don't do any code to handle navigation.
The visitor interface works at a bit lower level, but you have to handle the navigation yourself.
It's a bit hard to know how to respond to your question until you have this understanding down, and I suspect it may change the way you phrase your question (if you still have a question)
Upvotes: 0