@Override
  public BasicValue binaryOperation(AbstractInsnNode insn, BasicValue value1, BasicValue value2)
      throws AnalyzerException {
    switch (insn.getOpcode()) {
      case PUTFIELD: // put value2 into value1
        // skip untagged values
        if (!isTagged(value1)) {
          return null;
        }

        final TaggedValue taggedValue = (TaggedValue) value1;
        final FieldInsnNode field = (FieldInsnNode) insn;
        final boolean value2HasInputDependency = hasImportantDependencies(value2);

        // if value1 is not an input, make value1 a container and add value2 to it
        // PUTFIELD on inputs is not allowed
        if (!taggedValue.isInput() && value2HasInputDependency) {
          if (!taggedValue.canContainFields()) {
            taggedValue.setTag(Tag.CONTAINER);
          }
          taggedValue.addContainerMapping(field.name, tagged(value2), currentFrame);
        }
        // if value1 is filled with non-input, make it container and mark the field
        // PUTFIELD on inputs is not allowed
        else if (!taggedValue.isInput() && !value2HasInputDependency) {
          if (!taggedValue.canContainFields()) {
            taggedValue.setTag(Tag.CONTAINER);
          }
          taggedValue.addContainerMapping(field.name, null, currentFrame);
        }
        // PUTFIELD on input leads to input modification
        // make input regular
        else if (taggedValue.isInput()) {
          taggedValue.makeRegular();
        }
        return null;
      default:
        return super.binaryOperation(insn, value1, value2);
    }
  }
  @SuppressWarnings({"rawtypes", "unchecked"})
  @Override
  public BasicValue naryOperation(AbstractInsnNode insn, List rawValues) throws AnalyzerException {
    final List<BasicValue> values = (List<BasicValue>) rawValues;
    boolean isStatic = false;
    switch (insn.getOpcode()) {
      case INVOKESTATIC:
        isStatic = true;
      case INVOKESPECIAL:
      case INVOKEVIRTUAL:
      case INVOKEINTERFACE:
        final MethodInsnNode method = (MethodInsnNode) insn;
        String methodOwner = method.owner;

        // special case: in case that class is extending tuple we need to find the class
        // that contains the actual implementation to determine the tuple size
        if (method.name.equals("getField") || method.name.equals("setField")) {
          try {
            final String newMethodOwner =
                (String) findMethodNode(methodOwner, method.name, method.desc)[1];
            if (newMethodOwner.startsWith("org/apache/flink/api/java/tuple/Tuple")) {
              methodOwner = newMethodOwner;
            }
          } catch (IllegalStateException e) {
            // proceed with the known method owner
          }
        }

        // special case: collect method of Collector
        if (method.name.equals("collect")
            && methodOwner.equals("org/apache/flink/util/Collector")
            && isTagged(values.get(0))
            && tagged(values.get(0)).isCollector()) {
          // check for invalid return value
          if (isTagged(values.get(1)) && tagged(values.get(1)).isNull()) {
            analyzer.handleNullReturn();
          }
          // valid return value with input dependencies
          else if (hasImportantDependencies(values.get(1))) {
            // add a copy and a reference
            // to capture the current state and future changes in alternative paths
            analyzer.getCollectorValues().add(tagged(values.get(1)));
            analyzer.getCollectorValues().add(tagged(values.get(1)).copy());
          }
          // valid return value without input dependencies
          else {
            analyzer.getCollectorValues().add(null);
          }
        }
        // special case: iterator method of Iterable
        else if (method.name.equals("iterator")
            && methodOwner.equals("java/lang/Iterable")
            && isTagged(values.get(0))
            && tagged(values.get(0)).isInputIterable()) {
          return new TaggedValue(
              Type.getObjectType("java/util/Iterator"),
              (tagged(values.get(0)).isInput1Iterable())
                  ? Tag.INPUT_1_ITERATOR
                  : Tag.INPUT_2_ITERATOR);
        }
        // special case: hasNext method of Iterator
        else if (method.name.equals("hasNext")
            && methodOwner.equals("java/util/Iterator")
            && isTagged(values.get(0))
            && tagged(values.get(0)).isInputIterator()
            && !analyzer.isUdfBinary()
            && !analyzer.isIteratorTrueAssumptionApplied()) {
          return new TaggedValue(Type.BOOLEAN_TYPE, Tag.ITERATOR_TRUE_ASSUMPTION);
        }
        // special case: next method of Iterator
        else if (method.name.equals("next")
            && methodOwner.equals("java/util/Iterator")
            && isTagged(values.get(0))
            && tagged(values.get(0)).isInputIterator()) {
          // after this call it is not possible to assume "TRUE" of "hasNext()" again
          analyzer.applyIteratorTrueAssumption();
          if (tagged(values.get(0)).isInput1Iterator()) {
            return analyzer.getInput1AsTaggedValue();
          } else {
            return analyzer.getInput2AsTaggedValue();
          }
        }
        // if the UDF class contains instance variables that contain input,
        // we need to analyze also methods without input-dependent arguments
        // special case: do not follow the getRuntimeContext method of RichFunctions
        else if (!isStatic
            && isTagged(values.get(0))
            && tagged(values.get(0)).isThis()
            && hasImportantDependencies(values.get(0))
            && !isGetRuntimeContext(method)) {
          TaggedValue tv = invokeNestedMethod(values, method);
          if (tv != null) {
            return tv;
          }
        }
        // the arguments have input dependencies ("THIS" does not count to the arguments)
        // we can assume that method has at least one argument
        else if ((!isStatic
                && isTagged(values.get(0))
                && tagged(values.get(0)).isThis()
                && hasImportantDependencies(values, true))
            || (!isStatic
                && (!isTagged(values.get(0)) || !tagged(values.get(0)).isThis())
                && hasImportantDependencies(values, false))
            || (isStatic && hasImportantDependencies(values, false))) {
          // special case: Java unboxing/boxing methods on input
          Type newType;
          if (isTagged(values.get(0))
              && tagged(values.get(0)).isInput()
              && (!isStatic && (newType = checkForUnboxing(method.name, methodOwner)) != null
                  || (isStatic
                      && (newType = checkForBoxing(method.name, method.desc, methodOwner))
                          != null))) {
            return tagged(values.get(0)).copy(newType);
          }
          // special case: setField method of TupleXX
          else if (method.name.equals("setField")
              && methodOwner.startsWith("org/apache/flink/api/java/tuple/Tuple")
              && isTagged(values.get(0))) {
            final TaggedValue tuple = tagged(values.get(0));
            tuple.setTag(Tag.CONTAINER);

            // check if fieldPos is constant
            // if not, we can not determine a state for the tuple
            if (!isTagged(values.get(2)) || !tagged(values.get(2)).isIntConstant()) {
              tuple.makeRegular();
            } else {
              final int constant = tagged(values.get(2)).getIntConstant();

              if (constant < 0 || Integer.valueOf(methodOwner.split("Tuple")[1]) <= constant) {
                analyzer.handleInvalidTupleAccess();
              }

              // if it is at least tagged, add it anyways
              if (isTagged(values.get(1))) {
                tuple.addContainerMapping("f" + constant, tagged(values.get(1)), currentFrame);
              }
              // mark the field as it has an undefined state
              else {
                tuple.addContainerMapping("f" + constant, null, currentFrame);
              }
            }
          }
          // special case: getField method of TupleXX
          else if (method.name.equals("getField")
              && methodOwner.startsWith("org/apache/flink/api/java/tuple/Tuple")) {
            final TaggedValue tuple =
                tagged(values.get(0)); // we can assume that 0 is an input dependent tuple
            // constant field index
            if (isTagged(values.get(1)) // constant
                && tagged(values.get(1)).isIntConstant()) {
              final int constant = tagged(values.get(1)).getIntConstant();

              if (constant < 0 || Integer.valueOf(methodOwner.split("Tuple")[1]) <= constant) {
                analyzer.handleInvalidTupleAccess();
              }

              if (tuple.containerContains("f" + constant)) {
                final TaggedValue tupleField = tuple.getContainerMapping().get("f" + constant);
                if (tupleField != null) {
                  return tupleField;
                }
              }
            }
            // unknown field index
            else {
              // we need to make the tuple regular as we cannot track modifications of fields
              tuple.makeRegular();
              return new TaggedValue(Type.getObjectType("java/lang/Object"));
            }
          }
          // nested method invocation
          else {
            TaggedValue tv = invokeNestedMethod(values, method);
            if (tv != null) {
              return tv;
            }
          }
        }
        return super.naryOperation(insn, values);
      default:
        // TODO support for INVOKEDYNAMIC instructions
        return super.naryOperation(insn, values);
    }
  }