cabf7e4fae
^KT-65637 Fixed
223 lines
13 KiB
Markdown
223 lines
13 KiB
Markdown
## FIR/Builder inference
|
|
See also: [Kotlin Spec: Builder-style type inference](https://kotlinlang.org/spec/type-inference.html#builder-style-type-inference)
|
|
|
|
**NB:** This document is quite obsolete, see [pcla.md](pcla.md).
|
|
|
|
### Glossary
|
|
#### Postponed type variable
|
|
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](#initiation) phase
|
|
#### Stub type
|
|
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](#lambda-body-analysis) 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:
|
|
```kotlin
|
|
buildList { /* this: MutableList<Stub(T)> -> */
|
|
val self/*: MutableList<Stub(T)> */ = this
|
|
self.add("") // String <: Stub(T) from argument
|
|
}
|
|
```
|
|
#### Proper constraint
|
|
A constraint that doesn't reference any type variables
|
|
### Builder inference algorithm
|
|
The algorithm consists of the following phases, all of which happen during constraint system completion
|
|
- [Initiation](#initiation) - deciding if lambda is suitable for builder inference
|
|
- [Lambda analysis preparation](#lambda-analysis-preparation) - stub type and builder inference session creating
|
|
- [Lambda body analysis](#lambda-body-analysis) - collecting calls inside lambda body, which references stub-types among input types
|
|
- [Lambda analysis finalization](#lambda-analysis-finalization) - integrating collected information
|
|
- [Result write](#result-write) - writing of builder inference result to FIR tree of lambda (and implicit receiver stack update)
|
|
- [Result backpropagation](#result-backpropagation) - propagation of builder inference result to CS of call-tree
|
|
|
|
### Detailed description
|
|
#### Initiation
|
|
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:
|
|
|
|
- lambda argument has a non-empty list of input types
|
|
- any of the input types contain a non-fixed type variable in arguments (If the input type is a type-variable itself, it is **NOT** considered)
|
|
|
|
Such type-variables are marked as postponed and analysis of the lambda argument is performed
|
|
|
|
#### Lambda analysis preparation
|
|
First, `org.jetbrains.kotlin.fir.resolve.inference.PostponedArgumentsAnalyzer.analyzeLambda`
|
|
- creates stub types for all postponed type variables
|
|
- substitutes type-variable-type -> stub-type in a receiver, context receiver, parameters, and return types
|
|
|
|
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
|
|
|
|
#### Lambda body analysis
|
|
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:
|
|
```kotlin
|
|
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:
|
|
1. There's no contradiction in the constraint system of the call
|
|
2. The candidate (call) is suitable for builder inference based on **ANY** of the following conditions:
|
|
- The dispatch receiver's type contains a stub type
|
|
- The extension receiver's type contains a stub type and the corresponding symbol has a builder inference annotation in the session
|
|
3. The candidate doesn't have any not analyzed postponed atoms
|
|
4. Any type variable in call CS doesn't have a proper constraint, and it's not a postponed variable
|
|
|
|
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](#lambda-analysis-finalization)
|
|
|
|
#### 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](#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:
|
|
- Register all not-fixed type variables from CS of call-tree
|
|
- Re-apply all initial constraints from CS of call-tree
|
|
- Substitute Stub(PostponedTV) with TypeVariableType(PostponedTV)
|
|
- Optimization: Only constraints, where at least one of the types contains TypeVariableType from integration system are applied
|
|
- Constraints, that originate from builder inference results of other lambda arguments of the call-tree are skipped (See `org.jetbrains.kotlin.resolve.calls.inference.model.BuilderInferencePosition`)
|
|
- Re-apply all initial constraints of all completed calls that were collected during lambda body analysis
|
|
- Using the same algorithm as above
|
|
- Re-apply all initial constraints of all incomplete calls
|
|
- Using the same algorithm as above
|
|
- Also, register all fixed type variables and bring its fixation result as an equality constraint
|
|
|
|
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](#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](#lambda-body-analysis)), we don't analyze any postponed arguments here
|
|
|
|
#### Result write
|
|
See `org.jetbrains.kotlin.fir.resolve.inference.FirBuilderInferenceSession.updateCalls`
|
|
|
|
##### Resulting substitutor:
|
|
Solution(TV) - result type, that was inferred for a type variable TV in integration CS
|
|
|
|
Substitution will be the following:
|
|
- Stub(PostponedTV) -> Solution(PostponedTV) ||
|
|
- TypeVariableType(TV) -> Solution(TV) ||
|
|
- Stub(*) -> ERROR ||
|
|
- TypeVariableType(*) -> ERROR
|
|
|
|
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
|
|
|
|
#### Result backpropagation
|
|
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](#initiation) to process other lambda arguments of the call-tree
|
|
|
|
### Potential problems
|
|
#### Unclear naming
|
|
In fact, builder inference doesn't only apply to "builders". It's a general algorithm that applies to lambda arguments of particular shape
|
|
|
|
Ex:
|
|
```kotlin
|
|
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()) }
|
|
```
|
|
#### Incomplete criteria to perform builder-inference
|
|
Criteria to perform builder inference doesn't consider lambdas, which input types contain only type-variable-types
|
|
#### Incomplete criteria to mark calls as incomplete during lambda analysis
|
|
- Criteria to mark calls as incomplete depends on builder inference annotation, however it shouldn't be used anymore
|
|
- In case of present postponed arguments somewhere in the call-tree it will change behavior, failing to infer types
|
|
- Ad-hoc check for the presence of proper constraints
|
|
#### Lost diagnostics
|
|
The constraint system that is used to infer results for postponed type variables isn't checked for inconsistencies
|
|
#### Unclear substitution of fixed type variables
|
|
During [result writing](#result-write), we update fixed type variables of the main call-tree, reasons for that aren't clear
|
|
#### Potentially exponential complexity
|
|
- During [lambda analysis finalization](#lambda-analysis-finalization) we copy initial constraints from call-tree to integration system, it
|
|
can lead to exponential complexity (no concrete example here)
|
|
- During [result writing](#result-write) we traverse body of lambda argument recursively, in case if there are multiple nested lambdas that are
|
|
subjects to builder inference, it will be exponential
|
|
#### Separate constraint system creation and result backpropagation
|
|
During [lambda analysis finalization](#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](#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
|
|
#### Resulting substitution is unclear
|
|
Its unclear why we mix stub type substitutor with type-variable-type substitutor, and why we need to manually handle substitution to error
|
|
types
|
|
#### Incomplete call in return arguments
|
|
If incomplete call is present among return arguments during [Lambda analysis finalization](#lambda-analysis-finalization) we add it to the
|
|
main call-tree, but then complete it as part of [result writing](#result-write).
|
|
|
|
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](#Note-Incomplete-calls-in-return-arguments), but problem still
|
|
occurs:
|
|
- When we have [lambdas with non-Unit return-type](https://youtrack.jetbrains.com/issue/KT-58741)
|
|
- When the [incomplete call is present in a return argument that isn't last expression](https://youtrack.jetbrains.com/issue/KT-58742)
|