Is there a reason that that adding a `FirSupertype...
# compiler
z
Is there a reason that that adding a
FirSupertypeGenerationExtension
causes unrelated `FirDeclarationGenerationExtension`s to be in different resolve phases during
generateNestedClassLikeDeclaration
callbacks? A very short failing example:
Copy code
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.
Copy code
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?
d
The reason of it is that there is no straight point of time when
FirDeclarationGenerationExtensions
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 case
BTW you potentially might trigger the same problem even without supertype generation extensions:
Copy code
class 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 qualifier
z
I see, thanks for the explanation. The goal in my case is to specifically find a function that returns a specific type as a trigger and override it in generateFunctions. Is there an alternative approach for reading return types? Another similar case is for finding one abstract function
d
Do I understand correctly that the fact if such a function exists or not determines if the class will be generated or not?
BTW did you consider the case with implicit types?
Copy code
class Some {
    fun foo() = produceValueOfMySpecificType()
}
In this case the return type of
foo
is unknown until the implicit body resolve stage
👍 1
z
Ah sorry I should’ve clarified - in that example it’s decided during getCallableNamesForClass, but the same issue re:unresolved types
Basically generating a class that extends another class (likely an interface) and needs to declare + implement an override of a specific matching function
And leave unmatched ones alone
d
Hm, if you are generating a class which inherits the base one and override functions without any changes in it's signature, why do you even need to generate this override at frontend? You can just generate the empty class at frontend, fir2ir will generate fake-overrides for all functions, and then you can replace the fake-override with a specific implementation you need
z
My understanding was overrides with bodies needed to be declared, but sounds like that assumption was wrong
d
Copy code
open 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
z
There are cases where I do want to generate a class that includes a return type from a function like above though now that I think about it. Essentially fun int(): Int Generate class IntGetter : Getter<Int> { override fun get(): T = … }
d
I'm afraid that's not possible because of implicit return types
How do you compute the
IntGetter
name for
getNestedClassifiersNames/getTopLevelClassIds
if it could be called at the moment when anything is actually is resolved (at the beginning of supertypes stage)?
z
Hm, what if explicit return types were required for this case to work?
The name in this case btw is just from the function name, not the return type
The return type is needed during the class creation just to populate the generic supertype
d
It will be still problematic You need the type of
int
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 instead
z
Perfect, I think this is doable then as 1 is already triggered on an annotation in my case (sorry I’d gotten my wires crossed with the other case). The workaround for #2 is the missing piece I needed 👌
Thanks a bunch!
d
You are welcome
Note that you need to use
FirSupertypeGenerationExtension.computeAdditionalSupertypesForGeneratedNestedClass
for the #2
z
Ah good point. Thats in 2.1.20 right?
d
I've added this functionality in May, so it should be available in 2.1.0. Maybe even in 2.0.20, I don't remember when it was branched
z
Even better!
One other question - is it possible to tell during FIR if a function has an explicit return type?
d
Copy code
val 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 type
👍 1
z
Ok I've mostly been able to adjust my plugin to adhere to the above. One interesting edge case I've run into is that if I generate a supertype onto an existing source companion object, no fake override is generated for its callable. Is that intentional? It will just fail compilation before getting to the IR phase that would implement it
Copy code
e: ExampleClass.kt:15:13 Object 'ExampleClass.Companion' is not abstract and does not implement abstract member 'shouldBeFakeOverride'.
d
Could you please provide some code example with comments about what are you generating?
z
Let me try to put together a small repro
it's a contrived example but hope it makes sense
d
Copy code
@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:
Copy code
interface Factory {
    fun create(...) {}
    // or
    fun create(...): Something = null!!
}
Note that the fake-override I'm talking about is generated not by fir, but at IR level. To be precise, it happens during IR actualization phase which happens right after fir2ir and before calling the backend
z
gotcha. I think I misunderstood how that worked. In that case I think I end up stuck in the original problem of needing to find the abstract method that needs implementing 😕. Or do you think I could do that safely during nested callable names generation?
I'm curious also why this seems only on existing companion objects. If I generate a final class or companion during FIR that implements an interface, that does get fake overrides even if the function doesn't have a body in the source interface
d
Compiler doesn't run checkers on generated declarations, only on ones which are present in sources
👍 1
Actually, I just got the idea how you can generate the override even not knowing the return type of the overridden function I'll check it tomorrow and come back to you with results
z
awesome, looking forward to it
I should mention - we only really care about the override in this case of abstract function(s). This could also mean there are cases like
Copy code
interface Base<T> {
  fun create(): T
}

interface Factory : Base<ExampleClass>
etc.
d
I tried my idea, but it seems to be too complex and actually redundant for your case. When you are generating an override there are actually two cases: 1. The base function has body -> there is no sense to generate an override (in FIR), as the base function is already
open
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 types
z
Can you unpack the type substitutor bit for me? I’ve seen APIs around this but wasn’t able to find good docs
d
Substitutor is basically a mapping from type parameters to some type, which allows you to apply it to any arbitary type
Copy code
substitutor: { 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
Copy code
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)
)
z
Got it, thanks!
is it expected that
TypeResolveService
doesn't work when the plugin is run in the IDE? Or at least I've not found a case where it does
d
Is it
TypeResolveService
doesn't work, or the supertype generation extension is not called for generated classes at all?
z
The latter I think, I believe it’s always returning null. I’ll try to verify. Having to do a lot of println debugging for this since I’m not sure it’s possible to connect a debugger to the IDE 😅
d
It seems it just unsupported in IDE part of supertype resolution implementation. Could you please create an issue?
it’s possible to connect a debugger to the IDE
It'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
z
Will do when I’m at a computer
Is there a way to detect which client is running FIR at runtime? Thinking I could just gracefully degrade in parts that don’t necessarily need to be user-visible
d
You mean you want to distinguish between the CLI and Analysis API (IDE) modes?
z
Yeah
d
Copy code
val cliCompilation = session is FirCliSession
🙇 1
z
errr sorry it always returns
FirErrorTypeRef
, not null
to clarify
🆗 1
for debugging other IDE issues, is there a non-println way to log to the IDE from FIR? I had an option in my plugin to gate logging but that's just on MessageCollector, which I don't see an analogue for in FIR
printlns do work fwiw, just curious if there's a preferred way
related - one sort of funny workaround I've found is to basically supply a placeholder name during
getCallableNamesForClass
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 overloads
d
for 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
related - one sort of funny workaround I've found is to basically supply a placeholder name during
getCallableNamesForClass
and then generate a function (or functions) with the "real" name(s) during
generateFunctions
WDYM by "placeholder" and "real" name? There is a contract, that functions returned by
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 declarations
Yes, there are overloads in the language
z
Is that contract enforced? It currently seems not, which is proving quite helpful to defer lookups of real names (for example that abstract callable lookup) until the generateFunctions callback
d
It's not checked, but I'm surprised that it works if you violate it
z
😅
It does work
d
Damn @bnorm could you please note this case somewhere? It would be nice to investigate it in the nearest time
1
b
Created KT-74594 for tracking.
thank you color 1
z
Can I make it a feature request? I'm curious what the downsides are aside from the asymmetry
b
I think that's what we need to figure out, if there are actual downsides to the asymmetry. We will update the ticket as we investigate. But please add a comment about your use case and how you depend on this behavior if you are able to share.