Is this a compiler bug?

I tried to help someone on a discord server who posted a snippet that didn’t compile, but I wasn’t able to find an answer to why it wouldn’t compile. I managed to generalize it to a minimal runnable (or, in this case, not runnable) example:

object A
object B

fun A.foo() = println("Test A")
fun B.foo() = println("Test B")

fun main() {
    var test: A? = A
    test = null
    test?.foo()
}

(playground link: Kotlin Playground: Edit, Run, Share Kotlin Code Online)
the way I expected it to work is that test is of type A?, so no matter what happens, .foo() called on test would refer to the A.foo() overload. But since test is set to null before the safe call, the call simply shouldn’t execute and nothing will be printed.

Instead, the snippet doesn’t compile with the error: “Overload resolution ambiguity: public fun A.foo(): Unit defined in root package in file File.kt public fun B.foo(): Unit defined in root package in file File.kt”. So basically, the compiler cannot decide whether test?.foo() refers to A.foo() or to B.foo(). Which makes no sense since the type of test is explicitly declared as A? and there’s no mention of the B type whatsoever in the main() function.

But I think the weirdest part is what happens when you remove the test = null line:

// same code as before goes here

fun main() {
    var test: A? = A
    test?.foo()
}

(playground link: Kotlin Playground: Edit, Run, Share Kotlin Code Online)
Now, the code will compile just fine, and then obviously print Test A. The same happens when you change the initial value of test to null (playground link: Kotlin Playground: Edit, Run, Share Kotlin Code Online), it’ll then just not run the safe call and output nothing. Same goes for the val variants of those.

It seems like there’s some weird type inference happening when null is later assigned to the var, but that makes no sense since the type of test is explicitly declared, so type inference shouldn’t have a say in this. The fact that adding just an assignment statement makes the compiler fail on a different statement is what makes it seem like a compiler bug to me.

But I don’t know, is there someone who has more insight on this topic and could explain this to me?

I tested this with all available Kotlin versions on the playground, and also with JS Legacy and JS IR, all of them showed the same behaviour.

1 Like

Yes, it seems like a bug or a narrow corner case of smart-cast. Please report this on YouTrack.

3 Likes

Here’s a runnable/editable example just in case anyone else is as lazy as me and doesn’t want to open any links :wink:

object A
object B

fun A.foo() = println("Test A")
fun B.foo() = println("Test B")

fun main() {
    var test: A? = A
    // Try uncommenting the line below.
    //test = null
    test?.foo()
}
1 Like

There’s definitely some issue with smart casting going on here, as explicitly casting test behaves as expected:

fun main() {
    var test: A? = A
     test = null
    (test as A?)?.foo()
}

Elvis cast also works:

(test as? A)?.foo()

And, most surprisingly, initialising test as null also works:

fun main() {
    var test: A? = null
    test?.foo()
}

So that test = null somehow completely manages to throw off the inference…
EDIT: Just realized that the OP already included the last snippet.

1 Like

Well, this is not really that strange. What’s happening here is basically that the compiler infers test to be a “null” (technically it is Nothing?). It knows it is not just A?, it is guaranteed to be null. And technically speaking, we then can use B.foo() for the code: test?.foo(). This is similar to this code which also generates ambiguity error: null?.foo().

But of course, this is a very counter-intuitive behavior and I think it can be considered a bug.

The null literal has type Nothing?. Therefore, assigning null causes the variable to be smart cast to Nothing?. If this is changed, then the following will not compile:

fun main() {
    var s: CharSequence = StringBuilder()
    s = ""
    s.stringMethod()
}

fun String.stringMethod() {}

Yes, this is the case, and from type point of view it is even valid. But casting null to Nothing seems to be wrong.