code: private List<GroovyExpression> processOtherExpression(GroovyExpression source, OptimizationContext context) {
          UpdatedExpressions updatedChildren = getUpdatedChildren(source, context);
          if (!updatedChildren.hasChanges()) {
              return Collections.singletonList(source);
          }
          List<GroovyExpression> result = new ArrayList<GroovyExpression>();
  
          //The updated children list we get back has the possible values for each child
          //in the expression.  We compute a cartesian product to get all possible
          //combinations of child values.
          List<List<GroovyExpression>> updateChildLists = Lists.cartesianProduct(updatedChildren.getUpdatedChildren());
  
          for (List<GroovyExpression> updatedChildList : updateChildLists) {
              result.add(source.copy(updatedChildList));
          }
          return result;
      }
docs: This is called when we encounter an expression that is not an "or", for example an "and" expressio.  For these
  expressions, we process the children and create copies with the cartesian product of the updated
  arguments.
  
  Example:
  
  g.V().and(or(has('x),has('y'), or(has('a'),has('b')))
  
  Here, we have an "and" expression with two children:
  
  1) or(has('x),has('y')
  2) or(has('a'),has('b'))
  
  We first process these children.  They each yield 2 expressions:
  
  1 -> [ has('x'), has('y') ]
  2 -> [ has('a'), has('b') ]
  
  The cartesian product of these gives this:
  
  [ has('x'), has('a') ]
  [ has('x'), has('b') ]
  [ has('y'), has('a') ]
  [ has('y'), has('b') ]
  
  So the overall result is:
  
  g.V().and(has('x'), has('a'))
  g.V().and(has('x'), has('b'))
  g.V().and(has('y'), has('a'))
  g.V().and(has('y'), has('b'))
  
  
  @param source
  @param context
  @return expressions that should be unioned together to get the query result