I have observed a weird logical change in the foll...
# compiler
a
I have observed a weird logical change in the following code while migrating from kotlin 1.9.x to 2.x.x
Copy code
fun main() {
    val vr = SnippetVR(object: Interaction {
        override fun print() {
            println("hello")
        }
    })
    val snippet = vr.create()
    snippet.print()
    println("bye")
}

interface Interaction {
    fun print()
}


class SnippetVR(val interaction: Interaction? = null) {
    fun create(): Snippet {
        return Snippet.newInstance().apply { this?.interaction = interaction }!!
    }
}


class Snippet() {
    companion object {
        fun newInstance(): Snippet? = Snippet()
    }
    var interaction: Interaction? = null
    
    fun print() {
        interaction?.print()
    }
}
On k1.9, the code outputs 'hello' as well as 'bye' But on k2, 'hello' is not printed, only 'bye' is printed. In k2, the interaction is being self assigned and not being picked from class variable.
d
Yes, it is expected change caused by more smart algorithm of smartcasts
Copy code
class SnippetVR(val interaction: Interaction? = null) {
    fun create(): Snippet {
        return Snippet.newInstance().apply {
            // this: Snippet?
            this?.interaction = interaction
        }!!
    }
}
For the access to the
interaction
in RHS of assignment, there are two candidates for where it could be resolved: 1.
this@apply.interaction
2.
this@SnippetVR.interaction
Since these different
this
came from different scopes, the first one has more priority over the second one. But in your example there is a trick, that
this@apply.interaction
is nullable. So K1 compiler tries to resolve
this@apply.interaction
, sees that it has type
Snippet?
, reports (under the hood) an error that this call is unsafe and looks for other candidates (eventually founding the second one). But K2 compiler is smarter. It knows that we can execute the right hand side of
a?.b = c
only in case when
a != null
, so before resolving this part it adds smartcast that
a != null
(
this@Snippet != null
). This smartcast makes the first candidate compatible and everything successfully resolved to it.
Note that if you change
fun newInstance(): Snippet? = Snippet()
in your code to
fun newInstance(): Snippet = Snippet()
, then both K1 and K2 will resolve to
this@Snippet.interaction
, and only
bye
will be printed in both cases
a
Oh okay, thanks a lot for the explanation
But this makes the behavior confusing for the developer. Eg: Consider the below code (modified version of above code)
Copy code
fun main() {
    val vr = SnippetVR(object: Interaction {
        override fun print() {
            println("1")
        }
    })
    val snippet = vr.create()
    snippet.print()
    println("finish")
}

interface Interaction {
    fun print()
}


class SnippetVR(val interaction: Interaction? = null) {
    fun create(): Snippet {
        return Snippet.newInstance().apply {
            interaction?.print()
            this?.interaction = interaction
            interaction?.print()
        }!!
    }
}


class Snippet() {
    companion object {
        fun newInstance(): Snippet? = Snippet()
    }
    var interaction: Interaction? = object: Interaction {
        override fun print() {
            println("2")
        }
    }
    
    fun print() {
        interaction?.print()
    }
}
Here, when developer writes the code:
Copy code
interaction?.print() 
this?.interaction = interaction
interaction?.print()
He would assume that same instance of interaction is being accessed (getter) in all three lines, but in reality, this is resolved like this:
Copy code
interaction?.print()                        // prints 1
this?.interaction = interaction             // sets interaction that prints 2 
interaction?.print()                        // prints 1
d
Well, the source of the confusion goes from two things: 1. you named two different properties with the same name 2. you've added an implicit receiver of nullable type (
Snippet?
) In this scope there are two different
this
, and they are arranged in the syntactical order (firstly resolved the closest one, then outer and so on). But because the closest receiver is
Snippet?
the
interaction
access is resolved to
this.interaction
, which is an unsafe call. So the next thing the compiler is doing is trying to resolve the
interaction
access on the outer
this