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.fooZac 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 FirCliSessionZac 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) duringgetCallableNamesForClassgenerateFunctions
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