cabf7e4fae
^KT-65637 Fixed
158 lines
8.7 KiB
Markdown
158 lines
8.7 KiB
Markdown
## FIR/Delegated property inference
|
|
See also: [Kotlin Spec: Delegated property declaration](https://kotlinlang.org/spec/declarations.html#delegated-property-declaration) and some [Common inference terms definition](inference.md)
|
|
|
|
In many ways, delegated property inference works as a simpler version of PCLA, so it's worth beginning with [pcla.md](pcla.md).
|
|
|
|
### Glossary
|
|
#### Delegation related operator calls
|
|
Synthetically generated by Raw FIR builder calls to `getValue`, `setValue` or `provideDelegate` functions.
|
|
|
|
*NB:* They might be differentiated from regular calls by comparing origin to `FirFunctionCallOrigin.Operator`
|
|
|
|
#### Outer CS
|
|
A constraint system that defines type variables with their constraints is not brought by the call itself or its arguments.
|
|
The general idea is that before running resolution of a specific "inner" candidate, we copy all the outer variables to its own CS in the very
|
|
beginning, and after some successful candidate is chosen, we apply the "relevant" changes to the outer CS.
|
|
|
|
By "apply" operation currently we mean literally replacement of the old CS with the new one.
|
|
|
|
See `FirDelegatedPropertyInferenceSession.parentConstraintSystem`.
|
|
|
|
### Desugaring at Raw FIR building stage
|
|
The code like this
|
|
```kotlin
|
|
var prop by delegateExpression()
|
|
```
|
|
is being desugared to something similar to this (see `org.jetbrains.kotlin.fir.builder.ConversionUtilsKt.generateAccessorsByDelegate`)
|
|
```
|
|
val prop = propertyNode {
|
|
delegate = delegate {
|
|
expression = "delegateExpression()"
|
|
delegateProvider = "expression.provideDelegate()"
|
|
}
|
|
|
|
get() = delegate$field.getValue($thisRef, ::prop)
|
|
set(value) {
|
|
delegate$field.setValue($thisRef, ::prop, value)
|
|
}
|
|
}
|
|
```
|
|
|
|
Even in case the property type is specified, all the content types are set as implicit.
|
|
|
|
### Delegated property inference algorithm
|
|
|
|
#### Delegate expression inference
|
|
At first, we resolve delegate expression with `Delegate` resolution mode that behaves just like the regular `ContextDependent`
|
|
but it has some small differences (see usages of the relevant enum entry):
|
|
- If the delegate expression is a lambda, we would resolve it as it was an independent expression
|
|
- We treat it specially inside [PCLA](pcla.md).
|
|
|
|
Note that we do that outside delegate inference session (it's not created yet), so if delegated property inside a PCLA lambda, the delegate
|
|
expression would be analyzed under PCLA session.
|
|
|
|
*NB:* If the delegate expression is simple enough, i.e., it does not contain type parameters, or they might be inferred from the call itself,
|
|
we just run FULL completion on it as for regular `ContextDependent` and don't store its CS.
|
|
|
|
For reference, see `FirDeclarationsResolveTransformer.transformPropertyAccessorsWithDelegate`.
|
|
|
|
#### Session introduction
|
|
After delegate expression is analyzed, we create `FirDelegatedPropertyInferenceSession` and use it as an inference session for resolving
|
|
operator calls.
|
|
|
|
As an outer/parent CS we use either:
|
|
- A constraint system of the delegate expression if it's a `FirResolvable` (under PCLA, it would have *shared CS*)
|
|
- Otherwise, if delegate is some simple/non-call-like expression, obtain shared CS if PCLA is present, or just create the empty one
|
|
|
|
#### provideDelegate resolution
|
|
After delegate expression is resolved, we start regular resolution of `provideDelegate()` call stored at `FirWrappedDelegateExpression::delegateProvider`.
|
|
|
|
Note that as explicit receiver it uses just the same instance of `delegateExpression` we've just resolved on the previous step.
|
|
As the receiver still might contain some type variables, we use CS of it as [Outer CS](#outer-cs) for all the `provideDelegate` candidates.
|
|
|
|
If there is no single most specific successful candidate, then we just drop and forget the call.
|
|
|
|
Otherwise, after the resulting candidate is chosen, it has some state of CS that contains both "inner" type variables of the candidate itself and some
|
|
global ones brought by Outer CS.
|
|
|
|
Note that we don't force FULL completion (unlike K1 did), thus potentially leaving some of the type variables not fixed.
|
|
The most problematic part with that approach is that in some cases the return type of `provideDelegate` is a type variable, _and_ we
|
|
might need to look into its member scope to find `getValue` call there.
|
|
|
|
```kotlin
|
|
val test: String by materializeDelegate()
|
|
|
|
fun <T> materializeDelegate(): Delegate<T> = Delegate()
|
|
|
|
operator fun <K> K.provideDelegate(receiver: Any?, property: kotlin.reflect.KProperty<*>): K = this
|
|
|
|
class Delegate<V> {
|
|
operator fun getValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>): V = TODO()
|
|
}
|
|
```
|
|
|
|
But the problem is that member scope is not defined for variables, thus we emulate K1 full completion behavior by
|
|
|
|
- Looking if `provideDelegate` actually returns a type variable (`K`)
|
|
- Finding the current result type that might be used for fixation, but under the assumption that all outer type variables
|
|
(`[T]` in the example above) are considered proper, so could be used inside a fixation result
|
|
- Add equality constraint `K = FoundFixationResult` (`K = Delegate(Tv)` in the example above)
|
|
- When creating a final substitutor for `provideDelegate` expression, take that constraint into account
|
|
|
|
See `org.jetbrains.kotlin.fir.resolve.transformers.body.resolve.FirDeclarationsResolveTransformer.findResultTypeForInnerVariableIfNeeded`.
|
|
|
|
If `provideDelegate` is completed without contradictions, we effectively replace `delegateExpression` with `delegateExpression.provideDelegate()`.
|
|
|
|
#### getValue/setValue analysis
|
|
|
|
If a property type is set explicitly, it's being propagated to the accessors' signatures at `FirDeclarationsResolveTransformer::transformAccessors`
|
|
(return type of getter and parameter type of setter).
|
|
Otherwise, the types remain implicit.
|
|
|
|
Then, we effectively call `transformFunctionWithGivenSignature` on getter and _only_ if the property type was explicit,
|
|
call `transformFunctionWithGivenSignature` on setter, too.
|
|
|
|
We do that under the same delegate inference session, so we've got callbacks for `FirInferenceSession.onCandidatesResolution` thus
|
|
exactly for `getValue`/`setValue`/`provideDelegate` calls, we set outer CS from the session.
|
|
|
|
Note that there's no need to do that for the delegate expression or nested arguments (they should be resolved as usual in `ContextDependent` resolution mode).
|
|
|
|
Also, while accessors have a shape like `delegate$field.getValue($thisRef, ::prop)`, `delegate$field` is a special reference which type
|
|
is being set to the current type of `delegateExpression` at `FirExpressionsResolveTransformer.transformQualifiedAccessExpression`.
|
|
|
|
For those delegation operator calls, if they're successfully resolved, we preserve their CSs and use them as the main outer ones,
|
|
also we collect the calls as ones that need to be completed later (partially completed).
|
|
|
|
Note that `transformFunctionWithGivenSignature` if a return type is implicit propagates one from the body of the function, thus
|
|
after getter resolution its return type (and the property one) might contain some type variables.
|
|
|
|
See `org.jetbrains.kotlin.fir.resolve.transformers.body.resolve.FirDeclarationsResolveTransformer.transformPropertyAccessorsWithDelegate`.
|
|
|
|
#### Partial calls completion
|
|
|
|
At that stage, we've got all those delegation operators calls resolved effectively under the same constraint system.
|
|
So, what we need to do further is solving that CS, thus find the result types for all the type variables from those calls.
|
|
|
|
To achieve that, we run completion for all incomplete calls altogether as a list of `topLevelAtoms`, so it works mostly as regular FULL completion.
|
|
|
|
See `org.jetbrains.kotlin.fir.resolve.inference.FirDelegatedPropertyInferenceSession.completeCandidates`.
|
|
|
|
#### Completion results writing
|
|
|
|
In case of successful completion, we get a final substitutor that we may apply to property return type and accessor signatures, too.
|
|
Thus, getting rid of potentially left type variables there.
|
|
|
|
After that we run `FirCallCompletionResultsWriterTransformer` on each of the freshly completed calls, with a special mode
|
|
`DelegatedPropertyCompletion` that is only different in a meaning that for each qualified access it also, recursively ran on the explicit
|
|
receiver (that is necessary to update `delegate$field` references types for `getValue`/`setValue` calls).
|
|
|
|
Another nasty tweak that is needed to be made is updating substituted member after completion.
|
|
Some of the final candidates might be obtained from member scopes of types with type arguments containing variables
|
|
(like `Delegate<T>` from the example above), but after body transformation they should be resolved to the corrected symbols from the
|
|
scopes with substituted type arguments.
|
|
|
|
See `FirCallCompletionResultsWriterTransformer.updateSubstitutedMemberIfReceiverContainsTypeVariable` for details.
|
|
|
|
#### Delegation inside PCLA
|
|
See the relevant part inside the [document](pcla.md) on PCLA.
|