I believe that there are two things that may come as a surprise: the semantics of the language that allows such a call, and what happens at runtime when this code is executed.
On the language side, Kotlin allows a null receiver, but only for extensions . To write an extension function that accepts a receiver with a null value, you must either explicitly enter a type with a null value, or use a valid upper bound for the type parameter (in fact, when you do not specify an upper bound, the default value is nullable Any? ):
fun List<*>?.isEmptyOrNull() = this == null || size == 0
fun <T : CharSequence?> T.nullWhenEmpty() = if ("$this" == "") null else this
fun <T> T.identity() = this // default upper bound Any? is nullable
This function is used in kotlin-stdlib in several places: see CharSequence?.isNullOrEmpty() , CharSequence?.isNullOrBlank() ?.orEmpty() for containers and String?.orEmpty() and even Any?.toString() . Some functions, such as T.let , T.run , which you asked for, and some others simply do not provide an upper bound for the type parameter, and the default value is nullable Any? . And T.use provides a nullable upper bound Closeable? .
Under the hood, that is, in terms of runtime, extension calls are not compiled into invocation commands of JVM members INVOKEVIRTUAL , INVOKEINTERFACE or INVOKESPECIAL (the JVM checks the first argument of such calls, implicit this , for being null and throwing NPE if so , and so called Java and Kotlin member functions). Instead, the Kotlin extension functions are reduced to static methods, and the receiver is simply passed as the first argument. Such a method is called using the INVOKESTATIC , which does not check the arguments for a null value.
Note that when the extension recipient can be null, Kotlin does not allow it to be used where a non-null value is required, without first checking for null:
fun Int?.foo() = this + 1 // error, + is not defined for nullable Int?