```public fun FirExtension.createMemberFunction( ...
# compiler
m
Copy code
public fun FirExtension.createMemberFunction(
    owner: FirClassSymbol<*>,
    key: GeneratedDeclarationKey,
    name: Name,
    returnTypeProvider: (List<FirTypeParameter>) -> ConeKotlinType,
    config: SimpleFunctionBuildingContext.() -> Unit = {}
): FirNamedFunction
I construct a
ConeSubstitutor
object in the lambda I pass to
returnTypeProvider
, and I need to use it in
config
. Should I just use
SimpleFunctionBuildingContext
directly?
Copy code
var parameterSubstitution: ConeSubstitutor? = null
val companionMethod = this.createMemberFunction(
    // ...other arguments
    returnTypeProvider = { typeParameters ->
        val typeArgs = typeParameters
            .map(FirTypeParameter::symbol)
            .map(FirTypeParameterSymbol::toConeType)

        parameterSubstitution = substitutorByMap(
            outerClass.typeParameterSymbols
                .zip(typeArgs)
                .toMap(),
            this.session,
        )

        // ...
    },
) {
    val functionBuilder = this
    // contains calls to `parameterSubstitution.substituteOrNull`
}
This is what I’ve drafted so far, which I hate. I don’t want to initially set
parameterSubstitution
to
null
and assign a
ConeSubstitutor
later.
d
returnTypeProvider
is executed after the
config
, inside the
SimpleFunctionBuildingContext.build
call. So your option won't work. What do you need substitutor for?
m
I’m trying to copy over all type parameters.
Copy code
@MyAnnotation
class MyClass<T: MyInterface>(val impl: T)
Because I’m trying to generate a constructor overload.
Copy code
@MyAnnotation
class MyClass<T: MyInterface>(val impl: T) {
    companion object {
        operator fun <T: MyInterface> invoke(impl: T): T {
//                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            // logic
        }
    }
}
I thought I needed a substitutor to do that. Is there a better way?
d
You need to do it the other way around: 1. in
config
lambda you declare type parameters for your function 2. in
returnTypeProvider
you receive FIR type paramerers, which builder created for you, create a substitution and substitute the return type
Copy code
fun FirExtension.test(
    classSymbol: FirClassSymbol<*>,
    companionSymbol: FirClassSymbol<*>,
    key: GeneratedDeclarationKey,
    name: Name,
) {
    fun createSubstitutor(ownTypeParameters: List<FirTypeParameterRef>): ConeSubstitutor {
        return ConeSubstitutorByMap.create(
            classSymbol.typeParameterSymbols.zip(ownTypeParameters.map { it.symbol.toConeType() }).toMap(),
            session,
            allowIdenticalSubstitution = false
        )
    }

    createMemberFunction(
        owner = companionSymbol,
        key = key,
        name = name,
        config = {
            // ...
            for (typeParameterSymbol in classSymbol.typeParameterSymbols) {
                typeParameter(typeParameterSymbol.name) {
                    for (classBound in typeParameterSymbol.resolvedBounds) {
                        bound {
                            val substitutor = createSubstitutor(it)
                            substitutor.substituteOrSelf(classBound.coneType)
                        }
                    }
                }
            }
        },
        returnTypeProvider = { typeParameters ->
            val substitutor = createSubstitutor(typeParameters)
            TODO()
        }
    )
}
m
Copy code
returnTypeProvider = { typeParameters ->
    val substitutor = createSubstitutor(typeParameters)
    TODO()
}
I cannot pass the
typeParameters: List<FirTypeParameter>
to
createSubstitutor(ownTypeParameters: List<FirTypeParameterRef>)
though, can I? Also, why can’t I just create one substitutor and reuse it throughout the rest of the function?
d
?> I cannot pass the
typeParameters: List<FirTypeParameter>
to
createSubstitutor(ownTypeParameters: List<FirTypeParameterRef>)
though, You can.
List
is covariant type (it has
out
), so since
FirTypeParameter <: FirTypeParameterRef
it's true that
List<FirTypeParameter> <: List<FirTypeParameterRef>
. As a proof, the code is green on my side:
Also, why can’t I just create one substitutor and reuse it throughout the rest of the function?
To create a substitutor you need to have an access to created type parameters. And there is no single place to obtain them, which will be guaranteed to be executed before others. Substitutor is a small object with a small lifespan, it's creation is trivial. So if you care for performance here, please don't. The additional cost is negligible.
If you really want to have the full control over the function creation, you could use the
buildNamedFunction { ... }
API (used by the compiler itself) instead of
createMemberFunction
(which is simple version for plugin authors). But I don't recommend to.
➕ 1
m
What‘s the difference between
primaryConstructor.typeParameters
and
outerClass.typeParameterSymbols
where
Copy code
val outerClass: FirClassSymbol<*>
val primaryConstructor: FirConstructor

fun primaryConstructor(classItem: FirClassSymbol<*>): FirConstructor? {
    @OptIn(
        SymbolInternals::class,
        DirectDeclarationsAccess::class,
    )
    return classItem.fir.declarations
        .filterIsInstance<FirConstructor>()
        .firstOrNull(FirConstructor::isPrimary)
}
Does
TypeParameterBuildingContext#bound
pass the same
List<FirTypeParameterRef>
to the argument function while looping through
outerClass.typeParameterSymbols
?
Copy code
val functionBuilder: SimpleFunctionBuildingContext = this
for (typeParam in outerClass.typeParameterSymbols) {
    functionBuilder.typeParameter(typeParam.name) {
        val typeParamBuilder = this
        for (bound in typeParam.resolvedBounds) {
            typeParamBuilder.bound { typeParamRefs -> /* ... */ }
        }
    }
}
Also I’ve just discovered this existing function
Copy code
fun FirClassSymbol<*>.primaryConstructorIfAny(session: FirSession): FirConstructorSymbol? {
    return constructors(session).find(FirConstructorSymbol::isPrimary)
}
I think I’ll use it instead of my handrolled
primaryConstructor
. There aren’t any edge cases I should be aware of, right?
I suspect
TypeParameterBuildingContext#bound
and
FirExtension#createMemberFunction @param returnTypeProvider
are passed the same
List<FirTypeParameter>
but the architecture seems to force me to construct a new substitutor for it each time it’s given? What’s even going on?
d
What‘s the difference between
primaryConstructor.typeParameters
and
outerClass.typeParameterSymbols
Semantically constructors are kinda of static functions, which don't have access to parameters of a class. Because of that constructors have their own set of type parameters.
Also I’ve just discovered this existing function. I think I’ll use it instead of my handrolled primaryConstructor. There aren’t any edge cases I should be aware of, right?
This function is completely fine and safe to use.
I suspect
TypeParameterBuildingContext#bound
and
FirExtension#createMemberFunction @param returnTypeProvider
are passed the same
List<FirTypeParameter>
but the architecture seems to force me to construct a new substitutor for it each time it’s given?
Yes, that's correct. All types in the function signature (extension receiver, value parameter types, return type, type parameter bounds) could reference type parameters of the function. But you cannot produce the
ConeKotlinType
for type parameter, if
FirTypeParameter
for it was not created yet. And the logic of these
create...
functions is that you first configure the builder using lambdas and then call
build()
, which actually creates all the fir elements, including `FirTypeParameter`s. So to break this loop and allow to reference type parameters in all these types, there is a way to postpone calculation of these types until type parameter instances would be created (so here are
bound
,
returnTypeProvider
and other functions in DSL which accept lambda).
It is true, that you need to create a substitutor in each of these lambdas, but it's just a little API annoyance.
m
It feels like I’m writing absolutely crazy code I couldn’t convince myself that I was doing the right thing. I wonder why the API isn’t split into parts for things to set up before the type parameters have been locked in, and things to set up afterwards. But thank you for your explanation.