Hi. I experimented a bit with K2s `FirExpressionRe...
# compiler
m
Hi. I experimented a bit with K2s
FirExpressionResolutionExtension
. I mainly "misused" it for some code modifications becuase implicit bodies are resolved at this phase. When I try to build code for throwing an ( Java ) exception ( let's say java.lang.Exception ), it works as long as I only use the noarg constructor.
Copy code
context (FirSession)
fun buildExceptionThrow(): FirThrowExpression = buildThrowExpression {
    val exceptionId = ClassId.fromString("java/lang/Exception")
    val constructor = exceptionId.noArgConstructor

    exception = buildFunctionCall {
        typeRef = buildResolvedTypeRef {
            type = exceptionId.createConeType(this@FirSession)
        }
        calleeReference = buildResolvedNamedReference {
            name = constructor.name
            resolvedSymbol = constructor
        }
    }
}

context (FirSession)
val ClassId.noArgConstructor: FirConstructorSymbol
    get() = symbolProvider
        .getClassDeclaredConstructors(this)
        .firstOrNull {
            it.valueParameterSymbols.isEmpty()
        } ?: throw IllegalStateException("Noarg constructor not found!")
Now I want to use the message constructor ( org.jetbrains.kotlin.fir.java.declarations.FirJavaConstructor@68cb8e52: public constructor(p0: java.lang.String): R|java/lang/Exception| ).
Copy code
@OptIn(SymbolInternals::class)
val FirConstructorSymbol.takesSingleString: Boolean
    get() = with(valueParameterSymbols) {
        (size == 1) && first().fir.returnTypeRef.coneType.isString
    }
Filtering the parameters is kind of tricky because simply calling the
first().resolvedReturnType.isString
on an
FirJavaTypeRef
would immediately result in an
ClassCastException
(
java.lang.ClassCastException: class org.jetbrains.kotlin.fir.types.jvm.FirJavaTypeRef cannot be cast to class org.jetbrains.kotlin.fir.types.FirResolvedTypeRef (org.jetbrains.kotlin.fir.types.jvm.FirJavaTypeRef and org.jetbrains.kotlin.fir.types.FirResolvedTypeRef are in unnamed module of loader 'app'
). Maybe someone knows a better way to check the types than having to access symbol internals.
Copy code
context (FirSession)
fun buildExceptionThrow(
    message: String
): FirThrowExpression = buildThrowExpression {
    val exceptionId = ClassId.fromString("java/lang/Exception")
    val constructor = exceptionId.singleStringConstructor

    exception = buildFunctionCall {
        typeRef = buildResolvedTypeRef {
            type = exceptionId.createConeType(this@FirSession)
        }
        calleeReference = buildResolvedNamedReference {
            name = constructor.name
            resolvedSymbol = constructor
        }
        argumentList = buildArgumentList {
            arguments.add(message asResolved ConstantValueKind.String)
        }
    }
}

context (FirSession)
infix fun <Type> Type.asResolved(
    kind: ConstantValueKind<Type>
): FirConstExpression<Type> =
    buildConstExpression(null, kind, value = this).apply {
        replaceTypeRef(
            buildResolvedTypeRef {
                type = kind.expectedConeType(this@FirSession)
            }
        )
    }

context (FirSession)
val ClassId.singleStringConstructor: FirConstructorSymbol
    get() = symbolProvider
        .getClassDeclaredConstructors(this)
        .firstOrNull(FirConstructorSymbol::takesSingleString)
        ?: throw IllegalStateException("No constructor with single String argument found!")
When I pass the argument in the
FirArgumentListBuilder
as an
FirConstExpression
, I get
java.lang.IllegalStateException: Expected FirResolvedTypeRef with ConeKotlinType but was FirJavaTypeRef java.lang.String
. I already tried to replace the typeRef with a
FirJavaTypeRef
, but to build one using
buildJavaTypeRef
I need an instance of
JavaType
- and I'm not sure how to get that. Does someone have experience with calling Java functions ( in this case a constructor ) in FIR?
d
It's illegal to extract callable symbols via symbol itself (
symbolProvider.getClassDeclaredConstructors(this)
) Instead of it you should use scopes (
symbol.unsubstitutedScope(session)
m
It works! I replaced it with
Copy code
context (FirSession)
val ClassId.singleStringConstructor: FirConstructorSymbol get() =
    when(val symbol = symbolProvider.getClassLikeSymbolByClassId(this)) {
        is FirClassSymbol -> symbol.findConstructor { takesSingleString }
        else -> null
    } ?: throw IllegalStateException("No constructor with single String argument found!")

context (FirSession)
fun FirClassSymbol<*>.findConstructor(
    predicate: FirConstructorSymbol.() -> Boolean
): FirConstructorSymbol? = 
    unsubstitutedScope(this@FirSession, ScopeSession(), true)
        .getDeclaredConstructors()
        .firstOrNull(predicate)
Now the types changed. Instead of
org.jetbrains.kotlin.fir.java.declarations.FirJavaConstructor@4a36a35d: public constructor(p0: java.lang.String): R|java/lang/Exception|
it's now
org.jetbrains.kotlin.fir.declarations.impl.FirConstructorImpl@4f1f2f84: public constructor(p0: R|kotlin/String!|): R|java/lang/Exception|
. Since it's a Java method, the constructor takes a platform-type String. To make it work for this simple scenario I just checked the rendered text representation.
Copy code
val FirConstructorSymbol.takesSingleString: Boolean
    get() = with(valueParameterSymbols) {
        (size == 1) && first().resolvedReturnType.renderReadable() == "String!"
    }
Are there any utility methods to check platform types? ( Something like FirTypeRef.isBuiltinType(classId: ClassId, isNullable: Boolean) from FirTypeUtils.kt )
d
Copy code
unsubstitutedScope(this@FirSession, ScopeSession(), true).getDeclaredConstructors()
Actually, for constructors it's enough to use declared scope, it does not requre scope session
Something like
FirTypeRef.isBuiltinType(classId: ClassId, isNullable: Boolean)
FirTypeRef
represents some place in tree which have type, not type itself If you want to use types you need to extract
ConeKotlinType
from it (
typeRef.coneType
or
typeRef.coneTypeSafe<ConeKotlinType>()
if this particular type ref may be not resolved at moment you ask for it) Then you can use various number of utilities which exist for cone types If you want to check if this type with type constuctor with specific id then it's enough to check
coneType.classId == classId
Also if you work with potentially platform (flexible) types, this may be useful: •
coneType.lowerIfFlexible()
unwraps flexible type to lower bound (
String! = (String..String?)
to
String
) •
coneType is ConeFlexibleType
to check if type is really flexible
m
The
lowerIfFlexible()
I found in org/jetbrains/kotlin/types/flexibleTypes.kt extends only
KotlinType
( which isn't a supertype of
ConeKotlinType
). I ended up with using
lowerBoundIfFlexible()
from org/jetbrains/kotlin/fir/types/ConeTypeUtils.kt which extends
ConeKotlinType
.
d
Yeah, I meant exactly that method I wrote those names by memory without referencing to actual codebase
m
But it was very helpful at all! The only thing I can't figure out is where to get that declared scope from (?) There's the
declaredMemberScope()
extension for
FirClassSymbols
but it seems to be far away from what I'm looking for ( And would again cause a
ClassCastException
because the class symbol for
java.lang.Exception
originally contains a Java parameter reference of type
FirJavaTypeRef
).
d
Oh, for Java classes it's required to use use-site scopes I forgot that declared scope for then does not contain enhancement (it's a process which converts Java types to Kotlin types, with nullability, class mapping and other stuff)
m
Ok, sounds like using a
ScopeSession
like
Copy code
unsubstitutedScope(this@FirSession, ScopeSession(), true)
        .getDeclaredConstructors()
is currently the best solution for working with Java classes.