XOR for FreshMarker

“Today was good. Today was fun. Tomorrow is another one.”

Dr. Seuss

While reviewing the mutation tests for FreshMarker, I noticed an implementation gap in the logical expression. So if something needs to be changed, why not quickly add something new.

In the case of logical expressions, the operators & and && exist for conjunctions and the operators | and || for disjunctions. The difference between & and &&, or | and || is the short-circuit evaluation for && and ||. If the left operand can determine the result, the right operand is not evaluated. The advantage of short-circuit evaluation is the processing speed, because complicated expressions do not have to be fully evaluated.

In addition to the above operators, the XOR operator ^ should now also find a home in FreshMarker. The XOR operator is an exclusive-or operation. The following truth table shows the possible combinations of its operands and the result.

XYX \oplus Y
truetruefalse
truefalsetrue
falsetruetrue
falsefalsefalse

The result of the exclusive-or operation is therefore only ever true if both operands are different. There is also another logical expression with this truth table X\neq Y. But a new logical operator is more interesting.

For the XOR operator to be used, it must be included in the FreshMarker grammar. To keep things simple, the XOR operator in FreshMarker has the same precedence as the OR operators. In Java, the XOR has a slightly higher precedence.

OrExpression :
    AndExpression
    (
        (<OR>|<OR2>|<XOR>) AndExpression
    )*
;

Since all disjunctions and conjunctions in FreshMarker are processed via the TemplateJunction class, the processing of XOR expressions is added.

public class TemplateJunction implements TemplateBooleanExpression {
  private final TokenType type;
  private final TemplateObject left;
  private final TemplateObject right;

  public TemplateJunction(TokenType type, TemplateObject left, TemplateObject right) {
    this.type = type;
    this.left = left;
    this.right = right;
  }

  @Override
  public TemplateObject evaluateToObject(ProcessContext context) {
    TemplateBoolean leftValue = left.evaluate(context, TemplateBoolean.class);
    return TemplateBoolean.from(switch (type) {
      case AND -> leftValue.getValue() & right.evaluate(context, TemplateBoolean.class).getValue();
      case AND2 -> leftValue.getValue() && right.evaluate(context, TemplateBoolean.class).getValue();
      case OR -> leftValue.getValue() | right.evaluate(context, TemplateBoolean.class).getValue();
      case OR2 -> leftValue.getValue() || right.evaluate(context, TemplateBoolean.class).getValue();
      case XOR -> leftValue.getValue() ^ right.evaluate(context, TemplateBoolean.class).getValue();
      default -> throw new IllegalArgumentException("unsupported junction: " + type);
    });
  }

  @Override
  public TemplateObject not() {
    TemplateObject newLeft = left instanceof TemplateBooleanExpression leftExpression ? leftExpression.not() : left;
    return switch (type) {
      case AND -> new TemplateJunction(TokenType.OR, newLeft, right instanceof TemplateBooleanExpression r ? r.not() : right);
      case OR -> new TemplateJunction(TokenType.AND, newLeft, right instanceof TemplateBooleanExpression r ? r.not() : right);
      case AND2 -> new TemplateJunction(TokenType.OR2, newLeft, new TemplateNegative(right));
      case OR2 -> new TemplateJunction(TokenType.AND2, newLeft, new TemplateNegative(right));
      case XOR -> new TemplateNegative(this);
      default -> throw new IllegalArgumentException("unsupported junction: " + type);
    };
  }
}

In the evaluateToObject method, the value of the left operand is linked to the value of the right operand with ^ for the XOR case. The not method also receives an additional XOR case in which the current TemplateJunctor instance is encapsulated in a TemplateNegative instance.

The evaluation of the conjunctions in the InterpolationBuilder was kept simple in the first step. Incidentally, this was also the problem with the logical expressions. The implementation was missing in previous versions.

@Override
public TemplateObject visit(AndExpression expression, Object input) {
  TemplateObject left = expression.getChild(0).accept(this, null);
  TemplateObject right = expression.getChild(2).accept(this, null);
  return new TemplateJunction(((Token) expression.getChild(1)).getType(), left, right);
}

The two operands of the conjunction are evaluated and saved in the variables left and right. A TemplateJunction instance is then created with the two operands and the current token AND or AND2. However, some conjunctions with constants can be optimized.

For this purpose, the treatment is split into separate methods for better readability.

public TemplateObject visit(AndExpression expression, Object input) {
  return switch (((Token) expression.getChild(1)).getType()) {
    case AND -> handleAnd(expression);
    case AND2 -> handleAnd2(expression);
    default -> throw new IllegalArgumentException("invalid conjunction");
  };
}

The handleAnd method is available for handling the simple conjunction |.

private TemplateObject handleAnd(AndExpression expression) {
  TemplateObject left = expression.getChild(0).accept(this, null);
  TemplateObject right = expression.getChild(2).accept(this, null);
  if (!(right instanceof TemplateBoolean r)) {
    return new TemplateJunction(TokenType.AND, left, right);
  }
  if (left instanceof TemplateBoolean l) {
    return TemplateBoolean.from(l.getValue() & r.getValue());
  }
  if (TemplateBoolean.TRUE.equals(right)) {
    return left;
  }
  return new TemplateJunction(TokenType.AND, left, right);
}

If the right operand is not a constant, a TemplateJunction instance is created as before. If both operands are constants, a corresponding calculated value is returned instead of a TemplateJunction. Now there is the possibility that only the right or the left operand is a constant. In this constellation, there is still the optimization that the evaluation of the right operand can be omitted if it has the value true.

Theoretically, the evaluation of the right operand could be omitted if the left operand is the constant true. However, the | operator requires both operands to be evaluated, so no further optimization is performed here.

The handleAnd2 method is available for handling the short-circuit conjunction &&.

private TemplateObject handleAnd2(AndExpression expression) {
  TemplateObject left = expression.getChild(0).accept(this, null);
  TemplateObject right = expression.getChild(2).accept(this, null);
  if (TemplateBoolean.TRUE.equals(right)) {
    return left;
  }
  if (TemplateBoolean.TRUE.equals(left)) {
    return right;
  }
  if ((TemplateBoolean.FALSE.equals(left) || TemplateBoolean.FALSE.equals(right))) {
    return TemplateBoolean.FALSE;
  }
  return new TemplateJunction(TokenType.AND2, left, right);
}

This works slightly differently to the handleAnd method, as both operators do not always have to be evaluated. If one of the two operands is the constant true, the other operator is returned instead of a TemplateJunction instance. If one of the two operators is the constant false, the result is the constant FALSE. In the remaining cases, a TemplateJunction instance is created.

Similar optimizations will also be made for the disjunctions. However, in order not to make the article unnecessarily long, these will not be discussed here.

After the shortcomings of the first implementation have been eliminated, some optimizations have been made and the boolean XOR operator has been implemented, the latest version of FreshMarker can be downloaded from Maven Central.

<dependency>
    <groupId>de.schegge</groupId>
    <artifactId>freshmarker</artifactId>
    <version>0.5.7</version>
</dependency>

Leave a Comment