docs: def evalop(op,left,right):
    "this takes evaluated left and right (i.e. values not expressions)"
    if op in ('=','!=','>','<'): return threevl.ThreeVL.compare(op,left,right)
    elif op in ('+','-','*','/'): # todo: does arithmetic require threevl?
      if op=='/': raise NotImplementedError('todo: spec about int/float division')
      return (left + right) if op=='+' else (left - right) if op=='-' else (left * right) if op=='*' else (left/right)
    elif op=='in': return (tuple(left) in right) if isinstance(left,list) and isinstance(right[0],tuple) else (left in right)
    elif op in ('and','or'): return threevl.ThreeVL.andor(op,left,right)
    elif op in ('is not','is'):
      if right is not None: raise NotImplementedError('can null be on either side? what if neither value is null?')
      return (left is not None) if (op=='is not') else (left is None)
    elif op=='@>':
      # todo: support a TextSearchDoc that will overload a lot of these operators
      if not all(isinstance(x,list) for x in (left,right)): raise TypeError('non-array-args',op,left,right)
      return set(left)>=set(right)
    elif op=='||':
      if not all(isinstance(x,list) for x in (left,right)): raise TypeError('non-array-args',op,left,right)
      return left+right
    elif op=='@@':
      if not all(isinstance(x,set) for x in (left,right)): raise TypeError('non_set_args',op,type(left),type(right))
      return bool(left&right)
    else: raise NotImplementedError(op,left,right)