https://kotlinlang.org logo
Title
y

Youssef Shoaib [MOD]

07/26/2022, 12:14 PM
Is there any compile-time-evaluated way to check if a reified type is a specific class?
T::class == Desired::class
doesn't seem to be evaluated at compile time. I can also do
valueOfTypeDesired is T && !(valueOfSupertypeOfDesired is T)
but again this isn't evaluated at compile time. I'm interested not only because of performance but for a value-class optimization use case (can give more details if interested).
t

Tóth István Zoltán

07/26/2022, 5:01 PM
I don't think there is, for two reasons. First, the specification explicitly says that
reified
is about making the type runtime-available. IMHO, it means that when you use
reified
the exact type will be available during run-time, not just some generic, erased super-type. There is a reason, why
reified
applies only to
inline
functions. Second, all the checks you've written above are run-time checks, the only way to have a compile type check is to build a type system which enforces the type and/or use a non-inline function. Maybe your best bet is a compiler plugin, after playing around with them a bit it is quite easy to write one. (Disclaimer: I might be wrong, not an expert, ask your doctor, etc...)
y

Youssef Shoaib [MOD]

07/27/2022, 11:39 PM
For context, the compiler inlines any
value is ValueClass
checks and eliminates any branches that will never run as a result. This happens even in inline functions when
value
is of some type argument
T
. Is this optimization documented somewhere? because any other constant conditions never cause code to collapse like that. E.g.:
public inline fun <T : Any> printlnWithoutBoxing(value: T) {
  // This `is` check gets inlined by the compiler if `value` is statically known to be Result
  println(if(value is Result<*>) (value as Result<*>).toString() else value)
}

public fun test(){
  val myResult = Result.success(42)
  val myResultAny: Any = myResult
  println(myResult)
  println(myResultAny)
}

public fun test2(){
  val myResult = Result.success(42)
  val myResultAny: Any = myResult
  printlnWithoutBoxing(myResult)
  printlnWithoutBoxing(myResultAny)
}
The else branch disappears for the calls in test2, even in the
myResultAny
case! In fact, I have to use
myResultAny
as a "real" Any (i.e. pass it to a non-inline function that expects Any or a generic
T
, or pass it to an inline function that uses that argument as a "real" Any from the aforementioned criteria). The behavior is surprising, but it is useful. Is this how the compiler is expected to behave? (Decompilation of example in 🧵 )
Decompiled using cfr-0.152:
public final class MainKt {
    public static final void test() {
        Object myResult = Result.constructor-impl((Object)42);
        Result myResultAny = Result.box-impl((Object)myResult);
        Result result = Result.box-impl((Object)myResult);
        System.out.println(result);
        System.out.println(myResultAny);
    }

    public static final void test2() {
        Object myResult;
        Object myResultAny = myResult = Result.constructor-impl((Object)42);
        Object value$iv = myResult;
        boolean $i$f$printlnWithoutBoxing = false;
        System.out.println((Object)Result.toString-impl((Object)value$iv));
        boolean $i$f$printlnWithoutBoxing2 = false;
        System.out.println((Object)Result.toString-impl((Object)myResultAny));
    }

    public static final <T> void printlnWithoutBoxing(@NotNull T value) {
        Intrinsics.checkNotNullParameter(value, (String)"value");
        boolean $i$f$printlnWithoutBoxing = false;
        System.out.println((Object)(value instanceof Result ? Result.toString-impl((Object)((Result)value).unbox-impl()) : value));
    }
}
And, if I use
myResultAny
as an actual `Any`:
public fun test2(){
  val myResultAny: Any = Result.success(42).id() // ensures boxing
  printlnWithoutBoxing(myResultAny)
}
public fun <T> T.id(): T = this
it does indeed box, and you can see that in that case (i.e. when value isn't statically known to be a
Result
) the
else
branch persists:
public static final void test2() {
    Result myResultAny = MainKt.id(Result.box-impl((Object)Result.constructor-impl((Object)42)));
    boolean $i$f$printlnWithoutBoxing = false;
    System.out.println((Object)(myResultAny instanceof Result ? Result.toString-impl((Object)myResultAny.unbox-impl()) : myResultAny));
}
s

shikasd

07/28/2022, 8:31 AM
My guess is that the compiler should be fine optimizing this, as the branches don't have any side effects. About inline class optimization though, note that in your case the values are still boxed even if the comparison is discarded, kinda destroying the purpose of value classes in the process If you want compile time safety, I suggest just using overloads of inline functions accepting known types