docs/fir/builder_inference.md
See also: Kotlin Spec: Builder-style type inference
NB: This document is quite obsolete, see pcla.md.
Type-variable, that was used to build a stub type on, and fixation of which is postponed until the end of lambda analysis. Selection of type variables to postpone happens during Initiation phase
Type, that is equal to anything from the perspective of subtyping. Carries information about its base postponed type variable
Such types are used during analysis of lambda body inplace of postponed type variables to resolve calls, after end of analysis occurrences of such types are replaced with corresponding type inference result for its base type variable
Ex:
buildList { /* this: MutableList<Stub(T)> -> */
val self/*: MutableList<Stub(T)> */ = this
self.add("") // String <: Stub(T) from argument
}
A constraint that doesn't reference any type variables
The algorithm consists of the following phases, all of which happen during constraint system completion
Before, we try to fix all type variables that have enough type info to be fixed and analyze all lambda arguments, whose input types depend on such type variables.
If there are still not analyzed lambda arguments left, we try to perform builder inference
Entrypoint to builder inference is the function org.jetbrains.kotlin.fir.resolve.inference.ConstraintSystemCompleter.tryToCompleteWithBuilderInference
It happens only during full completion
Then, all lambda arguments, that weren't analyzed and satisfy the following criteria are considered:
Such type-variables are marked as postponed and analysis of the lambda argument is performed
First, org.jetbrains.kotlin.fir.resolve.inference.PostponedArgumentsAnalyzer.analyzeLambda
Then, before the actual transforming of the lambda body, an instance of org.jetbrains.kotlin.fir.resolve.inference.FirBuilderInferenceSession
is created and set as the current inference session
During body analysis of a lambda, for each call (inside lambda body) that is completed in the FULL mode, we ask the inference session if it can be completed
See org.jetbrains.kotlin.fir.resolve.inference.FirBuilderInferenceSession.shouldRunCompletion
It is needed in cases, where the result of builder inference can help with further system completion
Example:
fun <M> materialize(): M = null!!
fun foo() = buildList {
addAll(materialize()) // (1) To be able to infer type for materialize, we need to know builder inference result
add("str") // (2) This call already has complete type info, so we allow it to complete
}
In situation 1, we mark the call as an incomplete, see org.jetbrains.kotlin.fir.resolve.inference.FirBuilderInferenceSession.addPartiallyResolvedCall
In situation 2, we mark the call as a completed, see org.jetbrains.kotlin.fir.resolve.inference.FirInferenceSession.addCompletedCall
Note: Calls that were analyzed in PARTIAL mode will not be considered, as it always analyzed during FULL completion of the outer call. Such calls will not be added to incomplete/completed call lists
The criteria to mark a call as an incomplete is ALL following:
If call is marked incomplete its constraint system wouldn't be solved as usual in org.jetbrains.kotlin.fir.resolve.inference.FirCallCompleter.completeCall, completion result writing will not be performed.
It is postponed until lambda analysis finalization
See org.jetbrains.kotlin.fir.resolve.inference.PostponedArgumentsAnalyzer.applyResultsOfAnalyzedLambdaToCandidateSystem
Once the lambda body was analyzed return arguments are added into the call-tree
Note: Incomplete calls in return arguments
We don't add last expression of lambda to the call-tree as a return argument if its functional-type has Unit return-type, to avoid situations when last expression contains incomplete call, see Incomplete call in return arguments
Then, we perform inference of postponed type variables
See org.jetbrains.kotlin.fir.resolve.inference.FirBuilderInferenceSession.inferPostponedVariables
After such steps, the constraint system of the call-tree contains all output-type constraints
While systems for the completed and incomplete calls inside the lambda body contain all internal type constraints in the form of constraints with stub types
See org.jetbrains.kotlin.fir.resolve.inference.FirBuilderInferenceSession.buildCommonSystem
To find a solution for postponed type variables we need to integrate all aforementioned constraints. We do so, by constructing a new constraint system in the following way:
org.jetbrains.kotlin.resolve.calls.inference.model.BuilderInferencePosition)Note: Such a system shouldn't contain stub types from current builder inference session in constraints anymore
If the system is empty, we bail out and skip to the result write
After such a constraint system is constructed, we call the constraint system completer to solve the system
Note: As incomplete calls couldn't contain postponed arguments in the call-tree (as defined in lambda body analysis), we don't analyze any postponed arguments here
See org.jetbrains.kotlin.fir.resolve.inference.FirBuilderInferenceSession.updateCalls
Solution(TV) - result type, that was inferred for a type variable TV in integration CS
Substitution will be the following:
Note: Effectively, it means that if we have an inference result for a postponed type variable, we replace stub/type variable types inside with an error types
Unclear: If the constraint system wasn't empty we apply the resulting substitutor to fixed type variables from CS of the call-tree
We apply the resulting substitutor to all type references inside of lambda, recursively
After that, we apply the same substitution to all the implicit receivers, which originated from lambdas. It is needed to have full type information during the analysis of lambda arguments inside of return arguments in the current lambda (since those arguments may be analyzed after builder inference completion)
Then, we call the completion results writer to store completion results for incomplete calls with the result substitutor
See org.jetbrains.kotlin.fir.resolve.inference.PostponedArgumentsAnalyzer.applyResultsOfAnalyzedLambdaToCandidateSystem
Once the result for postponed variables is inferred and stored, we need to propagate results to the CS of call-tree since its completion isn't finished yet
To do so, we inject subtype constraint with result type from postponed variable inference
After that, we mark postponed type variables as no longer postponed
Lambda analysis is now finished, and we return to the Initiation to process other lambda arguments of the call-tree
In fact, builder inference doesn't only apply to "builders". It's a general algorithm that applies to lambda arguments of particular shape
Ex:
suspendCoroutine/* <String> inferred by 'builder inference' */ {
it.resume("")
}
// ...
fun <T> consumeIterator(input: Iterator<T>.() -> Unit) {}
fun takeString(s: String) {}
//...
consumeIterator/* <String> inferred by 'builder inference' */ { takeString(next()) }
Criteria to perform builder inference doesn't consider lambdas, which input types contain only type-variable-types
The constraint system that is used to infer results for postponed type variables isn't checked for inconsistencies
During result writing, we update fixed type variables of the main call-tree, reasons for that aren't clear
During lambda analysis finalization we create and solve separate CS.
However, we need to communicate results to the CS of the call-tree, during result backpropagation we add subtype constraints between type variable from the call-tree and solution from integration CS, loosing information about actual constraints.
It causes unsound solutions in the call-tree CS
Its unclear why we mix stub type substitutor with type-variable-type substitutor, and why we need to manually handle substitution to error types
If incomplete call is present among return arguments during Lambda analysis finalization we add it to the main call-tree, but then complete it as part of result writing.
It leads to violation of contract in org.jetbrains.kotlin.fir.resolve.inference.ConstraintSystemCompleter.getOrderedAllTypeVariables as type variables for completed call couldn't be found anymore
To avoid such problem, we have workaround for lambdas with Unit return-type, but problem still occurs: