@Override
  public @Nullable DfaMemoryState tryJoinExactly(@NotNull DfaMemoryState other) {
    DfaMemoryStateImpl that = (DfaMemoryStateImpl)other;
    StateMerger merger = new StateMerger(this, that);
    if (!merger.update(that.myEphemeral || !myEphemeral, myEphemeral || !that.myEphemeral)) return null;
    if (myStack.size() != that.myStack.size()) return null;
    for (int i = 0; i < myStack.size(); i++) {
      DfaValue thisValue = myStack.get(i);
      DfaValue thatValue = that.myStack.get(i);
      int finalI = i;
      if (!merger.update(isSuperValue(thisValue, thatValue),
                           isSuperValue(thatValue, thisValue),
                           () -> {
                             if (thisValue instanceof DfaTypeValue && thatValue instanceof DfaTypeValue) {
                               DfType type = thisValue.getDfType().tryJoinExactly(thatValue.getDfType());
                               if (type != null) {
                                 return new MergePatch(false, ms -> ms.myStack.set(finalI, myFactory.fromDfType(type)));
                               }
                             }
                             return null;
                           })) {
        return null;
      }
    }
    int[] thisToThat = getClassesMap(that);
    if (!merger.update(thisToThat != null, true)) return null;
    int[] thatToThis = that.getClassesMap(this);
    if (!merger.update(true, thatToThis != null)) return null;
    if (thisToThat != null) {
      for (DistinctPairSet.DistinctPair pair : myDistinctClasses) {
        int firstIndex = thisToThat[pair.getFirstIndex()];
        int secondIndex = thisToThat[pair.getSecondIndex()];
        if (!merger.updateEquivalence(pair, firstIndex, secondIndex, false)) return null;
        RelationType relation = that.myDistinctClasses.getRelation(firstIndex, secondIndex);
        if (!merger.updateOrdering(pair, relation, false)) return null;
      }
    }
    if (thatToThis != null) {
      for (DistinctPairSet.DistinctPair pair : that.myDistinctClasses) {
        int firstIndex = thatToThis[pair.getFirstIndex()];
        int secondIndex = thatToThis[pair.getSecondIndex()];
        if (!merger.updateEquivalence(pair, firstIndex, secondIndex, true)) return null;
        RelationType relation = myDistinctClasses.getRelation(firstIndex, secondIndex);
        if (!merger.updateOrdering(pair, relation, true)) return null;
      }
    }
    for (Map.Entry<DfaVariableValue, DfType> entry : this.myVariableTypes.entrySet()) {
      DfaVariableValue value = entry.getKey();
      DfType thisType = entry.getValue();
      // the inherent variable type is not always a superstate for any non-inherent type
      // (e.g. inherent can be nullable, but current type can be notnull)
      // so we cannot limit checking to myVariableTypes map only
      DfType thatType = that.getDfType(value);
      if (!merger.updateVariable(value, thisType, thatType)) return null;
    }
    for (Map.Entry<DfaVariableValue, DfType> entry : that.myVariableTypes.entrySet()) {
      DfaVariableValue value = entry.getKey();
      if (this.myVariableTypes.containsKey(value)) continue; // already processed in the previous loop
      DfType thisType = this.getDfType(value);
      DfType thatType = entry.getValue();
      if (!merger.updateVariable(value, thisType, thatType)) return null;
    }
    return merger.merge(this, that);
  }
@Override
  public @Nullable DfaMemoryState tryJoinExactly(@NotNull DfaMemoryState other)