[FIR] Consider explicit returns during computing return type of anonymous function

Introduce FirAnonymousFunctionReturnExpressionInfo

^KT-59386
This commit is contained in:
Ivan Kochurkin
2023-07-12 21:56:39 +02:00
committed by Space Team
parent 212c10e674
commit 8f5294a508
8 changed files with 26 additions and 19 deletions
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.fir.references.builder.buildErrorNamedReference
import org.jetbrains.kotlin.fir.references.builder.buildResolvedErrorReference import org.jetbrains.kotlin.fir.references.builder.buildResolvedErrorReference
import org.jetbrains.kotlin.fir.resolve.calls.* import org.jetbrains.kotlin.fir.resolve.calls.*
import org.jetbrains.kotlin.fir.resolve.dfa.PropertyStability import org.jetbrains.kotlin.fir.resolve.dfa.PropertyStability
import org.jetbrains.kotlin.fir.resolve.dfa.cfg.FirAnonymousFunctionReturnExpressionInfo
import org.jetbrains.kotlin.fir.resolve.diagnostics.* import org.jetbrains.kotlin.fir.resolve.diagnostics.*
import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider
import org.jetbrains.kotlin.fir.resolve.transformers.body.resolve.resultType import org.jetbrains.kotlin.fir.resolve.transformers.body.resolve.resultType
@@ -136,10 +136,10 @@ abstract class FirDataFlowAnalyzer(
return variable.stability to types.toMutableList() return variable.stability to types.toMutableList()
} }
fun returnExpressionsOfAnonymousFunctionOrNull(function: FirAnonymousFunction): Collection<FirExpression>? = fun returnExpressionsOfAnonymousFunctionOrNull(function: FirAnonymousFunction): Collection<FirAnonymousFunctionReturnExpressionInfo>? =
graphBuilder.returnExpressionsOfAnonymousFunction(function) graphBuilder.returnExpressionsOfAnonymousFunction(function)
fun returnExpressionsOfAnonymousFunction(function: FirAnonymousFunction): Collection<FirExpression> = fun returnExpressionsOfAnonymousFunction(function: FirAnonymousFunction): Collection<FirAnonymousFunctionReturnExpressionInfo> =
returnExpressionsOfAnonymousFunctionOrNull(function) returnExpressionsOfAnonymousFunctionOrNull(function)
?: error("anonymous function ${function.render()} not analyzed") ?: error("anonymous function ${function.render()} not analyzed")
@@ -25,6 +25,8 @@ import org.jetbrains.kotlin.fir.util.listMultimapOf
import org.jetbrains.kotlin.utils.addToStdlib.runIf import org.jetbrains.kotlin.utils.addToStdlib.runIf
import org.jetbrains.kotlin.utils.getOrPutNullable import org.jetbrains.kotlin.utils.getOrPutNullable
data class FirAnonymousFunctionReturnExpressionInfo(val expression: FirExpression, val isExplicit: Boolean)
@OptIn(CfgInternals::class) @OptIn(CfgInternals::class)
class ControlFlowGraphBuilder { class ControlFlowGraphBuilder {
private val graphs: Stack<ControlFlowGraph> = stackOf() private val graphs: Stack<ControlFlowGraph> = stackOf()
@@ -82,10 +84,10 @@ class ControlFlowGraphBuilder {
// ----------------------------------- Public API ----------------------------------- // ----------------------------------- Public API -----------------------------------
fun returnExpressionsOfAnonymousFunction(function: FirAnonymousFunction): Collection<FirExpression>? { fun returnExpressionsOfAnonymousFunction(function: FirAnonymousFunction): Collection<FirAnonymousFunctionReturnExpressionInfo>? {
val exitNode = function.controlFlowGraphReference?.controlFlowGraph?.exitNode ?: return null val exitNode = function.controlFlowGraphReference?.controlFlowGraph?.exitNode ?: return null
fun CFGNode<*>.returnExpression(): FirExpression? = when (this) { fun CFGNode<*>.returnExpression(): FirAnonymousFunctionReturnExpressionInfo? = when (this) {
is BlockExitNode -> when { is BlockExitNode -> when {
// lambda@{ x } -> x // lambda@{ x } -> x
// lambda@{ class C } -> Unit-returning stub // lambda@{ class C } -> Unit-returning stub
@@ -106,16 +108,21 @@ class ControlFlowGraphBuilder {
lastStatement.source?.kind != KtFakeSourceElementKind.ImplicitReturn.FromLastStatement -> lastStatement.source?.kind != KtFakeSourceElementKind.ImplicitReturn.FromLastStatement ->
null null
else -> else ->
lastStatement as? FirExpression (lastStatement as? FirExpression
?: buildUnitExpression { source = fir.statements.lastOrNull()?.source ?: fir.source } ?: buildUnitExpression { source = fir.statements.lastOrNull()?.source ?: fir.source }).let {
FirAnonymousFunctionReturnExpressionInfo(it, isExplicit = false)
}
} }
} }
// fun() { terminatingExpression } -> nothing (checker will emit an error if return type is not Unit) // fun() { terminatingExpression } -> nothing (checker will emit an error if return type is not Unit)
// fun() { throw } or fun() { returnsNothing() } -> Nothing-returning stub // fun() { throw } or fun() { returnsNothing() } -> Nothing-returning stub
else -> FirStub.takeIf { _ -> previousNodes.all { it is StubNode } } else -> FirStub.takeIf { _ -> previousNodes.all { it is StubNode } }
?.let { FirAnonymousFunctionReturnExpressionInfo(it, isExplicit = false) }
} }
// lambda@{ return@lambda x } -> x // lambda@{ return@lambda x } -> x
is JumpNode -> (fir as? FirReturnExpression)?.takeIf { it.target.labeledElement.symbol == function.symbol }?.result is JumpNode -> (fir as? FirReturnExpression)?.takeIf { it.target.labeledElement.symbol == function.symbol }?.result?.let {
FirAnonymousFunctionReturnExpressionInfo(it, isExplicit = true)
}
else -> null // shouldn't happen? expression bodies are implicitly wrapped in `FirBlock`s else -> null // shouldn't happen? expression bodies are implicitly wrapped in `FirBlock`s
} }
@@ -33,7 +33,6 @@ import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef
import org.jetbrains.kotlin.fir.types.impl.ConeClassLikeTypeImpl import org.jetbrains.kotlin.fir.types.impl.ConeClassLikeTypeImpl
import org.jetbrains.kotlin.fir.visitors.transformSingle import org.jetbrains.kotlin.fir.visitors.transformSingle
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.calls.inference.addEqualityConstraintIfCompatible import org.jetbrains.kotlin.resolve.calls.inference.addEqualityConstraintIfCompatible
import org.jetbrains.kotlin.resolve.calls.inference.addSubtypeConstraintIfCompatible import org.jetbrains.kotlin.resolve.calls.inference.addSubtypeConstraintIfCompatible
import org.jetbrains.kotlin.resolve.calls.inference.buildAbstractResultingSubstitutor import org.jetbrains.kotlin.resolve.calls.inference.buildAbstractResultingSubstitutor
@@ -341,7 +340,7 @@ class FirCallCompleter(
} }
transformer.context.dropContextForAnonymousFunction(lambdaArgument) transformer.context.dropContextForAnonymousFunction(lambdaArgument)
val returnArguments = components.dataFlowAnalyzer.returnExpressionsOfAnonymousFunction(lambdaArgument) val returnArguments = components.dataFlowAnalyzer.returnExpressionsOfAnonymousFunction(lambdaArgument).map { it.expression }
return ReturnArgumentsAnalysisResult(returnArguments, builderInferenceSession) return ReturnArgumentsAnalysisResult(returnArguments, builderInferenceSession)
} }
@@ -552,7 +552,7 @@ class FirCallCompletionResultsWriterTransformer(
// The case where we can't find any return expressions not common, and happens when there are anonymous function arguments // The case where we can't find any return expressions not common, and happens when there are anonymous function arguments
// that aren't mapped to any parameter in the call. So, we don't run body resolve transformation for them, thus there's // that aren't mapped to any parameter in the call. So, we don't run body resolve transformation for them, thus there's
// no control flow info either. Example: second lambda in the call like list.filter({}, {}) // no control flow info either. Example: second lambda in the call like list.filter({}, {})
val returnExpressions = dataFlowAnalyzer.returnExpressionsOfAnonymousFunctionOrNull(anonymousFunction) val returnExpressions = dataFlowAnalyzer.returnExpressionsOfAnonymousFunctionOrNull(anonymousFunction)?.map { it.expression }
?: return transformImplicitTypeRefInAnonymousFunction(anonymousFunction) ?: return transformImplicitTypeRefInAnonymousFunction(anonymousFunction)
val expectedType = data?.getExpectedType(anonymousFunction)?.let { expectedArgumentType -> val expectedType = data?.getExpectedType(anonymousFunction)?.let { expectedArgumentType ->
@@ -865,16 +865,16 @@ open class FirDeclarationsResolveTransformer(
} }
private fun FirAnonymousFunction.computeReturnTypeRef(expected: FirResolvedTypeRef?): FirResolvedTypeRef { private fun FirAnonymousFunction.computeReturnTypeRef(expected: FirResolvedTypeRef?): FirResolvedTypeRef {
// Any lambda expression assigned to `(...) -> Unit` returns Unit
if (isLambda && expected?.type?.isUnit == true) return expected
// `lambda@ { return@lambda }` always returns Unit
val returnExpressions = dataFlowAnalyzer.returnExpressionsOfAnonymousFunction(this) val returnExpressions = dataFlowAnalyzer.returnExpressionsOfAnonymousFunction(this)
if (shouldReturnUnit(returnExpressions)) return session.builtinTypes.unitType // Any lambda expression assigned to `(...) -> Unit` returns Unit if all return expressions are implicit
// `lambda@ { return@lambda }` always returns Unit
if (isLambda && expected?.type?.isUnit == true && returnExpressions.all { !it.isExplicit }) return expected
if (shouldReturnUnit(returnExpressions.map { it.expression })) return session.builtinTypes.unitType
// Here is a questionable moment where we could prefer the expected type over an inferred one. // Here is a questionable moment where we could prefer the expected type over an inferred one.
// In correct code this doesn't matter, as all return expression types should be subtypes of the expected type. // In correct code this doesn't matter, as all return expression types should be subtypes of the expected type.
// In incorrect code, this would change diagnostics: we can get errors either on the entire lambda, or only on its // In incorrect code, this would change diagnostics: we can get errors either on the entire lambda, or only on its
// return statements. The former kind of makes more sense, but the latter is more readable. // return statements. The former kind of makes more sense, but the latter is more readable.
val inferredFromReturnExpressions = session.typeContext.commonSuperTypeOrNull(returnExpressions.map { it.resultType.coneType }) val inferredFromReturnExpressions = session.typeContext.commonSuperTypeOrNull(returnExpressions.map { it.expression.resultType.coneType })
return inferredFromReturnExpressions?.let { returnTypeRef.resolvedTypeFromPrototype(it) } return inferredFromReturnExpressions?.let { returnTypeRef.resolvedTypeFromPrototype(it) }
?: session.builtinTypes.unitType // Empty lambda returns Unit ?: session.builtinTypes.unitType // Empty lambda returns Unit
} }
@@ -4,9 +4,9 @@ val a: () -> Int = <!INITIALIZER_TYPE_MISMATCH!>l@ {
if (flag) return@l 4 if (flag) return@l 4
}<!> }<!>
val b: () -> Unit = l@ { val b: () -> Unit = <!INITIALIZER_TYPE_MISMATCH!>l@ {
if (flag) return@l 4 if (flag) return@l 4
} }<!>
val c: () -> Any = l@ { val c: () -> Any = l@ {
if (flag) return@l 4 if (flag) return@l 4
@@ -10,13 +10,13 @@ val a: () -> Unit = l@{
if (true) 42 if (true) 42
} }
val b: () -> Unit = l@{ val b: () -> Unit = <!INITIALIZER_TYPE_MISMATCH!>l@{
// Error, coercion can't be applied at this position! // Error, coercion can't be applied at this position!
if (true) return@l "hello" if (true) return@l "hello"
// However, this is OK, because here coercion is applied // However, this is OK, because here coercion is applied
"hello" "hello"
} }<!>
val c: () -> Unit = { val c: () -> Unit = {
// Interesting enough, for such expessions we use expected type Unit // Interesting enough, for such expessions we use expected type Unit