Missing type check?

I’m working on an expression language based on KProperty instances and I ran across what seems to be a missing check by the type checker:

import kotlin.reflect.KProperty1

fun <T, R> KProperty1<T, R>.eq(x: R): String = "${this.name} eq $x"

class A(val n: Int)

val prop: KProperty1<A, Int> = A::n
val value: String = "foo"

prop.eq(value)

I would expect this code to fail compilation, because the receiver has type parameter R=Int while the argument x has R=String, instead it compiles and run, returning “n eq foo”

What am I missing here?

I tried both Kotlin 1.3.72 and 1.4.0

2 Likes

I believe this is expected and correct behavior.

eq function is a generic function, so i’ts actual type parameters can be either specified at the call site or inferred if possible.

`KProperty`` is defined as follows:

interface KProperty1<T, out V>

Note the V parameter is covariant. As stated in Kotlin Generics docs:

when a type parameter T of a class C is declared out, it may occur only in out-position in the members of C, but in return C<Base> can safely be a supertype of C<Derived>.

Thus, KProperty1<A, Any> is a supertype of KProperty1<A, Int>.
As for KProperty<A, Any> the parameter of eq method is Any, it can accept String.

Of course it’s possible to call method of base class on an instance of derived class, so as a result

prop.eq(value)

is inferred to

prop.eq<A, Any>(value)

Note that if the type parameter was contravariant, valid types for method call could still be inferred. Only the invariant version would not compile - see:

class Invariant<T>
class Covariant<out T>
class Contravariant<in T>

inline fun <reified T> Invariant<T>.inferType(x: T): String = "${T::class}"
inline fun <reified T> Covariant<T>.inferType(x: T): String = "${T::class}"
inline fun <reified T> Contravariant<T>.inferType(x: T): String = "${T::class}"

fun main() {

    val invariant = Invariant<Int>()
    println(invariant.inferType(1))         // kotlin.Int
//  println(invariant.eq("foo"))            // does not compile!

    val covariant = Covariant<Int>()
    println(covariant.inferType(1))         // kotlin.Int
    println(covariant.inferType("foo"))     // kotlin.Any

    val contravariant = Covariant<Int>()
    println(contravariant.inferType(1))     // kotlin.Int
    println(contravariant.inferType("foo")) // kotlin.Any
}

I see. So there is no way to add an extension method on properties and require the argument to be the same type as the property itself?

Yes, I don’t think it’s doable with extension method alone.
You could introduce some invariant wrappers for the property classes:

import kotlin.reflect.KProperty1

class A(val n: Int)

inline class InvariantKProperty1<T, V>(private val prop: KProperty1<T, V>)
inline fun <reified T, reified V> InvariantKProperty1<T, V>.inferType(x: V): String = "${T::class}, ${V::class}"

fun <T, V> KProperty1<T, V>.invariant() = InvariantKProperty1(this)

fun main() {

    val prop: KProperty1<A, Int> = A::n

    println(InvariantKProperty1(prop).inferType(1))     // A, Int
//  println(InvariantKProperty1(prop).inferType("foo")) // does not compile!

    println(prop.invariant().inferType(1))              // A, Int
//  println(prop.invariant().inferType("foo"))          // does not compile
}
1 Like

Related:

https://youtrack.jetbrains.com/issue/KT-13198

Unfortunately, no progress is being made with these fundamental issues while Kotlin team is busy building irrelevant stuff like multiplatform.

This is a fundamental limitation of the type system and of the semantics of extension functions.

I would also like for Kotlin to have more meta-programmability, such as a macro system, that would allow side-stepping issues like these.

But calling multi-platform “irrelevant” is disingenuous at best.

@akurczak @tobia Going back to the topic now, there is a bug introduced in Kotlin 1.4 where all the examples above will compile if your callsite and extension are in different files (which is always the case in practice). Only 1.3.72 is safe.

Sorry it’s not about different files. Here’s the minimal example modified from @akurczak.

package kotlin_14_bug

import kotlin.reflect.KProperty1

class Invariant2Test {

	class A(val n: Int)

	class InvariantKProperty1<T, V>(private val prop: KProperty1<T, V>)

	fun <T, V> KProperty1<T, V>.invariant() = InvariantKProperty1(this)
	fun <T, V> update(p: InvariantKProperty1<T, V>, x: V): String = ""

	fun main() {
		val prop: KProperty1<A, Int> = A::n

		update(A::n.invariant(), 123)
		// COMPILES - BUG KOTLIN 1.4
		update(A::n.invariant(), "foo")

		val x = A::n.invariant()
		update(x, 123)
		// does not compile
		update(x, "foo")
	}
}

So the difference is if it’s inlined as a variable or expression. Looks like compiler will mess it up when calling extension function. Anyway Kotlin 1.4 compiler is full of bugs and you’re better off to stay away from it.