Skip to content

Commit

Permalink
#427 Lazy operands for short-circuit evaluation of logic operators (#432
Browse files Browse the repository at this point in the history
)
  • Loading branch information
stevenylai authored Jan 16, 2024
1 parent 3f9a4e2 commit ad4ad69
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 21 deletions.
38 changes: 33 additions & 5 deletions docs/customization/custom_operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ Operation definition is made in two parts:

To ease some common implementation routines, a custom operator usually extends the
_AbstractOperator_ class.
As an example, we can look at the boolean "AND" operator:
As an example, we can look at the boolean "GREATER" operator:

```java
@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND)
public class InfixAndOperator extends AbstractOperator {
@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON)
public class InfixGreaterOperator extends AbstractOperator {

@Override
public EvaluationValue evaluate(
Expression expression, Token operatorToken, EvaluationValue... operands) {
return new EvaluationValue(operands[0].getBooleanValue() && operands[1].getBooleanValue());
Expression expression, Token operatorToken, EvaluationValue... operands) {
return expression.convertValue(operands[0].compareTo(operands[1]) > 0);
}
}
```
Expand Down Expand Up @@ -67,6 +67,34 @@ Precedence and associativity can be specified with the operator annotation.

There is a collection of predefined operator precedences in the _OperatorIfc_ interface.

#### Lazy Operands Evaluation

Infix operators can optionally be defined to allow lazy evaluation. Without lazy evaluation,
the value received by the operator would already have been evaluated.

Lazy evaluation can be helpful for certain situations where sometimes you may want to skip
part of the expression without affecting the result. One example is where you want to implement
[short-circuit evaluation](https://en.wikipedia.org/wiki/Short-circuit_evaluation). Consider an expression
"a != NULL && a > 0", in case "a" has a `NULL` value, the right side of the expression can be skipped
or an error will be thrown saying `NULL` is not comparable.

Note that currently only infix operators allow lazy evaluation. The "AND" operator demonstrates this:

```java
@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND, operandsLazy = true)
public class InfixAndOperator extends AbstractOperator {

@Override
public EvaluationValue evaluate(
Expression expression, Token operatorToken, EvaluationValue... operands)
throws EvaluationException {
return expression.convertValue(
expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue()
&& expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue());
}
}
```

### Adding the Operator

You can always add the operator directly to the operator dictionary, using the
Expand Down
26 changes: 18 additions & 8 deletions src/main/java/com/ezylang/evalex/Expression.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.ezylang.evalex.data.DataAccessorIfc;
import com.ezylang.evalex.data.EvaluationValue;
import com.ezylang.evalex.functions.FunctionIfc;
import com.ezylang.evalex.operators.OperatorIfc;
import com.ezylang.evalex.parser.*;
import java.math.BigDecimal;
import java.util.*;
Expand Down Expand Up @@ -122,14 +123,7 @@ public EvaluationValue evaluateSubtree(ASTNode startNode) throws EvaluationExcep
.evaluate(this, token, evaluateSubtree(startNode.getParameters().get(0)));
break;
case INFIX_OPERATOR:
result =
token
.getOperatorDefinition()
.evaluate(
this,
token,
evaluateSubtree(startNode.getParameters().get(0)),
evaluateSubtree(startNode.getParameters().get(1)));
result = evaluateInfixOperator(startNode, token);
break;
case ARRAY_INDEX:
result = evaluateArrayIndex(startNode);
Expand Down Expand Up @@ -212,6 +206,22 @@ private EvaluationValue evaluateStructureSeparator(ASTNode startNode) throws Eva
}
}

private EvaluationValue evaluateInfixOperator(ASTNode startNode, Token token)
throws EvaluationException {
EvaluationValue left;
EvaluationValue right;

OperatorIfc op = token.getOperatorDefinition();
if (op.isOperandLazy()) {
left = convertValue(startNode.getParameters().get(0));
right = convertValue(startNode.getParameters().get(1));
} else {
left = evaluateSubtree(startNode.getParameters().get(0));
right = evaluateSubtree(startNode.getParameters().get(1));
}
return op.evaluate(this, token, left, right);
}

