Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reword callable reference and class literal resolution algorithm, explain some corner cases (#5) #43

Merged
merged 7 commits into from
Aug 15, 2016
102 changes: 83 additions & 19 deletions proposals/bound-callable-references.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ fun sortedByComparator(strings: List<String>, comparator: Comparator<String>) =
strings.sortedBy(comparator::compare)
```

The function type of a bound reference differs from the type of the corresponding *unbound* reference in that it has arity lower by 1, and doesn't have the first type argument (the type of the instance or extension receiver parameter).

Both function references and property references can be bound.

Bound class literal is an expression which is evaluated to the runtime representation of the class of an object, similarly provided as an expression before `::`. Its semantics are similar to Java's `java.lang.Object#getClass`, except that its type is `kotlin.reflect.KClass<...>`, not `java.lang.Class<...>`. Example:
```
val x: Widget = ...
Expand Down Expand Up @@ -65,15 +69,15 @@ Double colon expressions are postfix expressions, so `::`'s priority is maximal

Note that now any expression can be followed by question marks (`?`) and then `::`. So, if we see `?` after an expression, we must perform a lookahead until the first non-`?`, and if it's `::`, parse this as a double colon expression.

### Resolution
### Resolution: left-hand side of callable reference

The LHS may now be interpreted as an expression, or a type, or both.
The LHS of a callable reference expression may now be interpreted as an expression, or a type, or both.
```
SimpleName::foo // expression (variable SimpleName) or type (class SimpleName)
Qualified.Name::foo // expression or type

Nullable?::foo // type (see section Nullable references below)
Generic<Arg>::foo // type
Nullable?::foo // type (see section "Nullable references" below)
Generic<Arg>::foo // type (see section "Calls to generic properties" below)

Generic<Arg>()::foo // expression
this::foo // expression
Expand All @@ -82,13 +86,17 @@ The LHS may now be interpreted as an expression, or a type, or both.
(ParenNullable)?::foo // type
```

The semantics are different when the LHS is interpreted as an expression or a type, so we establish a priority of one over another when both interpretations are applicable.
The algorithm is the following:
The semantics are different when the LHS is interpreted as an expression or a type, so we introduce the following algorithm to choose the resulting interpretation when both are applicable:

1. Try interpreting the LHS as an **expression** with the usual resolution algorithm for qualified expressions.
If the result represents a companion object of some class, continue to p.2.
Otherwise continue resolution of the member in the scope of the expression's type.
2. Resolve the unbound reference with the existing algorithm.
1. Type-check the LHS as an **expression**.
- If the result represents a _companion object of some class_ specified with the short syntax (via the short/qualified name of the containing class), discard the result and continue to p.2.
Copy link
Contributor

@erokhins erokhins Aug 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

represents a companion object or object. And what about enum entries?

Copy link
Member Author

@udalov udalov Aug 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you think non-companion objects are relevant here? They are handled in the second clause.

Enum entries are neither objects nor companion objects, so these rules do not apply to them.

- If the result represents an _object_ (either non-companion, or companion specified with the full syntax: `org.foo.Bar.Companion` or just `Bar.Companion`), remember the result and continue to p.2.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or (Bar)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, (Bar) is covered by the first clause because it's a companion object of some class specified with the short syntax

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about object Foo (Foo)?

Copy link
Member Author

@udalov udalov Aug 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what do you propose, could you elaborate? The two examples here explain what does "companion specified with the full syntax" mean, they do not relate to non-companion objects. I don't think usage of a non-companion object needs any explanation. Or are you talking about the (Foo)::class case?

UPD: discussed in person

- Otherwise the resolution of the LHS is complete and the result is the type-checked expression. Note that the resolution of the LHS is finished at this point even if errors were reported.
2. Resolve the LHS as a **type**.
- If the resulting type refers to the same object that was obtained in the first step, complete the resolution with the result obtained in the first step. In other words, the result is the object type-checked as expression, and the object instance will be bound to the reference.
- Otherwise the result is the resolved type and the reference is unbound.

> If there were no `object`s or `companion object`s in Kotlin, the algorithm would be very simple: first try resolving as expression, then as type. However, this would not be backwards compatible for a very common case: in an expression like `Obj::foo`, without any changes to the source code an object `Obj` might now win the resolution where previously (in Kotlin 1.0) a completely different class `Obj` had been winning. This is possible when you have a class named `Obj` and an object `Obj` in another package, coming from a star import.

Examples:
```
Expand Down Expand Up @@ -123,20 +131,75 @@ fun test() {
}
```

Resolution of a LHS of a class literal expression is performed exactly the same,
so that `C::class` means the class of C and `(C)::class` means the class of C.Companion.

It is an error if the LHS expression has nullable type and the resolved member is not an extension to nullable type.
It is an error if the LHS of a bound class literal has nullable type:
It is an error if the LHS expression has nullable type and the resolved member is not an extension to nullable type:
```
class C {
fun foo() {}
}

fun C?.ext() {}

fun test(c: C?) {
c::foo // error
c::class // error
null::class // error
c::foo // error
c::ext // ok
}
```

### Resolution: class literal

Resolution of the LHS of a class literal expression is performed with the same algorithm.

```
class C {
companion object
}

fun test() {
C::class // class of C
C()::class // class of C
(C)::class // class of C.Companion
C.Companion::class // class of C.Companion
}
```

Once the LHS is type-checked and its type is determined to be `T`, the type of the whole class literal expression is `KClass<erase(T)>`, where `erase(T)` represents the most accurate possible type-safe representation of the type `T` and is obtained by substituting `T`'s arguments with star projections (`*`), except when the type is an array.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type-arguments of arrays are not fully known at runtime either: their nullability is unclear. Does it matter in this context?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does matter, and we may revisit it in the future, but type-checking arrayOfStrings::class to KClass<Array<*>> would be rather unhelpful from Java user's point of view. We've briefly discussed this with @erokhins and decided (for now) to type-check arrayOfStrings::class to KClass<Array<String>>. We should discuss this in detail later anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note in JS we don't know anything about type-arguments of arrays at runtime.


More formally, let `T` = `C<A1, ..., AN>`, where `C` is a class and `N`, the number of type arguments, might be zero. `erase(T)` is then defined as follows:
- if `C` = `kotlin.Array`:
- if `A1` is a projection of some type (covariant, contravariant, or star), `erase(T)` = `kotlin.Array<*>`
- otherwise `erase(T)` = `kotlin.Array<erase(A1)>`
- otherwise `erase(T)` = `C<*, ..., *>`

Examples:
```
fun test() {
"a"::class // KClass<String>
listOf("a")::class // KClass<List<*>> (not "KClass<List<String>>"!)
listOf(listOf("a"))::class // KClass<List<*>> (not "KClass<List<List<String>>>"!)
mapOf("a" to 42)::class // KClass<Map<*, *>>
arrayOf("a")::class // KClass<Array<String>>
arrayOf(arrayOf("a"))::class // KClass<Array<Array<String>>>
arrayOf(listOf("a"))::class // KClass<Array<List<*>>>
}
```

> The reason for substitution with star projections is erasure: type arguments are not reified at runtime in Kotlin, so using class literals with seemingly full type information could result in exceptions at runtime. For example, consider the following code, where if we don't substitute arguments with `*`, an exception is thrown:
> ```
> fun test(): String {
> val kClass: KClass<List<String>> = listOf("")::class
> val strings: List<String> = kClass.cast(listOf(42))
> return strings[0] // ClassCastException!
> }
> ```
> If we do the substitution as proposed above, the type of `kClass` is `KClass<List<*>>` and you must perform a cast to make the code compile.

TODO: in JS, there seems to be no way to determine type arguments of arrays at runtime

It is an error if the LHS of a bound class literal has nullable type:
```
fun test(s: String?) {
s::class // error
null::class // error
}
```

Expand Down Expand Up @@ -212,7 +275,8 @@ class A {
}
```

Generated bytecode for `<expr>::class` should be similar to `<expr>.javaClass.kotlin`.
Generated bytecode for `<expr>::class` should be similar to `<expr>.javaClass.kotlin`. Note that for expressions of primitive types, the corresponding `KClass` instance at runtime should be backed by the primitive `Class` object, not the wrapper class. For example, `42::class.java` should return the `int` class, not `java.lang.Integer`.

An intrinsic for `<expr>::class.java`, meaning `<expr>.javaClass`, would be useful.

### Reflection
Expand Down