Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin hard-failing an up-cast to an inferred (in-site) parameter

Tags:

kotlin

I am not sure if 'hard-failing' is the right word, but here is the problem I am facing. And it's taken me quite some time to reproduce this to the smallest possible example, so here it goes:

class BaseParameterizedType<T>

fun <U: BaseParameterizedType<*>> getSpecific(clazz: KClass<in U>) : U {
     TODO()
}

fun example(arg: KClass<out BaseParameterizedType<*>>)) {
    getSpecific(arg.innerType)
}

Ok, so the code above fails at the 'TODO', but if it wasn't there and if the function returned normally, then it definitely fails with a null pointer exception. I tried hard to figure out what was going wrong, so I turned to the decompiled Java code (from the kotlin bytecode):

public static final void example(@NotNull KClass arg) {
  Intrinsics.checkParameterIsNotNull(arg, "arg");
  getSpecific(arg.getInnerType());
  throw null;  // <-- The problem
}

If I change the function signature of getSpecific(clz: KClass<in U>) : U to any of these forms:

  1. getSpecific(clz: KClass<out U>) : U
  2. getSpecific(clz: KClass<U>) : U
  3. getSpecific(clz: KClass<in U>) : BaseParameterizedType<*>

or even the function to example(arg: KClass<out BaseParameterizedType<*>) or example(arg: KClass<BaseParameterizedType<*>>), then the generated code is:

public static final void example(@NotNull KClass arg) {
  Intrinsics.checkParameterIsNotNull(arg, "arg");
  getSpecific(arg.getInnerType());
}

Now, let's say at the call-site, I change it to:

getSpecific(BaseParameterizedType::class)

then this also DOES NOT generate the throw null clause. So, I'm guessing this has something to do with kotlin assuming that this cast will always fail or that there is indeterminate information available to make the inference?

So, we know that arg.innerType is KClass<out BaseParameterizedType<*>> and we use it at a site accepting KClass<in BaseParameterizedType<*>>, so why isn't U inferred to BaseParamterizedType<*>>. That is literally the only type that will ever match.

At the same time, I think just generating a throw null statement is unbelievably difficult to debug. The stacktrace would just point to the line where there is getSpecific and good luck figuring out where the null pointer exception came from.

like image 695
Rohan Prabhu Avatar asked Oct 20 '17 16:10

Rohan Prabhu


2 Answers

Just as @hotkey mentioned, out means in Nothing and Nothing will throw null.So I do some tests like this:

fun main(args: Array<String>) {
    tryToReturnNothing()
}

fun tryToReturnNothing(): Nothing{
    TODO()
}

Generate ->

   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      tryToReturnNothing();
      throw null;  // here
   }

   @NotNull
   public static final Void tryToReturnNothing() {
      throw (Throwable)(new NotImplementedError((String)null, 1, (DefaultConstructorMarker)null));
   }

Considering the type of null is Nothing?, we can return Nothing? instead of Nothing. So I change U into U?, and then the throw null clause disappear:

fun <U: BaseParameterizedType<*>> getSpecific(clazz: KClass<in U>) : U? { // see here: change U to U?
    TODO()
}

fun example(arg: KClass<out BaseParameterizedType<*>>) {
    getSpecific(arg)
}

Generate ->

   @Nullable
   public static final BaseParameterizedType getSpecific(@NotNull KClass clazz) {
      Intrinsics.checkParameterIsNotNull(clazz, "clazz");
      throw (Throwable)(new NotImplementedError((String)null, 1, (DefaultConstructorMarker)null));
   }

   public static final void example(@NotNull KClass arg) {
      Intrinsics.checkParameterIsNotNull(arg, "arg");
      getSpecific(arg);
   }
like image 21
fashare2015 Avatar answered Oct 12 '22 18:10

fashare2015


This is a known issue regarding the the type inference corner case handling when the inferred type is Nothing (and it is in your case):

enter image description here

The inference behaves in this way because of a coercion attempt for the projections KClass<in U> and KClass<out BaseParameterizedType<*>>.

Basically, an out-projected type at the same time means in Nothing (because the actual type argument can be any of the subtypes, and nothing can be safely passed in). So, to match KClass<out BaseParameterizedType<*>> with KClass<in U> the compiler chooses U := Nothing, implying that the function call returns Nothing as well.

Remark: a Foo<out Any> projection cannot match Foo<in T> with T := Any, because the actual type argument of the value passed for Foo<out Any> can be, for example, Int. Then, if Foo<T> accepts T in some of its functions, allowing the aforementioned match will also allow you to pass Any instances to where Foo<Int> does not expect them. Actually, in Nothing becomes the only way to match them, because of the unknown nature of the out-projected type.

After that, for a Nothing-returning function call, the compiler inserts that throw null bytecode to make sure the execution does not proceed (evaluating a Nothing-typed expression is supposed to never finish correctly).

See the issues: KT-20849, KT-18789

like image 62
hotkey Avatar answered Oct 12 '22 19:10

hotkey