K2: Fix PCLA breaking change when using a bare type on a type variable

See the change at docs/fir/pcla.md and the kdoc
at `getAndSemiFixCurrentResultIfTypeVariable` for clarification

^KT-64840 Fixed
This commit is contained in:
Denis.Zharkov
2024-03-08 19:38:58 +01:00
committed by Space Team
parent 141be17b4b
commit 232c3aeadc
10 changed files with 78 additions and 33 deletions
@@ -37,6 +37,19 @@ abstract class FirInferenceSession {
open fun addSubtypeConstraintIfCompatible(lowerType: ConeKotlinType, upperType: ConeKotlinType, element: FirElement) {}
/**
* For non-trivial inference session (currently PCLA-only), if the type is a type variable that might be fixed,
* fix it and return a fixation result.
*
* Type variable might be fixed if it doesn't belong to an outer CS and have proper constraints.
*
* By semi-fixation we mean that only the relevant EQUALITY constraint is added,
* [org.jetbrains.kotlin.resolve.calls.inference.components.ConstraintSystemCompletionContext.fixVariable] is not expected to be called.
*
* NB: The callee must pay attention that exactly current common CS will be modified.
*/
open fun getAndSemiFixCurrentResultIfTypeVariable(type: ConeKotlinType): ConeKotlinType? = null
companion object {
val DEFAULT: FirInferenceSession = object : FirInferenceSession() {
override fun <T> processPartiallyResolvedCall(
@@ -135,7 +135,7 @@ class FirPCLAInferenceSession(
val system = (this as? FirResolvable)?.candidate()?.system ?: currentCommonSystem
if (resolutionMode is ResolutionMode.ReceiverResolution) {
fixVariablesForMemberScope(resolvedType, system)?.let { additionalBindings += it }
fixCurrentResultIfTypeVariableAndReturnBinding(resolvedType, system)?.let { additionalBindings += it }
}
val substitutor = system.buildCurrentSubstitutor(additionalBindings) as ConeSubstitutor
@@ -146,19 +146,22 @@ class FirPCLAInferenceSession(
}
}
fun fixVariablesForMemberScope(
override fun getAndSemiFixCurrentResultIfTypeVariable(type: ConeKotlinType): ConeKotlinType? =
fixCurrentResultIfTypeVariableAndReturnBinding(type, currentCommonSystem)?.second
fun fixCurrentResultIfTypeVariableAndReturnBinding(
type: ConeKotlinType,
myCs: NewConstraintSystemImpl,
): Pair<ConeTypeVariableTypeConstructor, ConeKotlinType>? {
return when (type) {
is ConeFlexibleType -> fixVariablesForMemberScope(type.lowerBound, myCs)
is ConeDefinitelyNotNullType -> fixVariablesForMemberScope(type.original, myCs)
is ConeTypeVariableType -> fixVariablesForMemberScope(type, myCs)
is ConeFlexibleType -> fixCurrentResultIfTypeVariableAndReturnBinding(type.lowerBound, myCs)
is ConeDefinitelyNotNullType -> fixCurrentResultIfTypeVariableAndReturnBinding(type.original, myCs)
is ConeTypeVariableType -> fixCurrentResultForNestedTypeVariable(type, myCs)
else -> null
}
}
private fun fixVariablesForMemberScope(
private fun fixCurrentResultForNestedTypeVariable(
type: ConeTypeVariableType,
myCs: NewConstraintSystemImpl,
): Pair<ConeTypeVariableTypeConstructor, ConeKotlinType>? {
@@ -120,7 +120,7 @@ class PostponedArgumentsAnalyzer(
// TODO: Fix variables for context receivers, too (KT-64859)
buildMap {
lambda.receiver
?.let { pclaInferenceSession.fixVariablesForMemberScope(it, candidate.system) }
?.let { pclaInferenceSession.fixCurrentResultIfTypeVariableAndReturnBinding(it, candidate.system) }
?.let(this::plusAssign)
}
}
@@ -867,7 +867,10 @@ open class FirExpressionsResolveTransformer(transformer: FirAbstractBodyResolveT
val firClass = type.lookupTag.toSymbol(session)?.fir ?: return this
if (firClass.typeParameters.isEmpty()) return this
val originalType = argument.unwrapExpression().resolvedType
val originalType = argument.unwrapExpression().resolvedType.let {
components.context.inferenceSession.getAndSemiFixCurrentResultIfTypeVariable(it) ?: it
}
val outerClasses by lazy(LazyThreadSafetyMode.NONE) { firClass.symbol.getClassAndItsOuterClassesWhenLocal(session) }
val newType = components.computeRepresentativeTypeForBareType(type, originalType)
?: if (
@@ -1,20 +0,0 @@
// ISSUE: KT-64840 (K2/PCLA difference)
class Controller<T> {
fun yield(t: T): Boolean = true
}
fun <S> generate(g: suspend Controller<S>.() -> Unit): S = TODO()
interface A<F> {
val a: F?
}
interface B<G> : A<G>
fun <X> predicate(x: X, c: Controller<in X>, p: (X) -> Boolean) {}
fun main(a: A<*>) {
generate {
predicate(a, this) { it is <!NO_TYPE_ARGUMENTS_ON_RHS!>B<!> }
}.a
}
@@ -1,3 +1,4 @@
// FIR_IDENTICAL
// ISSUE: KT-64840 (K2/PCLA difference)
class Controller<T> {
fun yield(t: T): Boolean = true
@@ -18,8 +18,8 @@ fun <X> withCallback(x: X, c: Controller<in X>, p: (X) -> Unit) {}
fun main(a: A<String>) {
val x = generate {
withCallback(a, this) {
(it as <!NO_TYPE_ARGUMENTS_ON_RHS!>B<!>).<!UNRESOLVED_REFERENCE!>b<!>.length
it.<!UNRESOLVED_REFERENCE!>b<!>.length
(it as B).b.length
it.b.length
it.a.length
}
}
@@ -15,6 +15,8 @@ interface C : CommonSupertype
fun <X> predicate(x: X, c: Controller<X>, p: (X) -> Unit) {}
fun main(a: A<*>, c: C) {
// Without having `is` check
// This PCLA/BI call works in the same way both in K1 and K2
val x1 = generate {
predicate(a, this) { x ->
// x is B
@@ -25,13 +27,19 @@ fun main(a: A<*>, c: C) {
<!DEBUG_INFO_EXPRESSION_TYPE("CommonSupertype")!>x1<!>
// But introducing `is` on the expression of `Xv` type as LHS and a bare type on RHS
// Leads to an early fixation of Xv to the current result type (A<*>) and automatically it leads to Sv fixation, too
// This case works differently in K1 (BI) and in K2 (PCLA), but in both cases it's red code
// Without the last `yield` call, it would be even green in K2
val x2 = generate {
predicate(a, this) { x ->
x is <!NO_TYPE_ARGUMENTS_ON_RHS!>B<!>
x is B
}
yield(c)
// For Sv we've got an EQUALITY constraint to A<*>
// Thus not allowing C type here
yield(<!ARGUMENT_TYPE_MISMATCH!>c<!>)
}
<!DEBUG_INFO_EXPRESSION_TYPE("CommonSupertype")!>x2<!>
<!DEBUG_INFO_EXPRESSION_TYPE("A<*>")!>x2<!>
}
@@ -15,6 +15,8 @@ interface C : CommonSupertype
fun <X> predicate(x: X, c: Controller<X>, p: (X) -> Unit) {}
fun main(a: A<*>, c: C) {
// Without having `is` check
// This PCLA/BI call works in the same way both in K1 and K2
val x1 = generate {
predicate(a, this) { x ->
// x is B
@@ -25,11 +27,17 @@ fun main(a: A<*>, c: C) {
<!DEBUG_INFO_EXPRESSION_TYPE("CommonSupertype")!>x1<!>
// But introducing `is` on the expression of `Xv` type as LHS and a bare type on RHS
// Leads to an early fixation of Xv to the current result type (A<*>) and automatically it leads to Sv fixation, too
// This case works differently in K1 (BI) and in K2 (PCLA), but in both cases it's red code
// Without the last `yield` call, it would be even green in K2
val x2 = generate {
predicate(a, this) { x ->
<!USELESS_IS_CHECK!>x is <!NO_TYPE_ARGUMENTS_ON_RHS!>B<!><!>
}
// For Sv we've got an EQUALITY constraint to A<*>
// Thus not allowing C type here
yield(c)
}
+29
View File
@@ -299,6 +299,35 @@ And that's how `addSubtypeConstraintIfCompatible` might be used.
One of the ideas particularly for assignment is that they should be resolved via setter call, thus the necessary constraint would be
introduced naturally when string literal would be an argument for `Fv` value parameter.
### getAndSemiFixCurrentResultIfTypeVariable
Before deep-diving into this section, it's worth reading [On demand variable fixation](#on-demand-variable-fixation) section.
Sometimes, besides computing member scope, there might be other cases when we need to fix a type variable on-demand.
```kotlin
interface A<F>
interface B<G> : A<G>
fun <X> predicate(x: X, c: MutableList<in X>, p: (X) -> Boolean) {}
fun main(a: A<*>) {
buildList {
predicate(a, this) {
it is B
}
}
}
```
In this example, for `is` check, `B` type on the right-hand side is a bare type and to compute its arguments properly, we need to know
the proper type representation of `it` which is not proper yet (`Xv` variable).
Potentially, we might've ignored that requiring full type arguments for `B`, but that would be a breaking change from a user project
([KT-64840](https://youtrack.jetbrains.com/issue/KT-64840)), so we decided to fix the type variable to the current result type.
That's how this callback is currently used from the place before bare-type computation is started.
## PCLA_POSTPONED_CALL completion mode
This mode is assumed to be used for postponed nested calls inside PCLA lambdas instead of FULL mode (i.e., mostly for top-level calls).