/**
* Rounds the given value.
*
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/ezylang/evalex/operators/AbstractOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public abstract class AbstractOperator implements OperatorIfc {

private final boolean leftAssociative;

private final boolean operandsLazy;

OperatorType type;

/**
Expand All @@ -44,14 +46,17 @@ protected AbstractOperator() {
this.type = OperatorType.INFIX_OPERATOR;
this.precedence = infixAnnotation.precedence();
this.leftAssociative = infixAnnotation.leftAssociative();
this.operandsLazy = infixAnnotation.operandsLazy();
} else if (prefixAnnotation != null) {
this.type = PREFIX_OPERATOR;
this.precedence = prefixAnnotation.precedence();
this.leftAssociative = prefixAnnotation.leftAssociative();
this.operandsLazy = false;
} else if (postfixAnnotation != null) {
this.type = OperatorType.POSTFIX_OPERATOR;
this.precedence = postfixAnnotation.precedence();
this.leftAssociative = postfixAnnotation.leftAssociative();
this.operandsLazy = false;
} else {
throw new OperatorAnnotationNotFoundException(this.getClass().getName());
}
Expand All @@ -67,6 +72,11 @@ public boolean isLeftAssociative() {
return leftAssociative;
}

@Override
public boolean isOperandLazy() {
return operandsLazy;
}

@Override
public boolean isPrefix() {
return type == PREFIX_OPERATOR;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/ezylang/evalex/operators/InfixOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@

/** Operator associativity, defaults to <code>true</code>. */
boolean leftAssociative() default true;

/** Operands are evaluated lazily, defaults to <code>false</code>. */
boolean operandsLazy() default false;
}
7 changes: 7 additions & 0 deletions src/main/java/com/ezylang/evalex/operators/OperatorIfc.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ enum OperatorType {
*/
int getPrecedence(ExpressionConfiguration configuration);

/**
* Checks if the operand is lazy.
*
* @return <code>true</code> if operands are defined as lazy.
*/
boolean isOperandLazy();

/**
* Performs the operator logic and returns an evaluation result.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@

import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_AND;

import com.ezylang.evalex.EvaluationException;
import com.ezylang.evalex.Expression;
import com.ezylang.evalex.data.EvaluationValue;
import com.ezylang.evalex.operators.AbstractOperator;
import com.ezylang.evalex.operators.InfixOperator;
import com.ezylang.evalex.parser.Token;

/** Boolean AND of two values. */
@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND)
@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND, operandsLazy = true)
public class InfixAndOperator extends AbstractOperator {

@Override
public EvaluationValue evaluate(
Expression expression, Token operatorToken, EvaluationValue... operands) {
return expression.convertValue(operands[0].getBooleanValue() && operands[1].getBooleanValue());
Expression expression, Token operatorToken, EvaluationValue... operands)
throws EvaluationException {
return expression.convertValue(
expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue()
&& expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@

import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_OR;

import com.ezylang.evalex.EvaluationException;
import com.ezylang.evalex.Expression;
import com.ezylang.evalex.data.EvaluationValue;
import com.ezylang.evalex.operators.AbstractOperator;
import com.ezylang.evalex.operators.InfixOperator;
import com.ezylang.evalex.parser.Token;

/** Boolean OR of two values. */
@InfixOperator(precedence = OPERATOR_PRECEDENCE_OR)
@InfixOperator(precedence = OPERATOR_PRECEDENCE_OR, operandsLazy = true)
public class InfixOrOperator extends AbstractOperator {

@Override
public EvaluationValue evaluate(
Expression expression, Token operatorToken, EvaluationValue... operands) {
return expression.convertValue(operands[0].getBooleanValue() || operands[1].getBooleanValue());
Expression expression, Token operatorToken, EvaluationValue... operands)
throws EvaluationException {
return expression.convertValue(
expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue()
|| expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ class InfixAndOperatorTest extends BaseEvaluationTest {
"\"true\"&&\"false\" : false",
"\"false\"&&\"false\" : false",
"(1==1)&&(2==2) : true",
"(5>4)&&(4<6) :true"
"(5>4)&&(4<6) :true",
"false && NULL < 0 : false"
})
void testInfixLessLiterals(String expression, String expectedResult)
throws EvaluationException, ParseException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class InfixOrOperatorTest extends BaseEvaluationTest {
"\"true\"||\"false\" : true",
"\"false\"||\"false\" : false",
"(1==1)||(2==3) : true",
"(2>4)||(4<6) :true"
"(2>4)||(4<6) :true",
"true || NULL < 0 : true"
})
void testInfixLessLiterals(String expression, String expectedResult)
throws EvaluationException, ParseException {
Expand Down

0 comments on commit ad4ad69

Please sign in to comment.