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

Improve resolution using expected type #378

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Examples for problems with functions
  • Loading branch information
serras committed Nov 18, 2024
commit 42f9783783c5931475fb45b86c4d8aa702d9a52e
51 changes: 37 additions & 14 deletions proposals/improved-resolution-expected-type.md
Original file line number Diff line number Diff line change
@@ -16,14 +16,15 @@ We propose an improvement of the name resolution rules of Kotlin based on the ex
* [Abstract](#abstract)
* [Table of contents](#table-of-contents)
* [Motivating example](#motivating-example)
* [The issue with overloading](#the-issue-with-overloading)
* [Importing the entire scopes](#importing-the-entire-scopes)
* [What is available in the contextual scope](#what-is-available-in-the-contextual-scope)
* [No-argument callables](#no-argument-callables)
* [Technical details](#technical-details)
* [Additional candidate resolution scope](#additional-candidate-resolution-scope)
* [Additional type resolution scope](#additional-type-resolution-scope)
* [Expected type propagation](#expected-type-propagation)
* [Single definite expected type](#single-definite-expected-type)
* [Additional contextual scope](#additional-contextual-scope)
* [Changes to overload resolution](#changes-to-overload-resolution)
* [Design decisions](#design-decisions)
* [Risks](#risks)
* [Implementation note](#implementation-note)

## Motivating example

@@ -122,7 +123,7 @@ when (color) {
}
```

**Extension** callables defined in the static and companion object scopes are **not** available. In most cases those callables can be imported without requiring any additional qualification on the call site.
**Extension** callables defined in the static and companion object scopes are **not** available. The receiver in that case acts as an additional parameter to resolve, putting us in the same situation as "regular" parameters.

```kotlin
class Color(...) {
@@ -136,6 +137,12 @@ when (color) {
}
```

As a workaround, in most cases those callables can be imported without requiring any additional qualification on the call site.

```kotlin
import Color.Companion.grey
```

There is no additional filtering of properties or functions based on their result type. For example, the following code _resolves_ correctly to `Color.NUMBER_OF_COLORS`, but then raises a "type mismatch" error between `Color` and `Long`.

```kotlin
@@ -164,6 +171,22 @@ Let us forget for a moment about lambda arguments; in that case the procedure is

The problem is that if we allow resolving to a function with some arguments, we could end up in a situation in which we do not resolve anything until we reach the top-level function call, which then gives us information to resolve the arguments. And if those arguments also had unresolved arguments themselves, this process could go arbitrarily deep. This is both costly for the compiler, and also quite brittle.

Take the following example, in which we extend the `Color` class and introduce a `Label` function (in the style of Jetpack Compose).

```kotlin
class Color(...) {
companion object {
fun withAlpha(color: Color, alpha: Double): Color = ...
}
}

fun Label(text: String, color: Color) = ...

val hello: Text = Label("hello", withAlpha(BLUE, 0.5))
```

During the resolution of the body of `hello`, we proceed arguments-first. So we already fail resolution at `BLUE`, since we do not know the type of it (yet). Going upwards we fail again for `withAlpha`. It is only when we get to `Label` that we understand that the second argument refers to `Color.withAlpha`, perform potential overload resolution, and then push the expected type of `BLUE` to finally resolve it. This already duplicates the work.

The no-argument rule ensures that this undesired behavior may not arise, as resolution does not need to go deeper in that case. This seems like a good balance, since the most common use cases like dropping the name of the enumeration are still possible. In a previous iteration of this proposal we went even further, forbidding any improved resolution inside function calls.

## Technical details
@@ -225,15 +248,15 @@ There are some scenarios in which the expected type propagation may lead to comp
* Otherwise, `sdet(T)` is undefined.
* Nullable types: `sdet(T?) = sdet(T)`.
* Types with variance:
* Covariance, `stde(out T) = stde(T)`,
* For contravariant arguments, `stde(in T)` is undefined.
* Captured types: `stde` is undefined.
* Flexible types, `stde(A .. B)`
* Compute `stde(A)` and `stde(B)`, and take it if they coincide; otherwise undefined.
* Covariance, `sdet(out T) = sdet(T)`,
* For contravariant arguments, `sdet(in T)` is undefined.
* Captured types: `sdet` is undefined.
* Flexible types, `sdet(A .. B)`
* Compute `sdet(A)` and `sdet(B)`, and take it if they coincide; otherwise undefined.
* This rule covers `A .. A?` as special case.
* Intersection types, `stde(A & B)`,
* Definitely not-null, `stde(A & Any) = stde(A)`,
* "Fake" intersection types in which `B` is a subtype of `A`, `stde(A & B) = stde(B)`; and vice versa.
* Intersection types, `sdet(A & B)`,
* Definitely not-null, `sdet(A & Any) = sdet(A)`,
* "Fake" intersection types in which `B` is a subtype of `A`, `sdet(A & B) = sdet(B)`; and vice versa.

### Additional contextual scope