Zac Sweers
01/13/2025, 5:19 AMFirSupertypeGenerationExtension
causes unrelated `FirDeclarationGenerationExtension`s to be in different resolve phases during generateNestedClassLikeDeclaration
callbacks? A very short failing example:
class ExampleDeclarationGenerator(session) : FirDeclarationGenerationExtension(session) {
override fun generateNestedClassLikeDeclaration(
owner: FirClassSymbol<*>,
name: Name,
context: NestedClassGenerationContext,
): FirClassLikeSymbol<*>? {
val firstReturnType = owner.declarationSymbols.filterIsInstance<FirNamedFunctionSymbol>()
.first()
.resolvedReturnTypeRef // This line
}
}
If this declaration generator is run on its own, it's able to access resolvedReturnTypeRef
fine during this callback.
However, if any FirSupertypeGenerationExtension
is present and running, the above snippet now fails even if it's looking at unrelated classes.
Caused by: org.jetbrains.kotlin.utils.exceptions.KotlinIllegalArgumentExceptionWithAttachments: Unexpected returnTypeRef. Expected is FirResolvedTypeRef, but was FirUserTypeRefImpl
at org.jetbrains.kotlin.fir.symbols.impl.UtilsKt.errorInLazyResolve(Utils.kt:42)
at org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol.calculateReturnType(FirCallableSymbol.kt:32)
at org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol.getResolvedReturnTypeRef(FirCallableSymbol.kt:24)
I couldn't find anything in FIR's docs on phases that explained this behavior, but maybe there's something I missed?dmitriy.novozhilov
01/13/2025, 8:31 AMFirDeclarationGenerationExtensions
is called. It's invocation is based on the lookup scheme, so any time the compiler looks for some name in some scope, it may trigger the generation extension from the related scope.
There are guarantees only for "the earliest phase when the generation might be triggered":
• during supertype resolution phase for classes and nested classes
• during status resolution phase for member callables
• during contract resolution phase for top-level callables
Note that it might happen later (up to fir2ir, if the declaration you are generating was not referenced from the code in any way)
So in your case there was a situation that generateNestedClassLikeDeclaration
was not called during the supertypes stage. But adding supertype generation extensions causes the compiler generate all classes, as supertype generation extensions might also add supertypes to them too.
Generally you main problem in calling resolvedReturnTypeRef
, as it's illegal in the general casedmitriy.novozhilov
01/13/2025, 8:32 AMclass Box<T>
open class A {
class Nested // Generated
}
class B : A() {
class C : Box<Nested>()
}
Here generation of all nested classes of A
will be triggered before entering the scope of class B
, as inside it all nested classes from supertypes are visible without qualifierZac Sweers
01/13/2025, 2:20 PMdmitriy.novozhilov
01/13/2025, 2:21 PMdmitriy.novozhilov
01/13/2025, 2:22 PMclass Some {
fun foo() = produceValueOfMySpecificType()
}
In this case the return type of foo
is unknown until the implicit body resolve stageZac Sweers
01/13/2025, 2:25 PMZac Sweers
01/13/2025, 2:26 PMZac Sweers
01/13/2025, 2:26 PMdmitriy.novozhilov
01/13/2025, 2:29 PMZac Sweers
01/13/2025, 2:30 PMdmitriy.novozhilov
01/13/2025, 2:32 PMopen class A {
fun foo() {}
}
class B : A() {
// fake-override fun foo() // generated in IR class for `B`
fun test(b: B) {
b.foo() // <------
}
In this case the call will be resolved to A.foo
at the frontend, but the fir2ir will generate fake-override B.foo
and the resolved symbol of corresponding IrCall
will point to B.foo
Zac Sweers
01/13/2025, 2:32 PMdmitriy.novozhilov
01/13/2025, 2:33 PMdmitriy.novozhilov
01/13/2025, 2:35 PMIntGetter
name for getNestedClassifiersNames/getTopLevelClassIds
if it could be called at the moment when anything is actually is resolved (at the beginning of supertypes stage)?Zac Sweers
01/13/2025, 2:36 PMZac Sweers
01/13/2025, 2:42 PMZac Sweers
01/13/2025, 2:42 PMdmitriy.novozhilov
01/13/2025, 2:43 PMint
function for three purposes:
1. Understand if the class should be generated or not and compute it's name
2. Generate the Getter<Int>
supertype
3. Generate override fun get(): ...
with a proper return type
The third case could we workarounded with IR approach as I described above.
The second one could we solved with a quirck:
• generate IntGetter
without the supertype
• add the supertype with FirSupertypeGenerationExtension
, as it has special typeResolver, which allows to resolve some user type (the type which is present in the code, but not resolved by the compiler yet).
This only caveat with this approach that this type resolver will resolve the type in context of supertypes of a class you are processing, so if the function is defined somewhere else (top-level/different class), the result of resolution might differ.
The first case is just unsolvable, and I recommend to use the annotations approach insteadZac Sweers
01/13/2025, 2:45 PMZac Sweers
01/13/2025, 2:45 PMdmitriy.novozhilov
01/13/2025, 2:46 PMdmitriy.novozhilov
01/13/2025, 2:46 PMFirSupertypeGenerationExtension.computeAdditionalSupertypesForGeneratedNestedClass
for the #2Zac Sweers
01/13/2025, 2:47 PMdmitriy.novozhilov
01/13/2025, 2:48 PMZac Sweers
01/13/2025, 2:48 PMZac Sweers
01/13/2025, 2:49 PMdmitriy.novozhilov
01/13/2025, 2:53 PMval isDefined = functionSymbol.fir.returnTypeRef is FirUserTypeRef
val isImplicit = functionSymbol.fir.returnTypeRef is FirImplicitTypeRef
To access .fir
you need to opt-in to SymbolInternals::class
, but it's safe in your case, as you actually interested in the unresolved typeZac Sweers
01/15/2025, 2:39 AMe: ExampleClass.kt:15:13 Object 'ExampleClass.Companion' is not abstract and does not implement abstract member 'shouldBeFakeOverride'.
dmitriy.novozhilov
01/15/2025, 1:28 PMZac Sweers
01/15/2025, 3:48 PMZac Sweers
01/15/2025, 4:42 PMZac Sweers
01/15/2025, 4:42 PMdmitriy.novozhilov
01/15/2025, 7:19 PM@RedactedType
class Example(val redactedString: String) {
@RedactedType.Factory
interface Factory {
fun create(redactedString: String)
}
companion object : /* generated */ Factory
}
The fake-override will indeed be generated, but the problem is that this code is not valid, as companion doesn't implement the create
.
It would work if you add a no-op implementation to it:
interface Factory {
fun create(...) {}
// or
fun create(...): Something = null!!
}
dmitriy.novozhilov
01/15/2025, 7:23 PMZac Sweers
01/15/2025, 7:48 PMZac Sweers
01/15/2025, 7:49 PMdmitriy.novozhilov
01/15/2025, 8:55 PMdmitriy.novozhilov
01/15/2025, 8:56 PMZac Sweers
01/15/2025, 8:56 PMZac Sweers
01/15/2025, 10:23 PMinterface Base<T> {
fun create(): T
}
interface Factory : Base<ExampleClass>
etc.dmitriy.novozhilov
01/16/2025, 7:44 AMopen
2. The base function hasn't body -> it definitely has a return type (and it is abstract
). In this case you need to generate an override, but you can access baseFunctionSymbol.resolvedReturnType
and then just substitute it with type substitutor from super typesZac Sweers
01/16/2025, 5:48 PMZac Sweers
01/16/2025, 5:48 PMdmitriy.novozhilov
01/16/2025, 8:06 PMsubstitutor: { K -> Int, V -> String }
original type: Map<K, List<V>>
substituted type: Map<Int, List<String>
In your case you need to create a substitutor based on the supertype of your class and then apply it to types of the original function
open class Base<T> {
fun foo(): T = ...
}
class Derived : Base<Int>()
Here the substitution is {T -> Int}
To create it you need to
• take the list of type parameters of the base class (classSymbol.typeParameterSymbols
)
• assosiate it with actual types you passed in the supertype (Int
)
• create a substitutor (ConeSubstitutorByMap(parameterToTypeMap)
• substitute the type (subsitutor.substututeOrSelf(originalFunction.resolvedReturnType)
)Zac Sweers
01/16/2025, 8:17 PMZac Sweers
01/19/2025, 10:19 PMTypeResolveService
doesn't work when the plugin is run in the IDE? Or at least I've not found a case where it doesdmitriy.novozhilov
01/20/2025, 8:03 AMTypeResolveService
doesn't work, or the supertype generation extension is not called for generated classes at all?Zac Sweers
01/20/2025, 2:49 PMdmitriy.novozhilov
01/20/2025, 2:51 PMdmitriy.novozhilov
01/20/2025, 2:53 PMit’s possible to connect a debugger to the IDEIt's definitely possible, but you need an another IDE instance running from which you will connect the debugger. As an option you can build the IJ-community locally and just run the IDEA (Community) run configuration with debugger. It could quite slow and a little frustrating for the first time, but after you will get a fully functioning sandbox
Zac Sweers
01/20/2025, 2:55 PMZac Sweers
01/20/2025, 2:56 PMdmitriy.novozhilov
01/20/2025, 3:00 PMZac Sweers
01/20/2025, 3:00 PMdmitriy.novozhilov
01/20/2025, 3:01 PMval cliCompilation = session is FirCliSession
Zac Sweers
01/20/2025, 3:26 PMFirErrorTypeRef
, not nullZac Sweers
01/20/2025, 3:26 PMZac Sweers
01/20/2025, 3:28 PMZac Sweers
01/20/2025, 6:57 PMZac Sweers
01/20/2025, 6:58 PMZac Sweers
01/20/2025, 7:40 PMgetCallableNamesForClass
and then generate a function (or functions) with the "real" name(s) during generateFunctions. Feels sneaky but it is working since the information I need is available then. Would there be any downsides to this approach? Seems intentionally supported since one name can yield multiple callable declarations, but wanted to check in case the expectation is just to produce overloadsdmitriy.novozhilov
01/21/2025, 8:41 AMfor debugging other IDE issues, is there a non-println way to log to the IDE from FIR?Don't know, I prefer to just the IDE/compiler process in such cases
dmitriy.novozhilov
01/21/2025, 8:44 AMrelated - one sort of funny workaround I've found is to basically supply a placeholder name duringWDYM by "placeholder" and "real" name? There is a contract, that functions returned byand then generate a function (or functions) with the "real" name(s) duringgetCallableNamesForClass
generateFunctions
generateFunctions
should have the same callable id as passed into it, and the compiler goes to plugins only for names returned from getCallableNamesForClass
.
Seems intentionally supported since one name can yield multiple callable declarationsYes, there are overloads in the language
Zac Sweers
01/21/2025, 3:45 PMdmitriy.novozhilov
01/21/2025, 3:48 PMZac Sweers
01/21/2025, 3:48 PMZac Sweers
01/21/2025, 3:48 PMdmitriy.novozhilov
01/21/2025, 3:50 PMbnorm
01/21/2025, 4:09 PMZac Sweers
01/21/2025, 4:09 PMbnorm
01/21/2025, 4:24 PMZac Sweers
01/21/2025, 4:51 PM