public static @Nullable PyTypeParameterMapping mapByShape(@NotNull List<? extends PyType> expectedTypes,
                                                            @NotNull List<? extends PyType> actualTypes,
                                                            Option @NotNull ... options) {
    EnumSet<Option> optionSet = EnumSet.noneOf(Option.class);
    optionSet.addAll(Arrays.asList(options));

    NullTolerantDeque<PyType> expectedTypesDeque = new NullTolerantDeque<>(flattenUnpackedTupleTypes(expectedTypes));
    NullTolerantDeque<PyType> actualTypesDeque = new NullTolerantDeque<>(flattenUnpackedTupleTypes(actualTypes));

    List<Couple<PyType>> leftMappedTypes = new ArrayList<>();
    List<Couple<PyType>> centerMappedTypes = new ArrayList<>();
    List<Couple<PyType>> rightMappedTypes = new ArrayList<>();

    boolean splittingTypeVarTuple = false;
    while (expectedTypesDeque.size() != 0 && actualTypesDeque.size() != 0) {
      PyType leftmostExpected = expectedTypesDeque.peekFirst();
      // Either a variadic type parameter *Ts or an unbounded unpacked tuple *tuple[int, ...] 
      if (leftmostExpected instanceof PyVariadicType) {
        break;
      }
      // The leftmost expected type is a regular type
      PyType leftmostActual = actualTypesDeque.peekFirst();
      if (leftmostActual instanceof PyVariadicType) {
        break;
      }
      expectedTypesDeque.removeFirst();
      actualTypesDeque.removeFirst();
      leftMappedTypes.add(Couple.of(leftmostExpected, leftmostActual));
    }

    while (expectedTypesDeque.size() != 0 && actualTypesDeque.size() != 0) {
      PyType rightmostExpected = expectedTypesDeque.peekLast();
      if (rightmostExpected instanceof PyVariadicType) {
        break;
      }
      expectedTypesDeque.removeLast();
      PyType rightmostActual = actualTypesDeque.peekLast();
      if (rightmostActual instanceof PyVariadicType rightmostActualVariadic) {
        if (rightmostActualVariadic instanceof PyUnpackedTupleType unpackedTupleType && unpackedTupleType.isUnbound()) {
          PyType repeatedActualType = unpackedTupleType.getElementTypes().get(0);
          rightMappedTypes.add(Couple.of(rightmostExpected, repeatedActualType));
        }
        else {
          splittingTypeVarTuple = true;
          break;
        }
      }
      else {
        actualTypesDeque.removeLast();
        rightMappedTypes.add(Couple.of(rightmostExpected, rightmostActual));
      }
    }

    if (expectedTypesDeque.size() != 0 && actualTypesDeque.size() != 0
        && !(expectedTypesDeque.peekFirst() instanceof PyVariadicType)
        && (actualTypesDeque.peekFirst() instanceof PyVariadicType variadic)) {
      if (variadic instanceof PyUnpackedTupleType actualUnpackedTupleType && actualUnpackedTupleType.isUnbound()) {
        while (expectedTypesDeque.size() != 0 && !(expectedTypesDeque.peekFirst() instanceof PyVariadicType)) {
          PyType repeatedActualType = actualUnpackedTupleType.getElementTypes().get(0);
          leftMappedTypes.add(Couple.of(expectedTypesDeque.peekFirst(), repeatedActualType));
          expectedTypesDeque.removeFirst();
        }
      }
      else {
        splittingTypeVarTuple = true;
      }
    }

    if (splittingTypeVarTuple) {
      return null;
    }

    boolean sizeMismatch;
    if (expectedTypesDeque.size() == 0) {
      boolean allActualTypesMatched = actualTypesDeque.size() == 0;
      boolean onlySingleActualVariadicLeft = actualTypesDeque.size() == 1 &&
                                             actualTypesDeque.peekFirst() instanceof PyVariadicType;
      sizeMismatch = !(allActualTypesMatched || onlySingleActualVariadicLeft);
    }
    else if (expectedTypesDeque.size() == 1) {
      PyType onlyLeftExpectedType = expectedTypesDeque.peekFirst();
      if (onlyLeftExpectedType instanceof PyVariadicType) {
        if (actualTypesDeque.size() == 1 && actualTypesDeque.peekFirst() instanceof PyVariadicType variadicType) {
          centerMappedTypes.add(Couple.of(onlyLeftExpectedType, variadicType));
        }
        else {
          List<PyType> unmatchedActualTypes = actualTypesDeque.toList();
          centerMappedTypes.add(Couple.of(onlyLeftExpectedType, PyUnpackedTupleTypeImpl.create(unmatchedActualTypes)));
        }
        sizeMismatch = false;
      }
      else if (optionSet.contains(Option.MAP_UNMATCHED_EXPECTED_TYPES_TO_ANY)) {
        centerMappedTypes.add(Couple.of(onlyLeftExpectedType, null));
        sizeMismatch = false;
      }
      else {
        sizeMismatch = true;
      }
    }
    else if (optionSet.contains(Option.MAP_UNMATCHED_EXPECTED_TYPES_TO_ANY)) {
      for (PyType unmatchedType : expectedTypesDeque.toList()) {
        centerMappedTypes.add(Couple.of(unmatchedType, null));
      }
      sizeMismatch = false;
    }
    else {
      sizeMismatch = true;
    }
    if (sizeMismatch) {
      return null;
    }
    List<Couple<PyType>> resultMapping = new ArrayList<>(leftMappedTypes);
    resultMapping.addAll(centerMappedTypes);
    Collections.reverse(rightMappedTypes);
    resultMapping.addAll(rightMappedTypes);
    return new PyTypeParameterMapping(resultMapping);
  }
public static @Nullable PyTypeParameterMapping mapByShape(@NotNull List<? extends PyType> expectedTypes,
                                                            @NotNull List<? extends PyType> actualTypes,
                                                            Option @NotNull ... options)