private Object reduceSubType(Scope scope, TypeBinding subCandidate, TypeBinding superCandidate) {
    // 18.2.3 Subtyping Constraints
    if (subCandidate.isProperType(true) && superCandidate.isProperType(true)) {
      if (subCandidate.isCompatibleWith(superCandidate, scope)) return TRUE;
      return FALSE;
    }
    if (subCandidate.id == TypeIds.T_null) return TRUE;
    if (superCandidate.id == TypeIds.T_null) return FALSE;
    if (subCandidate instanceof InferenceVariable)
      return new TypeBound((InferenceVariable) subCandidate, superCandidate, SUBTYPE, this.isSoft);
    if (superCandidate instanceof InferenceVariable)
      return new TypeBound(
          (InferenceVariable) superCandidate,
          subCandidate,
          SUPERTYPE,
          this.isSoft); // normalize to have variable on LHS
    switch (superCandidate.kind()) {
      case Binding.GENERIC_TYPE:
      case Binding.TYPE:
      case Binding.RAW_TYPE:
        {
          if (subCandidate.isSubtypeOf(superCandidate)) return TRUE;
          return FALSE;
        }
      case Binding.PARAMETERIZED_TYPE:
        {
          List<ConstraintFormula> constraints = new ArrayList<>();
          while (superCandidate != null
              && superCandidate.kind() == Binding.PARAMETERIZED_TYPE
              && subCandidate != null) {
            if (!addConstraintsFromTypeParameters(
                subCandidate, (ParameterizedTypeBinding) superCandidate, constraints)) return FALSE;
            // travel to enclosing types to check if they have type parameters, too:
            superCandidate = superCandidate.enclosingType();
            subCandidate = subCandidate.enclosingType();
          }
          switch (constraints.size()) {
            case 0:
              return TRUE;
            case 1:
              return constraints.get(0);
            default:
              return constraints.toArray(new ConstraintFormula[constraints.size()]);
          }
        }
      case Binding.ARRAY_TYPE:
        TypeBinding tPrime = ((ArrayBinding) superCandidate).elementsType();
        // let S'[] be the most specific array type that is a supertype of S (or S itself)
        ArrayBinding sPrimeArray = null;
        switch (subCandidate.kind()) {
          case Binding.INTERSECTION_TYPE:
            {
              WildcardBinding intersection = (WildcardBinding) subCandidate;
              sPrimeArray =
                  findMostSpecificSuperArray(
                      intersection.bound, intersection.otherBounds, intersection);
              break;
            }
          case Binding.ARRAY_TYPE:
            sPrimeArray = (ArrayBinding) subCandidate;
            break;
          case Binding.TYPE_PARAMETER:
            {
              TypeVariableBinding subTVB = (TypeVariableBinding) subCandidate;
              sPrimeArray =
                  findMostSpecificSuperArray(subTVB.firstBound, subTVB.otherUpperBounds(), subTVB);
              break;
            }
          default:
            return FALSE;
        }
        if (sPrimeArray == null) return FALSE;
        TypeBinding sPrime = sPrimeArray.elementsType();
        if (!tPrime.isPrimitiveType() && !sPrime.isPrimitiveType()) {
          return ConstraintTypeFormula.create(sPrime, tPrime, SUBTYPE, this.isSoft);
        }
        return TypeBinding.equalsEquals(tPrime, sPrime) ? TRUE : FALSE; // same primitive type?

        // "type variable" has two implementations in JDT:
      case Binding.WILDCARD_TYPE:
        if (subCandidate.kind() == Binding.INTERSECTION_TYPE) {
          ReferenceBinding[] intersectingTypes = subCandidate.getIntersectingTypes();
          if (intersectingTypes != null)
            for (int i = 0; i < intersectingTypes.length; i++)
              if (TypeBinding.equalsEquals(intersectingTypes[i], superCandidate)) return true;
        }
        WildcardBinding variable = (WildcardBinding) superCandidate;
        if (variable.boundKind == Wildcard.SUPER)
          return ConstraintTypeFormula.create(subCandidate, variable.bound, SUBTYPE, this.isSoft);
        return FALSE;
      case Binding.TYPE_PARAMETER:
        // similar to wildcard, but different queries for lower bound
        if (subCandidate.kind() == Binding.INTERSECTION_TYPE) {
          ReferenceBinding[] intersectingTypes = subCandidate.getIntersectingTypes();
          if (intersectingTypes != null)
            for (int i = 0; i < intersectingTypes.length; i++)
              if (TypeBinding.equalsEquals(intersectingTypes[i], superCandidate)) return true;
        }
        if (superCandidate instanceof CaptureBinding) {
          CaptureBinding capture = (CaptureBinding) superCandidate;
          if (capture.lowerBound != null
              && (capture.firstBound == null || capture.firstBound.id == TypeIds.T_JavaLangObject))
            return ConstraintTypeFormula.create(
                subCandidate, capture.lowerBound, SUBTYPE, this.isSoft);
        }
        return FALSE;
      case Binding.INTERSECTION_TYPE:
        superCandidate = ((WildcardBinding) superCandidate).allBounds();
        // $FALL-THROUGH$
      case Binding.INTERSECTION_TYPE18:
        TypeBinding[] intersectingTypes =
            ((IntersectionTypeBinding18) superCandidate).intersectingTypes;
        ConstraintFormula[] result = new ConstraintFormula[intersectingTypes.length];
        for (int i = 0; i < intersectingTypes.length; i++) {
          result[i] =
              ConstraintTypeFormula.create(
                  subCandidate, intersectingTypes[i], SUBTYPE, this.isSoft);
        }
        return result;
      case Binding.POLY_TYPE:
        PolyTypeBinding poly = (PolyTypeBinding) superCandidate;
        Invocation invocation = (Invocation) poly.expression;
        MethodBinding binding = invocation.binding();
        if (binding == null || !binding.isValidBinding()) return FALSE;
        TypeBinding returnType =
            binding.isConstructor() ? binding.declaringClass : binding.returnType;
        return reduceSubType(
            scope,
            subCandidate,
            returnType.capture(scope, invocation.sourceStart(), invocation.sourceEnd()));
    }
    throw new IllegalStateException("Unexpected RHS " + superCandidate); // $NON-NLS-1$
  }