min
09/03/2026, 6:20 AMpublic 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?min
09/03/2026, 6:23 AMvar 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.dmitriy.novozhilov
09/03/2026, 6:40 AMreturnTypeProvider is executed after the config, inside the SimpleFunctionBuildingContext.build call. So your option won't work.
What do you need substitutor for?min
09/03/2026, 6:46 AM@MyAnnotation
class MyClass<T: MyInterface>(val impl: T)
Because I’m trying to generate a constructor overload.
@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?dmitriy.novozhilov
09/03/2026, 6:57 AMconfig 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
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()
}
)
}min
09/03/2026, 7:10 AMreturnTypeProvider = { 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?dmitriy.novozhilov
09/03/2026, 7:12 AMtypeParameters: 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:dmitriy.novozhilov
09/03/2026, 7:15 AMAlso, 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.
dmitriy.novozhilov
09/03/2026, 7:18 AMbuildNamedFunction { ... } API (used by the compiler itself) instead of createMemberFunction (which is simple version for plugin authors). But I don't recommend to.min
09/03/2026, 7:34 AMprimaryConstructor.typeParameters and outerClass.typeParameterSymbols where
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)
}min
09/03/2026, 7:38 AMTypeParameterBuildingContext#bound pass the same List<FirTypeParameterRef> to the argument function while looping through outerClass.typeParameterSymbols?
val functionBuilder: SimpleFunctionBuildingContext = this
for (typeParam in outerClass.typeParameterSymbols) {
functionBuilder.typeParameter(typeParam.name) {
val typeParamBuilder = this
for (bound in typeParam.resolvedBounds) {
typeParamBuilder.bound { typeParamRefs -> /* ... */ }
}
}
}min
09/03/2026, 7:40 AMfun 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?min
09/03/2026, 8:39 AMTypeParameterBuildingContext#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?dmitriy.novozhilov
09/03/2026, 9:03 AMWhat‘s the difference betweenSemantically 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.andprimaryConstructor.typeParametersouterClass.typeParameterSymbols
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 suspectYes, 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 theandTypeParameterBuildingContext#boundare passed the sameFirExtension#createMemberFunction @param returnTypeProviderbut the architecture seems to force me to construct a new substitutor for it each time it’s given?List<FirTypeParameter>
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).dmitriy.novozhilov
09/03/2026, 9:04 AMmin
09/03/2026, 9:41 AM