blob: 9bcd0bdf0642a447351886314e639320ad641bbf [file] [log] [blame] [view] [edit]
# Partially Constrained Lambda Analysis
**Prerequisites:** That document is built under the assumption that the reader is more or less aware of how inference
works for calls without lambdas or with lambdas for which all input-types are proper at the point where
they should be analyzed. Also, some terms that are used here described at [Common inference terms definition](inference.md)
See also: [Kotlin Spec: Builder-style type inference](https://kotlinlang.org/spec/type-inference.html#builder-style-type-inference)
The most basic scenario where PCLA is used is calls to builder-like functions:
```kotlin
fun <E> buildList(builderAction: MutableList<E>.() -> Unit): List<E> = ...
fun main() {
buildList {
add("")
} // E is inferred to `String`
}
```
There in the `buildList` call there is no other source of information what type should be inferred for `E` beside the body of the lambda.
That is the case where partially constrained lambda analysis (PCLA) starts.
## Glossary
**Regular lambda analysis**
Currently, we start lambda analysis only during call completion (`ConstraintSystemCompleter.runCompletion`).
Let's call "regular lambda analysis" situation when we start body resolution of the anonymous function when all input types do not contain
uninferred TVs.
**PCLA (Partially Constrained Lambda Analysis)**
PCLA is the way of lambda analysis during call completion that happens when some input types are not properly inferred, and there are no other sources of constraints.
**PCLA lambda** = lambda analyzed with PCLA
**Main call/candidate** is the call that contains PCLA lambda
**Nested call/candidate** is a call being resolved in other than `ContextDependent` mode (usually statement-level) that belongs to PCLA lambda
* **Postponed nested calls** are nested calls which we decided not to complete in the FULL mode immediately
**Shared CS** is a constraint system being shared between postponed nested calls inside PCLA lambda
**Outer CS**
* Defined only for nested postponed candidates (for shared CS)
* It consists of type variables of the containing PCLA lambdas and all the type variables of already processed inner candidates
* For inner CS, all the outer CS-related type variables are always coming at the beginning of the list of all variables
**Outer Type Variable (TV)**
* In the context of *shared CS*, it's a subset of type variables that are brought from the *outer CS*
* In the list of all variables (`NewConstraintSystemImpl.allTypeVariables`) outer TV always comes in the beginning.
* See `NewConstraintSystemImpl.outerSystemVariablesPrefixSize`
## Entry-point to PCLA
The conditions to run a lambda resolution in PCLA mode is the following:
- Completion mode for the current call tree is `FULL`
- There are no other ways to infer some new constraints for any type variable.
- There is some lambda that among its input types has at least one with a not fixed TV being used as type argument
- So, if one of the input types is `MutableList<Ev>` that lambda suits for PCLA
- But if some of the input types are proper and other are top-level type variables (`Tv`, `Tv?`, `Tv & Any`, etc.), it doesn't suit
**References in code:**
- `ConstraintSystemCompleter.tryToCompleteWithPCLA`
During PCLA, there is a quite specific behavior of how actually lambda body resolution
happens, and describing it is actually a goal of the following parts document.
## Algorithm backbone
The basic idea is that we mostly run regular lambda body analysis, with special treatment for all calls inside. Namely,
- They use **shared CS**: adding their new variables there and constraints related to both *outer CS* and their own variables
- They are not being fully completed (leaving them postponed)
After lambda's body traversal, the whole **shared CS** is added to the CS of the *main candidate*, the resulting CS should be resolved
as usual, and after that at completion results writing phase all type variables are replaced with their result types.
### More details
* **NB:** This algorithm doesn't use any stub types, just regular type variables everywhere
* Once we see a need to start PCLA (see [Entry-point to PCLA](#entry-point-to-pcla)), we create
* `FirPCLAInferenceSession` (introduced at `LambdaAnalyzerImpl.analyzeAndGetLambdaReturnArguments`)
* Shared CS (see `FirPCLAInferenceSession.currentCommonSystem` property)
* It's mostly a copy of **outer CS** (the CS of the containing candidate), but we mark all the variables as *outer*
* See `FirInferenceSession.Companion.prepareSharedBaseSystem`
* After that, we run analysis inside the lambda
* For nested candidates that are really trivial (no receivers with TV, no lambdas, no value arguments using outer CS), we just regularly complete them
* For other calls
* Instead of empty initial CS, we supply a **shared CS**
* So, before introducing its own TVs, CS already contains of outer TV
* Run candidate resolution within this CS
* Do not run full completion, just gather all the constraints and variable back to the **shared CS**
* In the end, the **root CS** (aka CS for the Main candidate) contains TVs for all incomplete nested candidates.
* And may just continue `FULL` completion process on the Main, i.e., fixing variables for which we now have new proper constraints,
and analyze other lambdas.
* In the end, beside completion results writing for the main call, we also write results (inferred type arguments/expression type update)
for all incomplete/postponed calls (see `FirCallCompletionResultsWriterTransformer.runPCLARelatedTasksForCandidate`).
### Example 1
```
fun foo() {}
fun main() {
val x = listOf("")
buildList /* Main candidate/call */ {
foo() // Irrelevant call that doesn't refer PCLA variables, might be fully completed
// Uses shared CS
// Adds String <: Ev constraint
// Pushes all constraints back to the shared CS
add("") // Postponed nested call
// Uses shared CS
// Candidate declaration: fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C
// Adds Tv, Rv, Cv type variables
// And constraints
// String <: Tv [from List<String> <: Iterable<Tv>]
// MutableList<Ev> <: Cv
// Rv <: Ev [incorporation from MutableList<Ev> <: MutableCollection<in Rv>
// Fixing Tv := String
// Starting lamda analysis
// Having
// Rv <: String constraint
//
// Then add all the new constraints to the shared CS
x.mapTo(this) { it } // Postponed nested call
}
// And start looking for CS solution that would be sound
// Ev := String
// Rv := String
// Cv := MutableList<String>
```
### Example 2
```kotlin
fun <A, B, C> twoSteps(first: Inv<A>.() -> Unit, b: B, second: (B) -> C): Triple<A, B, C> {
val a = Inv<A>().apply(first)
val c = second(value)
return Triple(a, value, c)
}
fun main() {
twoSteps(
first = { set(1) },
b = "b",
second = { it }
)
}
```
Here, during the call completion of the call to `twoSteps` function we would:
1. Fix `Bv <:> String`, from the `String <: Bv` constraint.
2. Analyze `second` lambda, since it has enough type info, `Bv <:> String` is known now.
- `first` lambda still has `Av` in its input types: `/* input type */ Inv<Av>.() -> /* output type */ Unit`
- `second` lambda input types are known: `(/* input type */ Bv=String) -> /*output type*/ Cv`
- Get additional type constraint `Cv <: String` from the usage of it
3. Fix `Cv <:> String`, from the newly inferred lambda type info.
4. Realize, that the `first` lambda still doesn't have enough type info and its
input types are still unknown.
5. tryToCompleteWithPCLA
- Check if the `first` lambda type shape is suitable for PCLA
- Analyze the `first` lambda in PCLA mode
### Shared CS definition
- It's been initialized at `FirPCLAInferenceSession.currentCommonSystem`.
- Initially, it's an empty CS with the main candidate's system being added as an outer CS.
## Inference session callbacks
Currently, there are three implementations of inference session
- `Default` that effectively does nothing
- `FirPCLAInferenceSession` (being used during PCLA lambda resolution)
- `FirDelegatedPropertyInferenceSession` (being used for delegated operator resolution)
### baseConstraintStorageForCandidate
It returns *shared CS* if the candidate is considered applicable (non-trivial/postponed nested).
That callback is supposed to be called for each created candidate of all nested calls, and the
content of returned CS should be integrated into the candidate CS.
See `org.jetbrains.kotlin.fir.resolve.calls.Candidate.getSystem`.
The shared CS is not supposed to be modified during candidate resolution
(because there might be more than one of them for each call, while only a single one should be chosen).
### customCompletionModeInsteadOfFull
Being called after the single successful candidate is chosen just before the completion begins.
Currently, it returns `PCLA_POSTPONED_CALL` if the candidate is *postponed*.
See more details on `PCLA_POSTPONED_CALL` completion mode in the [section](#pclapostponedcall-completion-mode) below.
### processPartiallyResolvedCall
This callback is assumed to be called after `PARTIAL` or `PCLA_POSTPONED_CALL` completion terminates.
Mostly, it collects all *postponed* candidates and integrates their CS content into the shared CS.
Also, here we substitute the type of that given call expression with substitutor replacing already fixed type variables
with their result types.
Otherwise, using fixed TVs as expression types further might lead to `TypeCheckerStateForConstraintInjector.fixedTypeVariable`
throwing an exception.
```kotlin
fun <S> id(e: S) = e
fun main() {
buildList {
// `id(this.size) ` type is `Sv`.
// But we actually fixed `Sv` to Int because it's resolved in independent context and `Sv` is not related
// to the outer CS.
//
// But since we don't run completion writing results, no one would replace the expression type of the call
// with new resulting type.
// So, we do it at `processPartiallyResolvedCall`
val x = id(this.size)
add(x)
}
}
```
Note that this situation with fixed TVs can't happen outside PCLA context because
- For partial completion, we avoid fixing TVs that are used inside return types
- For FULL completion, we would run completion results writing, so there would be no candidates and type variables inside return types.
And the last part is fixing TVs for receiver (see the relevant [section](#on-demand-variable-fixation) for details).
### runLambdaCompletion
This part is rather controversial and hacky, hope we could get rid of it at some point.
When resolving a lambda at some nested call inside PCLA lambda, like
```kotlin
buildList {
myOtherCallWithLambda {
add("")
Unit
}
}
```
After completion for `add("")` call, we haven't yet called `processPartiallyResolvedCall` for `myOtherCallWithLambda { ... }`,
because it's not yet completed, and we are already going to call `processPartiallyResolvedCall` for `add("")`, that would contribute some
constraints to `buildList` type variable `Ev` in shared CS.
And after that we finally call `processPartiallyResolvedCall` for `myOtherCallWithLambda { ... }`, but it doesn't contain constraints
on `Ev` from `add(""")`, thus overwriting existing information.
That might be workarounded with some kind of merging constraints for the same TV instead of just replacement
(see `NewConstraintSystemImpl.addOtherSystem`), but that seems to be hacky too.
So, the current approach is the following: during nested lambda resolution we assume that currently *shared CS* is equal to the CS of the
`myOtherCallWithLambda` candidate. Thus, the constraints from `add` call are being merged into the `myOtherCallWithLambda`'s CS and then
finally into regular shared CS.
**Hacky place**: the proper solution might be just replacing instance of shared CS with current candidate's CS, but that needs
some further investigation.
Also, there's some special handling of overload resolution by lambda return type, but they are quite expected when we don't have a final
candidate yet (see `analyzeLambdaAndReduceNumberOfCandidatesRegardingOverloadResolutionByLambdaReturnType`)
### runCallableReferenceResolution
TODO: Mostly, the idea as the same as for lambdas, but for callable references
### addSubtypeConstraintIfCompatible
In some situations, some constraints might be originated not just from calls, but from other kinds of expression,
like from variable assignments:
```kotlin
var <F> MutableList<F>.firstElement
get() = get(0)
set(f: F) {
set(0, f)
}
fun main() {
buildList {
firstElement = ""
}
}
```
We don't specially resolve the call to the setter, thus to declare that `String <: Fv`, thus `String <: Ev`, and finally `Ev := String`,
there's a need to somehow send this information to the inference session.
And that's how `addSubtypeConstraintIfCompatible` might be used.
**Hacky place**: In general, this approach is quite fragile, and we might've forgotten some places where this method should be triggered.
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.
## 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).
The set of the type variables that might be considered for fixation are obtained from the call tree of the supplied nested call itself,
i.e., it would be a set of fresh type variables of the candidate itself, type variable of its arguments and from the return statements
of lambdas. That part works the same way as for regular calls outside PCLA context.
### Example
```
fun foo(x: List<String>) {
buildList {
x.mapTo(this) { // `mapTo` analyzed in PCLA_POSTPONED_CALL
add("") // analyzed in PCLA_POSTPONED_CALL
it.length // analyzed in PCLA_POSTPONED_CALL
}
println("") // analyzed in FULL mode (because it's a trivial call, irrelevant to outer CS)
add(x.get(0) /* analyzed in PARTIAL */) // analyzed in PCLA_POSTPONED_CALL
}
}
```
### Short description
- Type variables are gathered from the call tree of the supplied *nested* call.
- Variable fixation is not allowed if the variable is deeply related to some TV from the outer CS.
- All the lambdas belonging to the supplied call need to be analyzed during this completion.
- We don't run completion writing in this mode.
### Variable fixation limitation
The only limitation that we've got for `PCLA_POSTPONED_CALL` is that we don't allow fixing TVs that are *deeply related* to some outer TV
(for definition, see the relevant part at [inference.md](inference.md)).
**References in code:** `TypeVariableFixationReadiness.OUTER_TYPE_VARIABLE_DEPENDENCY`
See the following example,
```kotlin
interface A
object B : A
object C : A
class GenericController<T> {
fun yield(t: T) {}
}
fun <K> GenericController<K>.yieldAll(s: Collection<K>) {}
fun <S> generate(g: suspend GenericController<S>.() -> Unit): S = TODO()
fun foo(x: Collection<B>) {
generate {
// GenericController<K> <: GenericController<S> => K := S
// Collection<B> <: Collection<K>
// B <: K
// Let's imagine we fix K to B, then it would lead to the constraint S := B
yieldAll(x)
// And to constraint error here: C <: B
yield(C)
}
}
```
While in fact, `Sv` might be easily inferred to `A` as a common supertype. To make it happen we just postpone fixation of the `Sv` as one
having deep
### Forcing lambda analysis
Unlike `FULL` mode, it doesn't require fixing all the variables, in that sense it works more like `PARTIAL`.
But unlike `PARTIAL`, if forces analysis of all the lambdas even if some input types are not properly inferred yet.
It's crucial because otherwise, contract-affecting information gained would stop working across different statements.
For lambdas, there might be three kinds of situations:
- There's some lambda which input types do not contain any not fixed type variables => might be analyzed in a regular way
- For some lambda, some of the input types satisfy PCLA requirement (see [Entry-point to PCLA](#Entry-point-to-PCLA))
=> might be analyzed in recursive PCLA mode (see [Nested PCLA](#nested-pcla))
- For all lambdas, all the input types are either proper or a top-level type variable (most complicated).
In the third (and sometimes in the second) situation, we start regular lambda analysis, but with the following modifications:
- If the lambda parameter has a type of some not fixed TV, we just leave it there.
- If the receiver type of lambda is some type variable [on demand](#on-demand-variable-fixation). One of the options would be just leaving TV
not fixed as for value parameter, but that seems meaningless because almost any call inside the lambda might require the member scope
of the receiver, thus need to fix its result type.
## On demand variable fixation
Unlike non-PCLA context, here there might be legit situations when someone needs to look into member scope of some type variable.
For example, see the following case:
```kotlin
buildList {
// MutableList<Ev> <: Tv
// T can't be fixed, because it's deeply related to Ev
this.let { it -> // it: Tv
it.add("") // requesting member scope of Tv
}
}
```
There, we've got `it` used as a receiver in a call `it.add("")`, thus the first question we need to answer to is which member scope it has.
And the answer is that before starting the selector (`add("")`), we simply try to fix necessary type variable with the following steps:
- Don't do anything in case the type variable belongs, to outer CS
- Temporary override what *proper type* means
- Type variables from outer CS might be used as a type argument for a proper type (e.g., `MutableList<Ev>` is proper).
- Even type variables from outer CS can't be used as top-level proper type (e.g, we can't fix `Tv` to `Ev`).
- See `ConstraintSystemCompletionContext.withTypeVariablesThatAreCountedAsProperTypes`
and `TypeSystemInferenceExtensionContext.isProperTypeForFixation` extension.
- If there is some proper type for that TV, run regular result type computation `ResultType`.
- Add constraint `Tv := ResultType` and use `ResultType` as a member scope
- **NB:** we don't actually *fix* the variable via `NewConstraintSystemImpl.fixVariable`, because there's an assumption that resulting
type should not contain other type variables that might be not satisfied during on-demand fixation. But `fixVariable` would anyway be called
during final completion when all other type variables types would be properly substituted in the given equality constraint.
As follows, from the rules above, there are some scenarios where the variable can't be fixed. In those cases, we implicitly forbid
using them as receivers:
- By assuming that we've got their member scope empty
- Reporting a diagnostic at `org.jetbrains.kotlin.fir.resolve.calls.TypeVariablesInExplicitReceivers` resolution stage
(currently `BUILDER_INFERENCE_STUB_RECEIVER` being used).
```kotlin
fun <T> myLet(t: T, b: (T) -> Unit) {}
buildList {
get(0).toString() // BUILDER_INFERENCE_STUB_RECEIVER is reported with PCLA
myLet(get(0)) { it ->
get(0).toString() // BUILDER_INFERENCE_STUB_RECEIVER is reported with PCLA
}
}
```
See the implementations details at `FirPCLAInferenceSession.fixVariablesForMemberScope`.
## CST computation
When computing the result type for TV referencing other variables from outer CS there might be a situation when we need to compute
`CommonSuperType(Xv, ...)`:
```kotlin
fun <S> selectL(x: S, y: S, l: (S) -> Unit) {}
fun foo(x: MutableList<String>) {
buildList {
// Adding the constraints
// MutableList<String> <: Sv
// MutableList<Ev <: Sv
// But to analyze lambda we fix Sv to CST(MutableList<String>, MutableList<Ev>) = MutableList<CST(String, Ev)>
// The question is what is CST(String, Ev)
selectL(x, this) { x ->
x.add(<!ARGUMENT_TYPE_MISMATCH!>1<!>)
x.add("")
}
} // inferred to List<String>
}
```
**References:** `org.jetbrains.kotlin.resolve.calls.inference.components.ResultTypeResolver.prepareLowerConstraints`
In general, such a problem existed and be resolved before PCLA:
- On the fixation stage, type variable should have at least one proper constraint, but other might be not-proper, but still being used
- For that case, we do a controversial thing, namely
- Substituting all TVs with special stub types
- Compute CST with those substituted types
- For those stub types, there’s effectively a rule that for any type `Tv` and stub `XStub`: `CST(Tv, XStub) = Tv`
- Intuitively, such an approach seems correct because it just chooses some result and incorporates it with other TVs, at least not hiding any contradictions
- In the example above, we would get `Sv := MutableList<String>` and during incorporation we would assert the subtyping `Collection<S> <: Collection<String>`, thus having constraint `S <: String` making the whole CS sound
But unlike a non-PCLA case, our TV might have a single constraint containing a reference to some other (outer) TV, so we need to make
some tweaks:
- Because we've overridden what "proper" means here, but `prepareLowerConstraints` uses it in a sense of the usage some TV inside the type,
for PCLA context, we unconditionally perform stub substitution (a bit of dirty place)
- And after that we might need to substitute "local" stubs back to TVs
(impossible in non-PCLA, because in that case, at least one constraint is "proper" in regular sense, i.e., doesn't contain any TVs)
## Nested PCLA
```kotlin
buildList {
add("")
addAll(buildSet l2@{
add(1)
})
}
```
For nested PCLA lambdas, everything works pretty straightforward:
- We create own `FirPCLAInferenceSession` for it
- Run analysis of the nested lambda with that session just the same way as for an outer
- But the main candidate for `l2` automatically uses the shared CS for `l1` (thus, it's used as an outer CS for `l2`)
- But we don't run completion writing results at this stage yet
- Just applying the system for `l2` to the candidate of nested candidate owning the lambda ( `addAll` in the example), and after that it
automatically propagates to the shared CS during `processPartiallyResolvedCall`.
Thus, in the end, we would get the CS for the root call containing all variables from the whole PCLA-tree.
Completion results writing for nested lambdas happen recursively when it starts for the root call:
- In the example we start with `buildList`.
- Go to `FirCallCompletionResultsWriterTransformer.runPCLARelatedTasksForCandidate`.
- Find the postponed call `addAll`.
- Recursively run `runPCLARelatedTasksForCandidate` for it at some point.
- And while iterating the postponed call for the latter will write results for `add(1)` call.
## Nested Delegation inference
```kotlin
buildList {
val x by lazy {
get(0)
}
add("")
add(x)
}
```
Inference for delegated properties inside PCLA works in the following way:
- Delegate expression is being resolved as a regular postponed nested call under the same PCLA session.
- All the operator calls are being resolved under specially created delegate `FirDelegatedPropertyInferenceSession` in `PARTIAL` mode.
- *After* that we run completion for delegate expression via `PCLA_POSTPONED_CALL` mode.
- Then, we integrate delegation related CS into PCLA shared session and all the delegation related calls as regular postponed ones
of the PCLA session.
For more details, read [delegated_property_inference.md](delegated_property_inference.md),
`FirDelegatedPropertyInferenceSession.completeSessionOrPostponeIfNonRoot` and `FirPCLAInferenceSession.integrateChildSession`.