Back to Compose Multiplatform

Delegated Property Inference

docs/fir/delegated_property_inference.md

2.3.208.7 KB
Original Source

FIR/Delegated property inference

See also: Kotlin Spec: Delegated property declaration and some Common inference terms definition

In many ways, delegated property inference works as a simpler version of PCLA, so it's worth beginning with 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.

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 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 on PCLA.