K2: Avoid inappropriate approximation of captured type to Nothing?

There's a heuristic for approximation of a captured type that once
it has non-trivial lower bound (other than Nothing), it's worth
approximating it to sub-type even while the containing
top-level type is being approximated to super-type.

And that sounds reasonable in case the lower bound is indeed non-trivial,
but that's not the case because nullability here comes from
the nullability of captured type position.

So, the fix is basically not to treat such approximations as non-trivial.
And while that seems to be a bit of a change in the language semantics,
it still looks reasonable (see other changes in test data and KT-58087)

^KT-57958 Fixed
^KT-58087 Fixed
This commit is contained in:
Denis.Zharkov
2023-04-18 19:07:30 +02:00
committed by Space Team
parent 0a631ed8fc
commit 6651e6ed8c
10 changed files with 189 additions and 16 deletions
@@ -531,25 +531,27 @@ abstract class AbstractTypeApproximator(
continue@loop
}
/**
* Example with non-trivial both type approximations:
* Inv<In<C>> where C = in Int
* Inv<In<C>> <: Inv<out In<Int>>
* Inv<In<C>> <: Inv<in In<Any?>>
*
* So such case is rare and we will chose Inv<out In<Int>> for now.
*
* Note that for case Inv<C> we will chose Inv<in Int>, because it is more informative then Inv<out Any?>.
* May be we should do the same for deeper types, but not now.
*/
// In case of Inv<C> and C = Captured(in Int), we choose Inv<in Int> as resulting approximation
// NB: Inv<C> <: Inv<in Int> because Int <: C (as Int is a lower bound of the C captured type)
//
// That behavior of choosing non-trivial lower bound is crucial when there's also non-trivial upper bound,
// like if Inv would be declared as `interface Inv<T : CharSequence>` (see test approximationLeavesNonTrivialLowerBound.kt)
//
// In that case the next condition after that doesn't help because in case of both non-trivial bounds, it chooses the upper one
if (argumentType.typeConstructor().isCapturedTypeConstructor()) {
val subType = approximateToSubType(argumentType, conf, depth) ?: continue@loop
if (!subType.isTrivialSub()) {
if (shouldUseSubTypeForCapturedArgument(subType, argumentType, conf, depth)) {
newArguments[index] = createTypeArgument(subType, TypeVariance.IN)
continue@loop
}
}
// Example with non-trivial both type approximations:
// Inv<In<C>> where C = Captured(in Int)
// Inv<In<C>> <: Inv<out In<Int>>
// Inv<In<C>> <: Inv<in In<Any?>>
//
// So, both of the options are possible, but since such case is rare we will chose Inv<out In<Int>> for now
val approximatedSuperType =
approximateToSuperType(argumentType, conf, depth) ?: continue@loop // null means that this type we can leave as is
if (approximatedSuperType.isTrivialSuper()) {
@@ -577,6 +579,36 @@ abstract class AbstractTypeApproximator(
return approximateLocalTypes(approximatedType, conf, toSuper) ?: approximatedType
}
private fun shouldUseSubTypeForCapturedArgument(
subType: KotlinTypeMarker,
capturedArgumentType: KotlinTypeMarker,
conf: TypeApproximatorConfiguration,
depth: Int,
): Boolean {
if (subType.isTrivialSub()) return false
// For K1, the result is always `!subType.isTrivialSub()` (leaving the old behavior)
if (!isK2) return true
// Basically, what's written further might be simplified like
// return !approximateToSubType(capturedArgumentType.withNullability(false), conf, depth)!!.isTrivialSub()
// But it seems that now it looks a bit more clear and probably performant, thus first two if's are basically fast paths
// If it's not `Nothing?`, then the lower bound is indeed non-trivial
if (!subType.lowerBoundIfFlexible().isNullableNothing()) return true
// Here the subType is `Nothing?`, and it might be trivial only in cause the nullability is caused by nullability of captured type itself
// If captured type is not marked as nullable, then nullability of subType came from the lower bound of the captured type.
// Thus, the lower bound is non-trivial for sure
if (!capturedArgumentType.isMarkedNullable()) return true
val notMarkedNullableSubType =
approximateToSubType(capturedArgumentType.withNullability(false), conf, depth)
?: error("Not-marked-nullable version of captured type approximation should also return not-null")
return !notMarkedNullableSubType.isTrivialSub()
}
private fun KotlinTypeMarker.defaultResult(toSuper: Boolean) = if (toSuper) nullableAnyType() else {
if (this is SimpleTypeMarker && isMarkedNullable()) nullableNothingType() else nothingType()
}