code: @Nullable
    public static SoyType getSoyTypeForBinaryOperator(
        SoyType t0, SoyType t1, SoyTypeBinaryOperator operator) {
      if (t0.getKind() == Kind.ERROR || t1.getKind() == Kind.ERROR) {
        return ErrorType.getInstance();
      }
      // If both types are nullable, we will make the result nullable as well.
      // If only one of these input types is nullable, we don't. For example, {int} and {int|null}
      // probably should return {int} instead of {int|null}.
      boolean isNullable = isNullable(t0) && isNullable(t1);
      // TODO(b/64098780): Make arithmetic operations on nullable type consistent.
      // For now, we remove nulltype from the union type.
      SoyType left = tryRemoveNull(t0);
      SoyType right = tryRemoveNull(t1);
      if (left.getKind() == SoyType.Kind.UNION) {
        return getSoyTypeFromUnionForBinaryOperator((UnionType) left, right, isNullable, operator);
      }
      if (right.getKind() == SoyType.Kind.UNION) {
        // When we calculate the return type of a binary operator, it should always be commutative so
        // the order should not matter.
        return getSoyTypeFromUnionForBinaryOperator((UnionType) right, left, isNullable, operator);
      }
      SoyType result = operator.resolve(left, right);
      if (result == null) {
        return null;
      }
      return isNullable ? makeNullable(result) : result;
    }
docs: Returns the {@code SoyType} that should be the result of a binary operator.
  
  <p>This method is mostly for handling {@code UnionType} instances. For primitive soy types, the
  logic should be defined in the {@code SoyTypeBinaryOperator}. Recursion is used for collecting
  all possible combinations if any of the operands is {@code UnionType}. This method will return
  null if any of the combinations is incompatible (the operator returns null).