From c524d668a5fe59f4e3635c1ba91d11ca9fca0087 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 12 Apr 2024 11:16:27 +0200 Subject: [PATCH 01/22] Improved resolution using expected type --- .../improved-resolution-expected-type.md | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 proposals/improved-resolution-expected-type.md diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md new file mode 100644 index 000000000..dbf87be80 --- /dev/null +++ b/proposals/improved-resolution-expected-type.md @@ -0,0 +1,147 @@ +# Improve resolution using expected type + +* **Type**: Design proposal +* **Author**: Alejandro Serrano Mena +* **Status**: In discussion +* **Prototype**: Implemented in [this branch](https://github.com/JetBrains/kotlin/compare/rr/serras/improved-resolution-expected-type) +* **Discussion**: ?? +* **Related issues**: [KT-9493](https://youtrack.jetbrains.com/issue/KT-9493/Allow-short-enum-names-in-when-expressions), [KT-44729](https://youtrack.jetbrains.com/issue/KT-44729/Context-sensitive-resolution), [KT-16768](https://youtrack.jetbrains.com/issue/KT-16768/Context-sensitive-resolution-prototype-Resolve-unqualified-enum-constants-based-on-expected-type) + +## Abstract + +We propose an improvement of the name resolution rules of Kotlin based on the expected type of the expression. The goal is to decrease the amount of qualifications when the type of the expression is known. + +## Table of contents + +* [Abstract](#abstract) +* [Table of contents](#table-of-contents) +* [Motivating example](#motivating-example) + * [The issue with overloading](#the-issue-with-overloading) +* [Technical details](#technical-details) + * [Additional candidate resolution scope](#additional-candidate-resolution-scope) + * [Additional type resolution scope](#additional-type-resolution-scope) +* [Potential additions](#potential-additions) + * [Equality](#equality) + * [Nested inheritors](#nested-inheritors) +* [Implementation note](#implementation-note) + +## Motivating example + +The current rules of the language sometimes require Kotliners to qualify some members, where it feels that such qualification could be inferred from the types already spelled out in the code. One [infamous example](https://youtrack.jetbrains.com/issue/KT-9493/Allow-short-enum-names-in-when-expressions) is enumeration entries, which always live inside their defining class. In the example below, we need to write `Problem.`, even though `Problem` is already explicit in the type of the `problem` argument or the return type of `problematic`. + +```kotlin +enum class Problem { + CONNECTION, AUTHENTICATION, DATABASE, UNKNOWN +} + +fun message(problem: Problem): String = when (problem) { + Problem.CONNECTION -> "connection" + Problem.AUTHENTICATION -> "authentication" + Problem.DATABASE -> "database" + Problem.UNKNOWN -> "unknown" +} + +fun problematic(x: String): Problem = when (x) { + "connection" -> Problem.CONNECTION + "authentication" -> Problem.AUTHENTICATION + "database" -> Problem.DATABASE + else -> Problem.UNKNOWN +} +``` + +This KEEP addresses many of these problems by considering explicit expected types when doing name resolution. We try to propagate the information from argument and return types, declarations, and similar constructs. As a result, the following constructs are usually improved: + +* conditions on `when` expressions with subject, +* type checks and casts (`is`, `as`), +* `return`, both implicit and explicit, +* we propose an alternative to improve equality (`==`, `!=`). + +### The issue with overloading + +We leave out of this KEEP the improvement of resolution for arguments of functions. Taking the example above, you still need to qualify the argument to `message`. + +```kotlin +val s = message(Problem.CONNECTION) +``` + +What sets function calls apart from the constructs mentioned before is overloading. In Kotlin, there's a certain sequence in which function calls are resolved and type checked, as spelled out in the [specification](https://kotlinlang.org/spec/overload-resolution.html#overload-resolution). + +1. Infer the types of non-lambda arguments. +2. Choose an overload for the function based on the gathered type. +3. Check the types of lambda arguments (including inferring some remaining type arguments if `@BuilderInference` is present). + +Improving the resolution of arguments based on the function call would amount to reversing the order of (1) and (2), at least partially. There are potential techniques to solve this problem, but another KEEP seems more appropriate. + +As a consequence, operators in Kotlin that are desugared to function calls, like `in` or `thing[x]`, are also outside of the scope of this KEEP. + +## Technical details + +We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). + +This scope is added at the lowest priority level. This ensures that we do not change the current behavior, we only extend the amount of programs that are accepted. + +This scope is **not** propagated to children of the node in question. For example, when resolving `val x: T = f(x, y)`, the additional scope is present when resolving `f`, but not when resolving `x` and `y`. After all, `x` and `y` no longer have an expected type `T`. + +### Additional candidate resolution scope + +We propagate `T` to an expression `e` whenever the specification states _"the type of `e` must be a subtype of `T`"_. + +For declarations, a type `T` is propagated to the body, which may be an expression or a block. Note that we only propagate types given _explicitly_ by the developer. + +* _Default parameters of functions_: `x: T = e`; +* _Initializers of properties with explicit type_: `val x: T = e`; +* _Explicit return types of functions_: `fun f(...): T = e` or `fun f(...): T { ... }`, +* _Getters of properties with explicit type_: `val x: T get() = e` or `val x: T get() { ... }`. + +If a type `T` is propagated to a block, then the type is propagated to every return point of the block. + +* _Explicit `return`_: `return e`, +* _Implicit `return`_: the last statement. + +For other statements and expressions, we have the following rules. Here "known type of `x`" includes any additional typing information derived from smart casting. + +* _Assignments_: in `x = e`, the known type of `x` is propagated to `e`, +* _Branching_: if type `T` is propagated to a conditional (`if`) or `when` expressions, the type `T` is propagated to each of their branches, +* _`when` expression with subject_: in `when (x) { e -> ... }`, then known type of `x` is propagated to `e`, when `e` is not of the form `is T` or `in e`, +* _Elvis operator_: if type `T` is propagated to `e1 ?: e2`, then we propagate `T?` to `e1` and `T` to `e2`. +* _Type cast_: in `e as T` and `e as? T`, the type `T` is propagated to `e`, + * This rule follows from the similarity to doing `val x: T = e`. + +### Additional type resolution scope + +We introduce the additional scope during type solution in the following cases: + +* _Type check_: in `e is T`, `e !is T`, the known type of `e` is propagated to `T`. +* _`when` expression with subject_: in `when (x) { is T -> ... }`, then known type of `x` is propagated to `T`. + +## Potential additions + +### Equality + +It is possible (and implemented in the prototype) to add the additional propagation rule: + +* _Equality_: in `a == b` and `a != b`, then known type of `a` is propagated to `b`. + +This helps in common cases like `p == Problem.CONNECTION`. + +However, there are two potential problems with this approach, which require further discussion. + +1. It is not symmetric: it might be surprising that `p == CONNECTION` is accepted, but `CONNECTION == p` is rejected. +2. The [specification](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) mandates `a == b` to be equivalent to `(A as? Any)?.equals(B as Any?) ?: (B === null)` in the general case. So there is no actual type expected from `b` merely from participating in equality. + +### Nested inheritors + +The rules above handle enumeration, and it's also useful when defining a hierarchy of classes nested on the parent. + +```kotlin +sealed interface Either { + data class Left(error: E): Either + data class Right(value: A): Either +} +``` + +One way in which we can improve resolution even more is by considering the subclasses of the known type of an expression. Making every potential subclass available would be quite surprising, but sealed hierarchies form an interesting subset (and the information is directly accessible to the compiler). However, this means getting away from a more "syntactical" choice (nested elements of the type), which may be surprising. + +## Implementation note + +In the current K2 compiler, these rules amount to considering those places in which `WithExpectedType` is passed as the `ResolutionMode`, plus adding special rules for `as`, `is`, and `==`. Since `when` with subject is desugared as either `x == e` or `x is T`, we need no additional rules to cover them. \ No newline at end of file From 5011dd9f8a3369af8384b05f1e7a56a42d553c43 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 10 May 2024 15:04:09 +0200 Subject: [PATCH 02/22] Clarify some rules --- proposals/improved-resolution-expected-type.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index dbf87be80..3e8e3f9f0 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -5,7 +5,7 @@ * **Status**: In discussion * **Prototype**: Implemented in [this branch](https://github.com/JetBrains/kotlin/compare/rr/serras/improved-resolution-expected-type) * **Discussion**: ?? -* **Related issues**: [KT-9493](https://youtrack.jetbrains.com/issue/KT-9493/Allow-short-enum-names-in-when-expressions), [KT-44729](https://youtrack.jetbrains.com/issue/KT-44729/Context-sensitive-resolution), [KT-16768](https://youtrack.jetbrains.com/issue/KT-16768/Context-sensitive-resolution-prototype-Resolve-unqualified-enum-constants-based-on-expected-type) +* **Related issues**: [KT-9493](https://youtrack.jetbrains.com/issue/KT-9493/Allow-short-enum-names-in-when-expressions), [KT-44729](https://youtrack.jetbrains.com/issue/KT-44729/Context-sensitive-resolution), [KT-16768](https://youtrack.jetbrains.com/issue/KT-16768/Context-sensitive-resolution-prototype-Resolve-unqualified-enum-constants-based-on-expected-type), [KT-58939](https://youtrack.jetbrains.com/issue/KT-58939/K2-Context-sensitive-resolution-of-Enum-leads-to-Unresolved-Reference-when-Enum-has-the-same-name-as-one-of-its-Entries) ## Abstract @@ -58,7 +58,7 @@ This KEEP addresses many of these problems by considering explicit expected type ### The issue with overloading -We leave out of this KEEP the improvement of resolution for arguments of functions. Taking the example above, you still need to qualify the argument to `message`. +We leave out of this KEEP any extension to the propagation of information from function calls to their argument. One particularly visible implication of this choice is that you still need to qualify enumeration entries that appear as arguments as currently done in Kotlin. Following with the example above, you still need to qualify the argument to `message`. ```kotlin val s = message(Problem.CONNECTION) @@ -68,7 +68,7 @@ What sets function calls apart from the constructs mentioned before is overloadi 1. Infer the types of non-lambda arguments. 2. Choose an overload for the function based on the gathered type. -3. Check the types of lambda arguments (including inferring some remaining type arguments if `@BuilderInference` is present). +3. Check the types of lambda arguments. Improving the resolution of arguments based on the function call would amount to reversing the order of (1) and (2), at least partially. There are potential techniques to solve this problem, but another KEEP seems more appropriate. @@ -101,11 +101,16 @@ If a type `T` is propagated to a block, then the type is propagated to every ret For other statements and expressions, we have the following rules. Here "known type of `x`" includes any additional typing information derived from smart casting. * _Assignments_: in `x = e`, the known type of `x` is propagated to `e`, -* _Branching_: if type `T` is propagated to a conditional (`if`) or `when` expressions, the type `T` is propagated to each of their branches, * _`when` expression with subject_: in `when (x) { e -> ... }`, then known type of `x` is propagated to `e`, when `e` is not of the form `is T` or `in e`, +* _Branching_: if type `T` is propagated to an expression with several branches, the type `T` is propagated to each of them, + * _Conditionals_, either `if` or `when`, + * _`try` expressions_, where the type `T` is propagated to the `try` block and each of the `catch` handlers, * _Elvis operator_: if type `T` is propagated to `e1 ?: e2`, then we propagate `T?` to `e1` and `T` to `e2`. +* _Not-null assertion_: if type `T` is propagated to `e!!`, then we propagate `T?` to `e`, * _Type cast_: in `e as T` and `e as? T`, the type `T` is propagated to `e`, * This rule follows from the similarity to doing `val x: T = e`. + +All other operators and compound assignments (such as `x += e`) do not propagate information. The reason is that those operators may be _overloaded_, so we cannot guarantee their type. ### Additional type resolution scope From a1c7ee231485681d812558747b5483b47512674f Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 30 May 2024 14:56:46 +0200 Subject: [PATCH 03/22] Improvements after meeting --- .../improved-resolution-expected-type.md | 72 ++++++++++++++----- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 3e8e3f9f0..3a6ca5e6c 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -17,12 +17,12 @@ We propose an improvement of the name resolution rules of Kotlin based on the ex * [Table of contents](#table-of-contents) * [Motivating example](#motivating-example) * [The issue with overloading](#the-issue-with-overloading) + * [Importing the entire static scope](#importing-the-entire-static-scope) * [Technical details](#technical-details) * [Additional candidate resolution scope](#additional-candidate-resolution-scope) * [Additional type resolution scope](#additional-type-resolution-scope) -* [Potential additions](#potential-additions) - * [Equality](#equality) - * [Nested inheritors](#nested-inheritors) +* [Design decisions](#design-decisions) + * [Risks](#risks) * [Implementation note](#implementation-note) ## Motivating example @@ -54,11 +54,11 @@ This KEEP addresses many of these problems by considering explicit expected type * conditions on `when` expressions with subject, * type checks and casts (`is`, `as`), * `return`, both implicit and explicit, -* we propose an alternative to improve equality (`==`, `!=`). +* equality checks (`==`, `!=`). ### The issue with overloading -We leave out of this KEEP any extension to the propagation of information from function calls to their argument. One particularly visible implication of this choice is that you still need to qualify enumeration entries that appear as arguments as currently done in Kotlin. Following with the example above, you still need to qualify the argument to `message`. +We leave out of this KEEP any extension to the propagation of information from function calls to their argument. One particularly visible implication of this choice is that you still need to qualify enumeration entries that appear as arguments as currently done in Kotlin. Following the example above, you still need to qualify the argument to `message`. ```kotlin val s = message(Problem.CONNECTION) @@ -72,13 +72,35 @@ What sets function calls apart from the constructs mentioned before is overloadi Improving the resolution of arguments based on the function call would amount to reversing the order of (1) and (2), at least partially. There are potential techniques to solve this problem, but another KEEP seems more appropriate. -As a consequence, operators in Kotlin that are desugared to function calls, like `in` or `thing[x]`, are also outside of the scope of this KEEP. +As a consequence, operators in Kotlin that are desugared to function calls which in turn get resolved, like `in` or `thing[x]`, are also outside of the scope of this KEEP. Note that [value equalities](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) (`==`, `!=`) are not part of that group, since they are always resolved to `kotlin.Any.equals`. + +### Importing the entire static scope + +The current proposal imports the _entire_ static scope, which includes classes, properties, and functions. Whereas the first two are needed to cover common cases like enumeration entries, and comparison with objects, the usefulness of methods seems debatable. However, it allows some interesting constructions where we compare against a value coming from a factory: + +```kotlin +class Color(...) { + companion object { + val WHITE: Color = ... + val BLACK: Color = ... + fun fromRGB(r: Int, g: Int, b: Int): Color = ... + } +} + +// now when we match on a color... +when (color) { + WHITE -> ... + fromRGB(10, 10, 10) -> ... +} +``` + +An additional advantage is that of uniformity. In particular, the code behaves as you had `*`-imported the `Color` type. ## Technical details We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). -This scope is added at the lowest priority level. This ensures that we do not change the current behavior, we only extend the amount of programs that are accepted. +This scope has the same priority as `*`-imports. That way, the user may have the mental model that using the name of an enumeration entry without qualification, for example, is the same as if the enumeration was `*`-imported into the file. This scope is **not** propagated to children of the node in question. For example, when resolving `val x: T = f(x, y)`, the additional scope is present when resolving `f`, but not when resolving `x` and `y`. After all, `x` and `y` no longer have an expected type `T`. @@ -109,6 +131,9 @@ For other statements and expressions, we have the following rules. Here "known t * _Not-null assertion_: if type `T` is propagated to `e!!`, then we propagate `T?` to `e`, * _Type cast_: in `e as T` and `e as? T`, the type `T` is propagated to `e`, * This rule follows from the similarity to doing `val x: T = e`. +* _Equality_: in `a == b` and `a != b`, the known type of `a` is propagated to `b`. + * This helps in common cases like `p == Problem.CONNECTION`. + * Note that in this case the expected type should only be propagated for the purposes of name resolution. The [specification](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) mandates `a == b` to be equivalent to `(A as? Any)?.equals(B as Any?) ?: (B === null)` in the general case, so from a typing perspective there should be no constraint on the type of `b`. All other operators and compound assignments (such as `x += e`) do not propagate information. The reason is that those operators may be _overloaded_, so we cannot guarantee their type. @@ -119,24 +144,25 @@ We introduce the additional scope during type solution in the following cases: * _Type check_: in `e is T`, `e !is T`, the known type of `e` is propagated to `T`. * _`when` expression with subject_: in `when (x) { is T -> ... }`, then known type of `x` is propagated to `T`. -## Potential additions +## Design decisions + +**Priority level**: the current proposal puts the additional scope to be searched when the expected type is known at the same level as `*`-imports. This means that this feature is _not_ 100% backward-compatible, as we have the risk of ambiguity between a declaration imported in such a way, and one available in the static scope of the expected type. -### Equality +The most conservative option is for the new scope to have the lowest priority. In practical terms, that means that even built-ins and automatically imported declarations have higher priority, which seems like an odd choice too. As mentioned above, the mental model of these scopes working as `*`-imports seems like a useful tool for understanding the feature, so making them have the same priority level feels like a natural next step. -It is possible (and implemented in the prototype) to add the additional propagation rule: +**No `.value` syntax**: Swift has a very similar feature to the one proposed in this KEEP, namely [implicit member expressions](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions#Implicit-Member-Expression). The main difference is that one has to prepend `.` to access this new type-resolution-guided scope. -* _Equality_: in `a == b` and `a != b`, then known type of `a` is propagated to `b`. +The big drawback of this choice is that new syntax ambiguities may arise, and these are very difficult to predict upfront. Code that compiled perfectly may now have an ambiguous reading by the compiler. -This helps in common cases like `p == Problem.CONNECTION`. +Furthermore, Kotliners _already_ use a pattern to avoid re-writing the name of an enumeration over and over, namely importing all the enumeration entries, which makes them available without any `.` at the front. It seems better to support the style that the community already uses than trying to make people change their habits. -However, there are two potential problems with this approach, which require further discussion. +**On equality**: using the rules above, equalities propagate type information from left to right. But there are other two options: propagating from right to left, or even not propagating any information at all. -1. It is not symmetric: it might be surprising that `p == CONNECTION` is accepted, but `CONNECTION == p` is rejected. -2. The [specification](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) mandates `a == b` to be equivalent to `(A as? Any)?.equals(B as Any?) ?: (B === null)` in the general case. So there is no actual type expected from `b` merely from participating in equality. +The current choice is obviously not symmetric: it might be surprising that `p == CONNECTION` is accepted, but `CONNECTION == p` is rejected. A preliminary assessment shows that the pattern `CONSTANT == variable` is not as prevalent in the Kotlin community as in other programming languages. -### Nested inheritors +On the other hand, the proposed flow of information is consistent with the ability to refactor `when (x) { A -> ...}` into `when { x == A -> ...}`, without any further qualification required. We think that this uniformity is important for developers. -The rules above handle enumeration, and it's also useful when defining a hierarchy of classes nested on the parent. +**Sealed subclasses**: the rules above handle enumeration, and it's also useful when defining a hierarchy of classes nested on the parent. ```kotlin sealed interface Either { @@ -145,7 +171,17 @@ sealed interface Either { } ``` -One way in which we can improve resolution even more is by considering the subclasses of the known type of an expression. Making every potential subclass available would be quite surprising, but sealed hierarchies form an interesting subset (and the information is directly accessible to the compiler). However, this means getting away from a more "syntactical" choice (nested elements of the type), which may be surprising. +One way in which we can improve resolution even more is by considering the subclasses of the known type of an expression. Making every potential subclass available would be quite surprising, but sealed hierarchies form an interesting subset (and the information is directly accessible to the compiler). + +At this point, we have decided against it for practical reasons. If the subclasses are defined inside the parent class (like in `Either` above), this proposal already helps because the subclasses are in the static scope of the parent. If they are inside the parent, then we are not making the particular piece of code any smaller, only avoiding one import. Since imports are usually disregarded by the developers anyway, it seems that adding all sealed subclasses to the scope brings no additional benefit. + +### Risks + +One potential risk of this proposal is the difficulty of understanding _when_ exactly it is OK to drop the qualifier, which essentially corresponds to understanding the propagation of the expected type through the compiler. On the other hand, maybe this complete understanding is not required, as developers will be able to count on the main scenarios: the conditions on a `when` with subject, the immediate expression after a `return`, or the initializer of a property, given that the return type is known. + +Another potential risk is that we add more coupling between type inference and candidate resolution. On the other hand, in Kotlin those two processes are inevitably linked together -- to resolve the candidates of a call you need the type of the receivers and arguments -- so the step taken by this proposal feels quite small in comparison. + +The third potential risk is whether this additional scope may lead to surprises. In particular, whether programs are accepted which are not expected by the developer, or the resolution points to a different declaration than expected. We think that the very low priority of the new scope is enough to mitigate those problems. In any case, IDE implementors should be aware of this new feature of the language, providing their usual support for code navigation. ## Implementation note From 8ffecdc49a0a3bf353332052e4aca44f630f2c6d Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 3 Jun 2024 09:55:27 +0200 Subject: [PATCH 04/22] Change resolution of `as` --- proposals/improved-resolution-expected-type.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 3a6ca5e6c..d330441d0 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -129,8 +129,6 @@ For other statements and expressions, we have the following rules. Here "known t * _`try` expressions_, where the type `T` is propagated to the `try` block and each of the `catch` handlers, * _Elvis operator_: if type `T` is propagated to `e1 ?: e2`, then we propagate `T?` to `e1` and `T` to `e2`. * _Not-null assertion_: if type `T` is propagated to `e!!`, then we propagate `T?` to `e`, -* _Type cast_: in `e as T` and `e as? T`, the type `T` is propagated to `e`, - * This rule follows from the similarity to doing `val x: T = e`. * _Equality_: in `a == b` and `a != b`, the known type of `a` is propagated to `b`. * This helps in common cases like `p == Problem.CONNECTION`. * Note that in this case the expected type should only be propagated for the purposes of name resolution. The [specification](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) mandates `a == b` to be equivalent to `(A as? Any)?.equals(B as Any?) ?: (B === null)` in the general case, so from a typing perspective there should be no constraint on the type of `b`. @@ -142,6 +140,7 @@ All other operators and compound assignments (such as `x += e`) do not propagate We introduce the additional scope during type solution in the following cases: * _Type check_: in `e is T`, `e !is T`, the known type of `e` is propagated to `T`. +* _Type cast_: in `e as T` and `e as? T`, the known type of `e` is propagated to `T`. * _`when` expression with subject_: in `when (x) { is T -> ... }`, then known type of `x` is propagated to `T`. ## Design decisions From c533d8db0c03582d3fe428db3aa3500cf78f9ff3 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 5 Jun 2024 10:00:10 +0200 Subject: [PATCH 05/22] Mention companion object scope --- proposals/improved-resolution-expected-type.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index d330441d0..5ba816dcd 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -16,13 +16,13 @@ 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 static scope](#importing-the-entire-static-scope) + * [The issue with overloading](#the-issue-with-overloading) + * [Importing the entire scopes](#importing-the-entire-scopes) * [Technical details](#technical-details) - * [Additional candidate resolution scope](#additional-candidate-resolution-scope) - * [Additional type resolution scope](#additional-type-resolution-scope) + * [Additional candidate resolution scope](#additional-candidate-resolution-scope) + * [Additional type resolution scope](#additional-type-resolution-scope) * [Design decisions](#design-decisions) - * [Risks](#risks) + * [Risks](#risks) * [Implementation note](#implementation-note) ## Motivating example @@ -74,9 +74,9 @@ Improving the resolution of arguments based on the function call would amount to As a consequence, operators in Kotlin that are desugared to function calls which in turn get resolved, like `in` or `thing[x]`, are also outside of the scope of this KEEP. Note that [value equalities](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) (`==`, `!=`) are not part of that group, since they are always resolved to `kotlin.Any.equals`. -### Importing the entire static scope +### Importing the entire scopes -The current proposal imports the _entire_ static scope, which includes classes, properties, and functions. Whereas the first two are needed to cover common cases like enumeration entries, and comparison with objects, the usefulness of methods seems debatable. However, it allows some interesting constructions where we compare against a value coming from a factory: +The current proposal imports the _entire_ static and companion object scopes, which include classes, properties, and functions. Whereas the first two are needed to cover common cases like enumeration entries, and comparison with objects, the usefulness of methods seems debatable. However, it allows some interesting constructions where we compare against a value coming from a factory: ```kotlin class Color(...) { @@ -145,7 +145,7 @@ We introduce the additional scope during type solution in the following cases: ## Design decisions -**Priority level**: the current proposal puts the additional scope to be searched when the expected type is known at the same level as `*`-imports. This means that this feature is _not_ 100% backward-compatible, as we have the risk of ambiguity between a declaration imported in such a way, and one available in the static scope of the expected type. +**Priority level**: the current proposal puts the additional scope to be searched when the expected type is known at the same level as `*`-imports. This means that this feature is _not_ 100% backward-compatible, as we have the risk of ambiguity between a declaration imported in such a way, and one available in the static or companion object scope of the expected type. The most conservative option is for the new scope to have the lowest priority. In practical terms, that means that even built-ins and automatically imported declarations have higher priority, which seems like an odd choice too. As mentioned above, the mental model of these scopes working as `*`-imports seems like a useful tool for understanding the feature, so making them have the same priority level feels like a natural next step. From a42280d6555f84eb94d739c6748018b59165297a Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 7 Jun 2024 09:48:21 +0200 Subject: [PATCH 06/22] Small fix --- proposals/improved-resolution-expected-type.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 5ba816dcd..9e13e792b 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -172,7 +172,7 @@ sealed interface Either { One way in which we can improve resolution even more is by considering the subclasses of the known type of an expression. Making every potential subclass available would be quite surprising, but sealed hierarchies form an interesting subset (and the information is directly accessible to the compiler). -At this point, we have decided against it for practical reasons. If the subclasses are defined inside the parent class (like in `Either` above), this proposal already helps because the subclasses are in the static scope of the parent. If they are inside the parent, then we are not making the particular piece of code any smaller, only avoiding one import. Since imports are usually disregarded by the developers anyway, it seems that adding all sealed subclasses to the scope brings no additional benefit. +At this point, we have decided against it for practical reasons. If the subclasses are defined inside the parent class (like in `Either` above), this proposal already helps because the subclasses are in the static scope of the parent. If they are defined outside of the parent, then we are not making the particular piece of code any smaller, only avoiding one import. Since imports are usually disregarded by the developers anyway, it seems that adding all sealed subclasses to the scope brings no additional benefit. ### Risks From 9e2619c330719738b74c1b8360ac76e57c4af53e Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 7 Jun 2024 10:09:03 +0200 Subject: [PATCH 07/22] Add example for priority --- .../improved-resolution-expected-type.md | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 9e13e792b..c1b0fd1cf 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -94,13 +94,13 @@ when (color) { } ``` -An additional advantage is that of uniformity. In particular, the code behaves as you had `*`-imported the `Color` type. +In addition, note that the rules to filter out which functions should or shouldn't be available are far from clear; potential generic substitutions are one such complex example. ## Technical details We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). -This scope has the same priority as `*`-imports. That way, the user may have the mental model that using the name of an enumeration entry without qualification, for example, is the same as if the enumeration was `*`-imported into the file. +This scope has the lowest priority and _should keep_ that lowest priority even after further extensions to the language. The mental model is that the expected type is only use for resolution purposes after any other possibility has failed. This scope is **not** propagated to children of the node in question. For example, when resolving `val x: T = f(x, y)`, the additional scope is present when resolving `f`, but not when resolving `x` and `y`. After all, `x` and `y` no longer have an expected type `T`. @@ -145,9 +145,28 @@ We introduce the additional scope during type solution in the following cases: ## Design decisions -**Priority level**: the current proposal puts the additional scope to be searched when the expected type is known at the same level as `*`-imports. This means that this feature is _not_ 100% backward-compatible, as we have the risk of ambiguity between a declaration imported in such a way, and one available in the static or companion object scope of the expected type. +**Priority level**: the current proposal makes this new scope have the lowest priority. In practical terms, that means that even built-ins and automatically imported declarations have higher priority. In a previous iteration, we made it have the same level as `*`-imports; but added ambiguity where currently there is not. -The most conservative option is for the new scope to have the lowest priority. In practical terms, that means that even built-ins and automatically imported declarations have higher priority, which seems like an odd choice too. As mentioned above, the mental model of these scopes working as `*`-imports seems like a useful tool for understanding the feature, so making them have the same priority level feels like a natural next step. +```kotlin +enum class Test: List { + FIRST; + + companion object { + fun emptyList(): Test { + return FIRST + } + } +} + +fun foo(x: Test){ + when(x) { + emptyList() -> 1 + else -> 2 + } +} +``` + +Note however that this KEEP not only states that the scope coming from the expected type has the lowest priority, but also that this should _keep being the case_ in the future. We foresee that further extensions to the language (like contexts) may add new scopes, but still the one from the type should be regarded as the "fallback mechanism". **No `.value` syntax**: Swift has a very similar feature to the one proposed in this KEEP, namely [implicit member expressions](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions#Implicit-Member-Expression). The main difference is that one has to prepend `.` to access this new type-resolution-guided scope. From 5b5e0814322cf15cdb893f9ca1a11407e4955870 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 10 Jun 2024 09:49:49 +0200 Subject: [PATCH 08/22] Change rule for `as` --- proposals/improved-resolution-expected-type.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index c1b0fd1cf..e99138312 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -52,7 +52,7 @@ fun problematic(x: String): Problem = when (x) { This KEEP addresses many of these problems by considering explicit expected types when doing name resolution. We try to propagate the information from argument and return types, declarations, and similar constructs. As a result, the following constructs are usually improved: * conditions on `when` expressions with subject, -* type checks and casts (`is`, `as`), +* type checks (`is`), * `return`, both implicit and explicit, * equality checks (`==`, `!=`). @@ -140,7 +140,6 @@ All other operators and compound assignments (such as `x += e`) do not propagate We introduce the additional scope during type solution in the following cases: * _Type check_: in `e is T`, `e !is T`, the known type of `e` is propagated to `T`. -* _Type cast_: in `e as T` and `e as? T`, the known type of `e` is propagated to `T`. * _`when` expression with subject_: in `when (x) { is T -> ... }`, then known type of `x` is propagated to `T`. ## Design decisions @@ -180,6 +179,11 @@ The current choice is obviously not symmetric: it might be surprising that `p == On the other hand, the proposed flow of information is consistent with the ability to refactor `when (x) { A -> ...}` into `when { x == A -> ...}`, without any further qualification required. We think that this uniformity is important for developers. +**On type cast**: a previous iteration included the additional rule "in `e as T` and `e as? T`, the known type of `e` is propagated to `T`", but this has been dropped. There are two reasons for this change: + +1. On a conceptual level, it is not immediately obvious what happens if `e as T` as a whole also has an expected type: should `T` be resolved using the known type of `e` or that expected type? It's possible to create an example where depending on the answer the resolution differs, and this could be quite surprising to users. +2. On a technical level, the compiler _sometimes_ uses the type `T` to guide generic instantiation of `e`. This conflicts with the rule above. + **Sealed subclasses**: the rules above handle enumeration, and it's also useful when defining a hierarchy of classes nested on the parent. ```kotlin @@ -203,4 +207,4 @@ The third potential risk is whether this additional scope may lead to surprises. ## Implementation note -In the current K2 compiler, these rules amount to considering those places in which `WithExpectedType` is passed as the `ResolutionMode`, plus adding special rules for `as`, `is`, and `==`. Since `when` with subject is desugared as either `x == e` or `x is T`, we need no additional rules to cover them. \ No newline at end of file +In the current K2 compiler, these rules amount to considering those places in which `WithExpectedType` is passed as the `ResolutionMode`, plus adding special rules for `is`, and `==`. Since `when` with subject is desugared as either `x == e` or `x is T`, we need no additional rules to cover them. \ No newline at end of file From dcf647c8f935916855c483b446bdd80ad9e1f5f6 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 17 Jun 2024 13:41:31 +0200 Subject: [PATCH 09/22] Add link to discussion --- proposals/improved-resolution-expected-type.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index e99138312..ce9f6a82f 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -4,7 +4,7 @@ * **Author**: Alejandro Serrano Mena * **Status**: In discussion * **Prototype**: Implemented in [this branch](https://github.com/JetBrains/kotlin/compare/rr/serras/improved-resolution-expected-type) -* **Discussion**: ?? +* **Discussion**: [KEEP-379](https://github.com/Kotlin/KEEP/issues/379) * **Related issues**: [KT-9493](https://youtrack.jetbrains.com/issue/KT-9493/Allow-short-enum-names-in-when-expressions), [KT-44729](https://youtrack.jetbrains.com/issue/KT-44729/Context-sensitive-resolution), [KT-16768](https://youtrack.jetbrains.com/issue/KT-16768/Context-sensitive-resolution-prototype-Resolve-unqualified-enum-constants-based-on-expected-type), [KT-58939](https://youtrack.jetbrains.com/issue/KT-58939/K2-Context-sensitive-resolution-of-Enum-leads-to-Unresolved-Reference-when-Enum-has-the-same-name-as-one-of-its-Entries) ## Abstract From 739e1fdda0011dd3f15c26dd18e105ff66ca9b1b Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 19 Jun 2024 10:00:13 +0200 Subject: [PATCH 10/22] Clarifications --- .../improved-resolution-expected-type.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index ce9f6a82f..adfd4655a 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -96,6 +96,19 @@ when (color) { In addition, note that the rules to filter out which functions should or shouldn't be available are far from clear; potential generic substitutions are one such complex example. +Extension functions defined in the static and companion object scopes are also available. + +```kotlin +class Duration { + companion object { + val Int.seconds: Duration get() = ... + } +} + +// we can use 'seconds' without additional imports +val d: Duration = 1.seconds +``` + ## Technical details We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). @@ -169,9 +182,11 @@ Note however that this KEEP not only states that the scope coming from the expec **No `.value` syntax**: Swift has a very similar feature to the one proposed in this KEEP, namely [implicit member expressions](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions#Implicit-Member-Expression). The main difference is that one has to prepend `.` to access this new type-resolution-guided scope. -The big drawback of this choice is that new syntax ambiguities may arise, and these are very difficult to predict upfront. Code that compiled perfectly may now have an ambiguous reading by the compiler. +The big drawback of this choice is that new syntax ambiguities may arise, and these are very difficult to predict upfront. Code that compiled perfectly may now have an ambiguous reading by the compiler. The reason is that `foo.bar`, now unambiguously a field access, would get an additional interpretation as part of an infix function call following with `.bar`. Note that using `.` is very common in Kotlin code, so we should be extremely careful on that regard. + +One possibility would be to make the parsing dependent on some compiler flag. However, that means that now parsing the file depends on some external input (Gradle file), so tools need to be updated to consult this (which is not always trivial). This goes against the efforts in toolability from the Kotlin team. -Furthermore, Kotliners _already_ use a pattern to avoid re-writing the name of an enumeration over and over, namely importing all the enumeration entries, which makes them available without any `.` at the front. It seems better to support the style that the community already uses than trying to make people change their habits. +One very important difference with Swift is that we take a restrictive route, in which we are very clear about when you can expect types to guide resolution. Swift, on the other hand, allows `.value` syntax everywhere, and tries its best to decide which is the correct way to resolve it. We explicitly do not want to take Swift's route, because it makes the complexity of type checking and resolution much worse; which again goes against giving great tooling. **On equality**: using the rules above, equalities propagate type information from left to right. But there are other two options: propagating from right to left, or even not propagating any information at all. From d82b448160002981012c6c3c535b9e98f2d71621 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 21 Jun 2024 10:35:05 +0200 Subject: [PATCH 11/22] Clarify what happens with lambdas --- proposals/improved-resolution-expected-type.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index adfd4655a..532678f18 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -74,6 +74,12 @@ Improving the resolution of arguments based on the function call would amount to As a consequence, operators in Kotlin that are desugared to function calls which in turn get resolved, like `in` or `thing[x]`, are also outside of the scope of this KEEP. Note that [value equalities](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) (`==`, `!=`) are not part of that group, since they are always resolved to `kotlin.Any.equals`. +Expected type information is propagate to _none_ of the arguments, and that includes trailing lambda arguments. For example, the following fails to compile: + +```kotlin +fun problemByNumber(n: Int): Problem = n.let { UNKNOWN } +``` + ### Importing the entire scopes The current proposal imports the _entire_ static and companion object scopes, which include classes, properties, and functions. Whereas the first two are needed to cover common cases like enumeration entries, and comparison with objects, the usefulness of methods seems debatable. However, it allows some interesting constructions where we compare against a value coming from a factory: @@ -133,6 +139,12 @@ If a type `T` is propagated to a block, then the type is propagated to every ret * _Explicit `return`_: `return e`, * _Implicit `return`_: the last statement. +If a functional type `(...) -> R` is propagated to a lambda, then the return type `R` is propagated to the body of the lambda (alongside the parameter types being propagated to the formal parameters, if available). + +```kotlin +val unknown: () -> Problem = { UNKNOWN } +``` + For other statements and expressions, we have the following rules. Here "known type of `x`" includes any additional typing information derived from smart casting. * _Assignments_: in `x = e`, the known type of `x` is propagated to `e`, From 0abbe1560e0be2c03b0e51a8c684a1b60dd530bc Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 22 Jul 2024 15:01:13 +0200 Subject: [PATCH 12/22] Add section about intersection types --- .../improved-resolution-expected-type.md | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 532678f18..b62970ba2 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -115,6 +115,21 @@ class Duration { val d: Duration = 1.seconds ``` +#### Intersection types + +Sometimes the type to be propagated is an [intersection type](https://kotlinlang.org/spec/type-system.html#intersection-types); for example, when dealing with smart casting. In that case, prior to obtaining the static and companion object scopes, we should perform [type approximation](https://kotlinlang.org/spec/type-system.html#type-approximation), but only with the subset of intersected types which is _not_ a type parameter. + +For example, in the following code, at the `d == 1.seconds` expression, the known type of `d` is `T & Any & Duration`. When approximating, the `T` is dropped, and the intersection becomes the trivial `Duration`. + +```kotlin +fun foo(d: T): Int = when { + d is Duration && d == 1.seconds -> 0 + else -> 1 +} +``` + +With this design, it is always clear which is the sole type from which we obtain the companion object. This should cover the common case in which the smartcasted type is a subtype of the original one. Another possibility is to check all the companion objects of the intersected types. + ## Technical details We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). @@ -132,6 +147,7 @@ For declarations, a type `T` is propagated to the body, which may be an expressi * _Default parameters of functions_: `x: T = e`; * _Initializers of properties with explicit type_: `val x: T = e`; * _Explicit return types of functions_: `fun f(...): T = e` or `fun f(...): T { ... }`, + * This includes _accessors_ with explicit type: `val x get(): T = e`, * _Getters of properties with explicit type_: `val x: T get() = e` or `val x: T get() { ... }`. If a type `T` is propagated to a block, then the type is propagated to every return point of the block. @@ -142,13 +158,18 @@ If a type `T` is propagated to a block, then the type is propagated to every ret If a functional type `(...) -> R` is propagated to a lambda, then the return type `R` is propagated to the body of the lambda (alongside the parameter types being propagated to the formal parameters, if available). ```kotlin -val unknown: () -> Problem = { UNKNOWN } +val unknown: () -> Problem = { + // ^ propagated as type of the lambda + + UNKNOWN // implicit return: we use 'Problem' for resolution +} ``` For other statements and expressions, we have the following rules. Here "known type of `x`" includes any additional typing information derived from smart casting. * _Assignments_: in `x = e`, the known type of `x` is propagated to `e`, -* _`when` expression with subject_: in `when (x) { e -> ... }`, then known type of `x` is propagated to `e`, when `e` is not of the form `is T` or `in e`, +* _`when` expression with subject_: in `when (x) { t -> ... }`, then known type of `x` is propagated to `t`, when `c` is an expression (that is, not of the form `is S`, `in l`, or their negations), + * For [guards](https://github.com/Kotlin/KEEP/blob/guards/proposals/guards.md) `when (x) { t if c -> ... }`, the type is propagated to `t`, but not to the guard `c`, * _Branching_: if type `T` is propagated to an expression with several branches, the type `T` is propagated to each of them, * _Conditionals_, either `if` or `when`, * _`try` expressions_, where the type `T` is propagated to the `try` block and each of the `catch` handlers, @@ -165,7 +186,8 @@ All other operators and compound assignments (such as `x += e`) do not propagate We introduce the additional scope during type solution in the following cases: * _Type check_: in `e is T`, `e !is T`, the known type of `e` is propagated to `T`. -* _`when` expression with subject_: in `when (x) { is T -> ... }`, then known type of `x` is propagated to `T`. +* _`when` expression with subject_: in `when (x) { is T -> ... }`, then known type of `x` is propagated to `T`, + * For [guards](https://github.com/Kotlin/KEEP/blob/guards/proposals/guards.md) `when (x) { is T if c -> ... }`, the type is propagated to `T`, but not to the guard `c`. ## Design decisions From 689df15ea5aaa0374d8d0b21637bdaed02057a48 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 28 Aug 2024 11:15:34 +0200 Subject: [PATCH 13/22] Small suggestions --- proposals/improved-resolution-expected-type.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index b62970ba2..dddad38b4 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -132,9 +132,9 @@ With this design, it is always clear which is the sole type from which we obtain ## Technical details -We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). +We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). In platforms that allow defining static callables directly on types, like the JVM, those are included too. -This scope has the lowest priority and _should keep_ that lowest priority even after further extensions to the language. The mental model is that the expected type is only use for resolution purposes after any other possibility has failed. +This scope has the lowest priority (even lower than that of default and star imports) and _should keep_ that lowest priority even after further extensions to the language. The mental model is that the expected type is only use for resolution purposes after any other possibility has failed. This scope is **not** propagated to children of the node in question. For example, when resolving `val x: T = f(x, y)`, the additional scope is present when resolving `f`, but not when resolving `x` and `y`. After all, `x` and `y` no longer have an expected type `T`. @@ -168,7 +168,7 @@ val unknown: () -> Problem = { For other statements and expressions, we have the following rules. Here "known type of `x`" includes any additional typing information derived from smart casting. * _Assignments_: in `x = e`, the known type of `x` is propagated to `e`, -* _`when` expression with subject_: in `when (x) { t -> ... }`, then known type of `x` is propagated to `t`, when `c` is an expression (that is, not of the form `is S`, `in l`, or their negations), +* _`when` expression with subject_: in `when (x) { t -> ... }`, then known type of `x` is propagated to `t`, when `t` is an expression (that is, not of the form `is S`, `in l`, or their negations), * For [guards](https://github.com/Kotlin/KEEP/blob/guards/proposals/guards.md) `when (x) { t if c -> ... }`, the type is propagated to `t`, but not to the guard `c`, * _Branching_: if type `T` is propagated to an expression with several branches, the type `T` is propagated to each of them, * _Conditionals_, either `if` or `when`, From 84ae2dd1ff990f6f4ca91b6416e7bbc4e14b1b3f Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 6 Sep 2024 11:58:17 +0200 Subject: [PATCH 14/22] Clarify rules with supertypes --- .../improved-resolution-expected-type.md | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index dddad38b4..2eb9d28e0 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -54,7 +54,8 @@ This KEEP addresses many of these problems by considering explicit expected type * conditions on `when` expressions with subject, * type checks (`is`), * `return`, both implicit and explicit, -* equality checks (`==`, `!=`). +* equality checks (`==`, `!=`), +* assignments. ### The issue with overloading @@ -115,11 +116,30 @@ class Duration { val d: Duration = 1.seconds ``` -#### Intersection types +#### Scopes from supertypes -Sometimes the type to be propagated is an [intersection type](https://kotlinlang.org/spec/type-system.html#intersection-types); for example, when dealing with smart casting. In that case, prior to obtaining the static and companion object scopes, we should perform [type approximation](https://kotlinlang.org/spec/type-system.html#type-approximation), but only with the subset of intersected types which is _not_ a type parameter. +When supertypes are involved, we may have more than one static and companion object scope to choose from. -For example, in the following code, at the `d == 1.seconds` expression, the known type of `d` is `T & Any & Duration`. When approximating, the `T` is dropped, and the intersection becomes the trivial `Duration`. +```kotlin +interface A { + companion object { + val Foo: A = ... + } +} + +class B: A { } + +// now when we match on a B... +when (b) { + Foo -> ... // is Foo in scope? +} +``` + +The current proposal answers "yes" to the question in the code above. We look at the static and companion object scopes of the type and every supertype. There are two reasons for this decision. + +The first one is that enables _safe refactoring_. Imagine that you have some code in which you know that a variable is of type 'A', so when you use `when` on it you can use `Foo` directly. However, because of changes on your code, now at the point in which `when` is performed, you know more about that variable: that the type is 'B'. However, if we don't look at the supertypes, the code you had written becomes incorrect, since 'Foo' would no longer be found. This breaks the basic expectation of subtyping in which you can always replace of value of a type with one of a subtype. + +The second reason is that it allows us to give a clean behavior for [intersection type](https://kotlinlang.org/spec/type-system.html#intersection-types). Although these types are not denotable by the developer, the arise often in combination with smart casting. For example, in the following code, at the `d == 1.seconds` expression, the known type of `d` is `T & Any & Duration`. Looking at all supertypes means in particular looking at `Duration`, which allows us to resolve the call to `seconds`. ```kotlin fun foo(d: T): Int = when { @@ -128,19 +148,21 @@ fun foo(d: T): Int = when { } ``` -With this design, it is always clear which is the sole type from which we obtain the companion object. This should cover the common case in which the smartcasted type is a subtype of the original one. Another possibility is to check all the companion objects of the intersected types. +One downside of this extended search is potential performance problems. Fortunately, most Kotlin code have a relatively shallow type hierarchy, so this should not be a problem. Our preliminary benchmarks against the Kotlin and IntelliJ codebases show no performance degradation. ## Technical details -We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). In platforms that allow defining static callables directly on types, like the JVM, those are included too. +We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`and its supertypes, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). In platforms that allow defining static callables directly on types, like the JVM, those are included too. This scope has the lowest priority (even lower than that of default and star imports) and _should keep_ that lowest priority even after further extensions to the language. The mental model is that the expected type is only use for resolution purposes after any other possibility has failed. This scope is **not** propagated to children of the node in question. For example, when resolving `val x: T = f(x, y)`, the additional scope is present when resolving `f`, but not when resolving `x` and `y`. After all, `x` and `y` no longer have an expected type `T`. +There is no relative precedence between the members coming from the type `T` and those coming from their supertypes. For example, if two of the involved types define a property with the same name in the companion object, none of them hides the other, which means it leads to an ambiguity error. + ### Additional candidate resolution scope -We propagate `T` to an expression `e` whenever the specification states _"the type of `e` must be a subtype of `T`"_. +We propagate `T` to an expression `e` whenever the specification states _"the type of `e` must be a subtype of `T`"_. For declarations, a type `T` is propagated to the body, which may be an expression or a block. Note that we only propagate types given _explicitly_ by the developer. From ca65fc7b599cd139b1a326c59aad344b161219d9 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 15 Nov 2024 15:28:59 +0100 Subject: [PATCH 15/22] New version with function arguments --- .../improved-resolution-expected-type.md | 247 +++++++++++------- 1 file changed, 153 insertions(+), 94 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 2eb9d28e0..cab8774f0 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -47,6 +47,8 @@ fun problematic(x: String): Problem = when (x) { "database" -> Problem.DATABASE else -> Problem.UNKNOWN } + +val databaseProblemMessage: String = message(Problem.DATABASE) ``` This KEEP addresses many of these problems by considering explicit expected types when doing name resolution. We try to propagate the information from argument and return types, declarations, and similar constructs. As a result, the following constructs are usually improved: @@ -55,35 +57,51 @@ This KEEP addresses many of these problems by considering explicit expected type * type checks (`is`), * `return`, both implicit and explicit, * equality checks (`==`, `!=`), -* assignments. - -### The issue with overloading +* assignments and initializations, +* function calls. -We leave out of this KEEP any extension to the propagation of information from function calls to their argument. One particularly visible implication of this choice is that you still need to qualify enumeration entries that appear as arguments as currently done in Kotlin. Following the example above, you still need to qualify the argument to `message`. +As a very short summary of this proposal, all usages of `Problem.` can be removed. Intuitively, the compiler uses the known or expected type to resolve the enumeration entries. ```kotlin -val s = message(Problem.CONNECTION) +fun message(problem: Problem): String = when (problem) { + CONNECTION -> "connection" + AUTHENTICATION -> "authentication" + // other cases +} + +fun problematic(x: String): Problem = when (x) { + "connection" -> CONNECTION + // other cases +} + +val databaseProblemMessage: String = message(DATABASE) ``` -What sets function calls apart from the constructs mentioned before is overloading. In Kotlin, there's a certain sequence in which function calls are resolved and type checked, as spelled out in the [specification](https://kotlinlang.org/spec/overload-resolution.html#overload-resolution). +### What is available in the contextual scope -1. Infer the types of non-lambda arguments. -2. Choose an overload for the function based on the gathered type. -3. Check the types of lambda arguments. +The core of this proposal is the addition of a scope, the **contextual scope**, whose available members are defined by type information known to the compiler at that point. In order to describe what this proposal allows as contextually-scoped identifiers, we need to separate the two positions in which such an identifier may occur: -Improving the resolution of arguments based on the function call would amount to reversing the order of (1) and (2), at least partially. There are potential techniques to solve this problem, but another KEEP seems more appropriate. +* _Type position_: as the right-hand side of `is`, both in standalone and branch conditions. +* _Expression position_: in any place where an expression is expected. -As a consequence, operators in Kotlin that are desugared to function calls which in turn get resolved, like `in` or `thing[x]`, are also outside of the scope of this KEEP. Note that [value equalities](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) (`==`, `!=`) are not part of that group, since they are always resolved to `kotlin.Any.equals`. +Note the restrictive nature of the type position. Improved resolution is not available anywhere else: not in type annotations for variables, not in the list of supertypes or generic constraints, and so on. -Expected type information is propagate to _none_ of the arguments, and that includes trailing lambda arguments. For example, the following fails to compile: +In **type position** only classifiers which are both _nested_ and _sealed inheritors_ of the expected type are available. ```kotlin -fun problemByNumber(n: Int): Problem = n.let { UNKNOWN } -``` +sealed interface Either { -### Importing the entire scopes + data class Left(val error: E): Either + data class Right(val value: A): Either +} -The current proposal imports the _entire_ static and companion object scopes, which include classes, properties, and functions. Whereas the first two are needed to cover common cases like enumeration entries, and comparison with objects, the usefulness of methods seems debatable. However, it allows some interesting constructions where we compare against a value coming from a factory: +fun Either.getOrElse(default: A) = when (this) { + is Left -> default + is Right -> value +} +``` + +In **expression position** we extend the scope mention in the type position with _no-argument_ callables from the static and companion object scopes of the expected type. Although the technical description appears below, the intuition is those members which would be available using `ExpectedType.` and with no additional arguments in parentheses. ```kotlin class Color(...) { @@ -94,90 +112,82 @@ class Color(...) { } } -// now when we match on a color... +val Color.Companion.BLUE: Color = ... + +// now when we match on a `color: Color`... when (color) { - WHITE -> ... - fromRGB(10, 10, 10) -> ... + WHITE -> ... // OK + fromRGB(10, 10, 10) -> ... // NO, fromRGB has arguments + BLUE -> ... // OK, extension function over companion object } ``` -In addition, note that the rules to filter out which functions should or shouldn't be available are far from clear; potential generic substitutions are one such complex example. - -Extension functions defined in the static and companion object scopes are also available. +**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. ```kotlin -class Duration { +class Color(...) { companion object { - val Int.seconds: Duration get() = ... + val Int.grey get() = ... } } -// we can use 'seconds' without additional imports -val d: Duration = 1.seconds +when (color) { + 10.grey -> ... // this is not allowed +} ``` -#### Scopes from supertypes - -When supertypes are involved, we may have more than one static and companion object scope to choose from. +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 -interface A { +class Color(...) { companion object { - val Foo: A = ... + val NUMBER_OF_COLORS: Long = 255 * 255 * 255 } } -class B: A { } - -// now when we match on a B... -when (b) { - Foo -> ... // is Foo in scope? +when (color) { + NUMBER_OF_COLORS -> ... // type mismatch } ``` -The current proposal answers "yes" to the question in the code above. We look at the static and companion object scopes of the type and every supertype. There are two reasons for this decision. - -The first one is that enables _safe refactoring_. Imagine that you have some code in which you know that a variable is of type 'A', so when you use `when` on it you can use `Foo` directly. However, because of changes on your code, now at the point in which `when` is performed, you know more about that variable: that the type is 'B'. However, if we don't look at the supertypes, the code you had written becomes incorrect, since 'Foo' would no longer be found. This breaks the basic expectation of subtyping in which you can always replace of value of a type with one of a subtype. +We do **not** look in the static and companion object scopes of **supertypes** of the expected type. This is in accordance to the rules of [resolution with an explicit type receiver](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). Technically, we perform some pre-processing of the expected type to simplify it, but the general rule is that only _one_ classifier is searched. -The second reason is that it allows us to give a clean behavior for [intersection type](https://kotlinlang.org/spec/type-system.html#intersection-types). Although these types are not denotable by the developer, the arise often in combination with smart casting. For example, in the following code, at the `d == 1.seconds` expression, the known type of `d` is `T & Any & Duration`. Looking at all supertypes means in particular looking at `Duration`, which allows us to resolve the call to `seconds`. +### No-argument callables -```kotlin -fun foo(d: T): Int = when { - d is Duration && d == 1.seconds -> 0 - else -> 1 -} -``` +The reason for restricting available members to no-argument ones is to avoid an _resolution explosion_ in the case of nested function calls. To understand the problem, we need to understand the sequence in which function calls are resolved and type checked, which is spelled out in the [specification](https://kotlinlang.org/spec/overload-resolution.html#overload-resolution). -One downside of this extended search is potential performance problems. Fortunately, most Kotlin code have a relatively shallow type hierarchy, so this should not be a problem. Our preliminary benchmarks against the Kotlin and IntelliJ codebases show no performance degradation. +1. Infer the types of non-lambda arguments. +2. Choose an overload for the function based on the gathered types. +3. Check the types of lambda arguments. -## Technical details +Let us forget for a moment about lambda arguments; in that case the procedure is completely bottom-up: we move from arguments to function calls, performing resolution at each stage of the process. However, to improve the resolution we sometimes need to perform the tree walk in the other direction: from resolving the function overload we know the expected type of the argument, which we can use to resolve that expression. -We introduce an additional scope, present both in type and candidate resolution, which always depends on a type `T` (we say we **propagate `T`**). This scope contains the static and companion object callables of the aforementioned type `T`and its supertypes, as defined by the [specification](https://kotlinlang.org/spec/overload-resolution.html#call-with-an-explicit-type-receiver). In platforms that allow defining static callables directly on types, like the JVM, those are included too. +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. -This scope has the lowest priority (even lower than that of default and star imports) and _should keep_ that lowest priority even after further extensions to the language. The mental model is that the expected type is only use for resolution purposes after any other possibility has failed. +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. -This scope is **not** propagated to children of the node in question. For example, when resolving `val x: T = f(x, y)`, the additional scope is present when resolving `f`, but not when resolving `x` and `y`. After all, `x` and `y` no longer have an expected type `T`. +## Technical details -There is no relative precedence between the members coming from the type `T` and those coming from their supertypes. For example, if two of the involved types define a property with the same name in the companion object, none of them hides the other, which means it leads to an ambiguity error. +We start by defining how the expected type is actually propagated, and then describe changes to the resolution for the new contextually-scoped identifiers. -### Additional candidate resolution scope +### Expected type propagation -We propagate `T` to an expression `e` whenever the specification states _"the type of `e` must be a subtype of `T`"_. +In general, we say that the expected type of `e` must be `T` whenever the specification states _"the type of `e` must be a subtype of `T`"_. In this section we formally describe the propagation of the expected type, that is, how the expected type of an expression depends on the expected type of its parent. -For declarations, a type `T` is propagated to the body, which may be an expression or a block. Note that we only propagate types given _explicitly_ by the developer. +For declarations, the expected type `T` is propagated to the body, which may be an expression or a block. Note that we only propagate types given _explicitly_ by the developer. * _Default parameters of functions_: `x: T = e`; * _Initializers of properties with explicit type_: `val x: T = e`; * _Explicit return types of functions_: `fun f(...): T = e` or `fun f(...): T { ... }`, - * This includes _accessors_ with explicit type: `val x get(): T = e`, + * This includes _accessors_ with explicit type: `val x get(): T = e`; * _Getters of properties with explicit type_: `val x: T get() = e` or `val x: T get() { ... }`. -If a type `T` is propagated to a block, then the type is propagated to every return point of the block. +If a type `T` is expected for a block, then the type is propagated to every return point of the block. * _Explicit `return`_: `return e`, * _Implicit `return`_: the last statement. -If a functional type `(...) -> R` is propagated to a lambda, then the return type `R` is propagated to the body of the lambda (alongside the parameter types being propagated to the formal parameters, if available). +If a functional type `(...) -> R` is expected for a lambda expression, then the return type `R` is propagated to the body of the lambda (alongside the parameter types being propagated to the formal parameters, if available). ```kotlin val unknown: () -> Problem = { @@ -189,46 +199,94 @@ val unknown: () -> Problem = { For other statements and expressions, we have the following rules. Here "known type of `x`" includes any additional typing information derived from smart casting. -* _Assignments_: in `x = e`, the known type of `x` is propagated to `e`, -* _`when` expression with subject_: in `when (x) { t -> ... }`, then known type of `x` is propagated to `t`, when `t` is an expression (that is, not of the form `is S`, `in l`, or their negations), - * For [guards](https://github.com/Kotlin/KEEP/blob/guards/proposals/guards.md) `when (x) { t if c -> ... }`, the type is propagated to `t`, but not to the guard `c`, -* _Branching_: if type `T` is propagated to an expression with several branches, the type `T` is propagated to each of them, +* _Assignments_: in `x = e`, the expected type of `x` is propagated to `e`; +* _Type check_: in `e is T`, `e !is T`, the known type of `e` is propagated to `T`; +* _Branching_: if type `T` is expected for an with several branches, the type `T` is propagated to each of them, * _Conditionals_, either `if` or `when`, - * _`try` expressions_, where the type `T` is propagated to the `try` block and each of the `catch` handlers, -* _Elvis operator_: if type `T` is propagated to `e1 ?: e2`, then we propagate `T?` to `e1` and `T` to `e2`. -* _Not-null assertion_: if type `T` is propagated to `e!!`, then we propagate `T?` to `e`, + * _`try` expressions_, where the type `T` is propagated to the `try` block and each of the `catch` handlers; +* _`when` expression with subject_: those cases should be handled as if the subject had been inlined on every condition, as described in the [specification](https://kotlinlang.org/spec/expressions.html#when-expressions); +* _Elvis operator_: if type `T` is expected for `e1 ?: e2`, then we propagate `T?` to `e1` and `T` to `e2`; +* _Not-null assertion_: if type `T` is expected for `e!!`, then we propagate `T?` to `e`; * _Equality_: in `a == b` and `a != b`, the known type of `a` is propagated to `b`. - * This helps in common cases like `p == Problem.CONNECTION`. - * Note that in this case the expected type should only be propagated for the purposes of name resolution. The [specification](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) mandates `a == b` to be equivalent to `(A as? Any)?.equals(B as Any?) ?: (B === null)` in the general case, so from a typing perspective there should be no constraint on the type of `b`. - -All other operators and compound assignments (such as `x += e`) do not propagate information. The reason is that those operators may be _overloaded_, so we cannot guarantee their type. + * This helps in common cases like `p == CONNECTION`. + * Note that in this case the expected type should only be propagated for the purposes of additional resolution. The [specification](https://kotlinlang.org/spec/expressions.html#value-equality-expressions) mandates `a == b` to be equivalent to `(A as? Any)?.equals(B as Any?) ?: (B === null)` in the general case, so from a typing perspective there should be no constraint on the type of `b`. + +> [!NOTE] +> In the current K2 compiler, these rules amount to considering those places in which `WithExpectedType` is passed as the `ResolutionMode`, plus adding special rules for `is`, `==`, and updating overload resolution. Since `when` with subject is desugared as either `x == e` or `x is T`, we need no additional rules to cover them. + +### Single definite expected type + +There are some scenarios in which the expected type propagation may lead to complex types, like intersections or bounded type parameters. In order to define exactly which scope we should look into, we introduce the notion of the **single definite expected type**, `sdet(T)`, which is defined recursively, starting with the expected type propagated by the rules above. It is possible for this type to be undefined. + +* Built-in and classifier types: `sdet(T) = T`. + * Note that type arguments do not influence the scope. +* Type parameters: + * If there is a single supertype, ``, `sdet(T) = sdet(A)`, + * 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. + * 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. + +### Additional contextual scope + +Whenever they is a single definite expected type for an expression, this is resolved with an additional **contextual** scope. This scope has the lowest priority (even lower than that of default and star imports) and _should keep_ that lowest priority even after further extensions to the language. The mental model is that the expected type is only use for resolution purposes after any other possibility has failed. + +The contextual scope for a single definite type `T` is made from three different sources: + +1. The static scope of the type `T`, +2. The companion object scope of the type `T`, +3. Imported extension functions over the companion object of `T`. -### Additional type resolution scope +Furthermore, only two kinds of members are available: -We introduce the additional scope during type solution in the following cases: +1. Classifiers which are nested in and inherit from type `T`, if `T` is a `sealed` class. +2. No-argument callables, which must satisfy: + - Having no value parameters, which includes both properties and enumeration entries, + - Having no extension receiver, except for imported extension functions defined over the companion object of `T`. -* _Type check_: in `e is T`, `e !is T`, the known type of `e` is propagated to `T`. -* _`when` expression with subject_: in `when (x) { is T -> ... }`, then known type of `x` is propagated to `T`, - * For [guards](https://github.com/Kotlin/KEEP/blob/guards/proposals/guards.md) `when (x) { is T if c -> ... }`, the type is propagated to `T`, but not to the guard `c`. +### Changes to [overload resolution](https://kotlinlang.org/spec/overload-resolution.html#overload-resolution) + +In order to accomodate improved resolution for function arguments, the algorithm must be slightly modified. As a reminder, overload resolution for a function call `f(...)` takes the following steps: + +1. Resolve all non-lambda arguments, +2. _Applicability_: gather all potential overloads of the function `f`, and for each of them generate a constraint system that specifies the requirements between argument and parameter types. Filter out those overloads for which the constraint system is unsatisfiable. If no applicable overloads remain after this step, an _unresolved error_ is issued. +3. _Choice of most specific overload_: if more than one applicable overload remains after the previous step, try to decide which is the "most specific" by applying [some rules](https://kotlinlang.org/spec/overload-resolution.html#choosing-the-most-specific-candidate-from-the-overload-candidate-set). After this step, only one overload should remain, otherwise an _ambiguity error_ is issued. +4. _Completion_: use the information from the chosen overload to resolve lambda arguments, callable references, and fix type variables. + +The first change relates to _no-argument expressions_, that is, those made only from a [`simpleIdentifier`](https://kotlinlang.org/spec/syntax-and-grammar.html#grammar-rule-simpleIdentifier). + +1. The expression has no argument, so step (1) does not apply. +2. If during the _applicability_ phase no potential overloads are found, and the expression appears as argument to a function, then do not issue an error, but rather mark the expression as **delayed**. + +The second change relates to any function call, which now must handle delayed expressions as arguments. + +1. Resolve all non-lambda arguments, where some of those may become delayed. +2. During the _applicability_ step, do not introduce information about delayed arguments inside each of the constraint systems. + - **Open question**: should we filter out those candidates for which the resolution of the delayed argument with the corresponding parameter type fails? +3. The choice of the most specific overload remains the same. +4. During _completion_, delayed arguments are resolved again using the expected type obtained from the overload chosen in step (3). + - If the answer to open question above is affirmative, such resolution would be done as part of the applicability step. ## Design decisions **Priority level**: the current proposal makes this new scope have the lowest priority. In practical terms, that means that even built-ins and automatically imported declarations have higher priority. In a previous iteration, we made it have the same level as `*`-imports; but added ambiguity where currently there is not. ```kotlin -enum class Test: List { - FIRST; - - companion object { - fun emptyList(): Test { - return FIRST - } - } +sealed interface Test { + object Any : Test { } } fun foo(x: Test){ when(x) { - emptyList() -> 1 + Any -> 1 // should still resolve to kotlin.Any else -> 2 } } @@ -255,18 +313,23 @@ On the other hand, the proposed flow of information is consistent with the abili 1. On a conceptual level, it is not immediately obvious what happens if `e as T` as a whole also has an expected type: should `T` be resolved using the known type of `e` or that expected type? It's possible to create an example where depending on the answer the resolution differs, and this could be quite surprising to users. 2. On a technical level, the compiler _sometimes_ uses the type `T` to guide generic instantiation of `e`. This conflicts with the rule above. -**Sealed subclasses**: the rules above handle enumeration, and it's also useful when defining a hierarchy of classes nested on the parent. +**Interaction with smart casting**: the main place in which the notion of "single definite expected type" may bring some problems is smart castings, as they are the main source of intersection types. + +The following code does _not_ work under the current rules. ```kotlin -sealed interface Either { - data class Left(error: E): Either - data class Right(value: A): Either +class Box(val value: T) + +fun Box.foo() = when (value) { + is Problem if value == UNKNOWN -> ... + ... } ``` -One way in which we can improve resolution even more is by considering the subclasses of the known type of an expression. Making every potential subclass available would be quite surprising, but sealed hierarchies form an interesting subset (and the information is directly accessible to the compiler). +The problem is that at the expression `value == UNKNOWN`, the known type of `value` is `T & Problem`, a case for which the single definite expected type is undefined. There are two reasons for this choice: -At this point, we have decided against it for practical reasons. If the subclasses are defined inside the parent class (like in `Either` above), this proposal already helps because the subclasses are in the static scope of the parent. If they are defined outside of the parent, then we are not making the particular piece of code any smaller, only avoiding one import. Since imports are usually disregarded by the developers anyway, it seems that adding all sealed subclasses to the scope brings no additional benefit. +- It is unclear in general how to treat intersection types, since other cases may not be as simple as dropping a type argument. +- If in the future Kotlin gets a feature similar to GADTs, we may piggy back on the knowledge that `T` is equal to `Problem`, and face no problem in computing the single definite expected type. ### Risks @@ -274,8 +337,4 @@ One potential risk of this proposal is the difficulty of understanding _when_ ex Another potential risk is that we add more coupling between type inference and candidate resolution. On the other hand, in Kotlin those two processes are inevitably linked together -- to resolve the candidates of a call you need the type of the receivers and arguments -- so the step taken by this proposal feels quite small in comparison. -The third potential risk is whether this additional scope may lead to surprises. In particular, whether programs are accepted which are not expected by the developer, or the resolution points to a different declaration than expected. We think that the very low priority of the new scope is enough to mitigate those problems. In any case, IDE implementors should be aware of this new feature of the language, providing their usual support for code navigation. - -## Implementation note - -In the current K2 compiler, these rules amount to considering those places in which `WithExpectedType` is passed as the `ResolutionMode`, plus adding special rules for `is`, and `==`. Since `when` with subject is desugared as either `x == e` or `x is T`, we need no additional rules to cover them. \ No newline at end of file +The third potential risk is whether this additional scope may lead to surprises. In particular, whether programs are accepted which are not expected by the developer, or the resolution points to a different declaration than expected. We think that the very low priority of the new scope is enough to mitigate those problems. In any case, IDE implementors should be aware of this new feature of the language, providing their usual support for code navigation. \ No newline at end of file From 42f9783783c5931475fb45b86c4d8aa702d9a52e Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 18 Nov 2024 11:23:32 +0100 Subject: [PATCH 16/22] Examples for problems with functions --- .../improved-resolution-expected-type.md | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index cab8774f0..b0b237baa 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -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 From 2fe4e7dc61c93950bc898a6cf4099cebfc9b5db0 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 18 Nov 2024 13:00:04 +0100 Subject: [PATCH 17/22] Refine definition of "no-argument callables" --- .../improved-resolution-expected-type.md | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index b0b237baa..3426f74a3 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -102,7 +102,7 @@ fun Either.getOrElse(default: A) = when (this) { } ``` -In **expression position** we extend the scope mention in the type position with _no-argument_ callables from the static and companion object scopes of the expected type. Although the technical description appears below, the intuition is those members which would be available using `ExpectedType.` and with no additional arguments in parentheses. +In **expression position** we extend the scope mention in the type position with _properties_ defined in or extending the static and companion object scoped of the expected type. For the purposes of this KEEP, _enumeration entries_ count as properties defined in the static scope of the enumeration class. We describe the reasons for this restricted scope in the [_no-argument callables_](#no-argument-callables) section below. ```kotlin class Color(...) { @@ -110,6 +110,7 @@ class Color(...) { val WHITE: Color = ... val BLACK: Color = ... fun fromRGB(r: Int, g: Int, b: Int): Color = ... + fun background(): Color = ... } } @@ -117,13 +118,14 @@ val Color.Companion.BLUE: Color = ... // now when we match on a `color: Color`... when (color) { - WHITE -> ... // OK - fromRGB(10, 10, 10) -> ... // NO, fromRGB has arguments - BLUE -> ... // OK, extension function over companion object + WHITE -> ... // OK, member property of the companion object + fromRGB(10, 10, 10) -> ... // NO, not a property + background() -> ... // NO, not a property + BLUE -> ... // OK, extension property over companion object } ``` -**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. +**Extension** properties 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(...) { @@ -161,7 +163,7 @@ We do **not** look in the static and companion object scopes of **supertypes** o ### No-argument callables -The reason for restricting available members to no-argument ones is to avoid an _resolution explosion_ in the case of nested function calls. To understand the problem, we need to understand the sequence in which function calls are resolved and type checked, which is spelled out in the [specification](https://kotlinlang.org/spec/overload-resolution.html#overload-resolution). +The reason for restricting available members is avoiding _resolution explosion_ in the case of nested function calls. To understand the problem, we need to understand the sequence in which function calls are resolved and type checked, which is spelled out in the [Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#overload-resolution). 1. Infer the types of non-lambda arguments. 2. Choose an overload for the function based on the gathered types. @@ -169,7 +171,7 @@ The reason for restricting available members to no-argument ones is to avoid an Let us forget for a moment about lambda arguments; in that case the procedure is completely bottom-up: we move from arguments to function calls, performing resolution at each stage of the process. However, to improve the resolution we sometimes need to perform the tree walk in the other direction: from resolving the function overload we know the expected type of the argument, which we can use to resolve that expression. -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. +The problem is that if we allowed 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). @@ -185,7 +187,9 @@ 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. +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. + +Up to this point, nothing seems to be against allowing a function call without arguments, like `background()`. Alas, a function call without any explicit arguments may still have optional ones. Furthermore, forbiding _any_ function call leads to more uniformity. 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. @@ -250,13 +254,14 @@ There are some scenarios in which the expected type propagation may lead to comp * Types with variance: * Covariance, `sdet(out T) = sdet(T)`, * For contravariant arguments, `sdet(in T)` is undefined. -* Captured types: `sdet` is undefined. +* Captured types: `sdet(T)` 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, `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. + * Otherwise, `sdet(T)` is undefined. ### Additional contextual scope @@ -271,9 +276,9 @@ The contextual scope for a single definite type `T` is made from three different Furthermore, only two kinds of members are available: 1. Classifiers which are nested in and inherit from type `T`, if `T` is a `sealed` class. -2. No-argument callables, which must satisfy: - - Having no value parameters, which includes both properties and enumeration entries, - - Having no extension receiver, except for imported extension functions defined over the companion object of `T`. +2. No-argument callables, which must: + - Be either properties or enumeration entries (including properties synthetized from interoperating with other languages, like Java), + - Have no extension receiver nor context parameters, except for imported extension functions defined over the companion object of `T`. ### Changes to [overload resolution](https://kotlinlang.org/spec/overload-resolution.html#overload-resolution) From 62de9b621f9240fed8bc553687c7db918a1f8a49 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 29 Nov 2024 10:33:27 +0100 Subject: [PATCH 18/22] Clarification --- proposals/improved-resolution-expected-type.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 3426f74a3..17247b9ef 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -278,7 +278,8 @@ Furthermore, only two kinds of members are available: 1. Classifiers which are nested in and inherit from type `T`, if `T` is a `sealed` class. 2. No-argument callables, which must: - Be either properties or enumeration entries (including properties synthetized from interoperating with other languages, like Java), - - Have no extension receiver nor context parameters, except for imported extension functions defined over the companion object of `T`. + - Have no context receivers nor context parameters. + - Have no extension receiver, except for extension functions defined over the companion object of `T`. ### Changes to [overload resolution](https://kotlinlang.org/spec/overload-resolution.html#overload-resolution) @@ -289,12 +290,9 @@ In order to accomodate improved resolution for function arguments, the algorithm 3. _Choice of most specific overload_: if more than one applicable overload remains after the previous step, try to decide which is the "most specific" by applying [some rules](https://kotlinlang.org/spec/overload-resolution.html#choosing-the-most-specific-candidate-from-the-overload-candidate-set). After this step, only one overload should remain, otherwise an _ambiguity error_ is issued. 4. _Completion_: use the information from the chosen overload to resolve lambda arguments, callable references, and fix type variables. -The first change relates to _no-argument expressions_, that is, those made only from a [`simpleIdentifier`](https://kotlinlang.org/spec/syntax-and-grammar.html#grammar-rule-simpleIdentifier). +The first change relates to _no-argument expressions_, that is, those made only from a [`simpleIdentifier`](https://kotlinlang.org/spec/syntax-and-grammar.html#grammar-rule-simpleIdentifier). In that case only the _applicability_ step of the previous list apply. If during that phase no potential overloads are found, and the expression appears as argument to a function, then do not issue an error, but rather mark the expression as **delayed**. -1. The expression has no argument, so step (1) does not apply. -2. If during the _applicability_ phase no potential overloads are found, and the expression appears as argument to a function, then do not issue an error, but rather mark the expression as **delayed**. - -The second change relates to any function call, which now must handle delayed expressions as arguments. +The second change relates to any function call, which now must handle delayed expressions as arguments. The resolution algorithm is modified as follows. 1. Resolve all non-lambda arguments, where some of those may become delayed. 2. During the _applicability_ step, do not introduce information about delayed arguments inside each of the constraint systems. From 780644ac48d04d89873893c866efbcf2f701fc6e Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 29 Nov 2024 12:18:48 +0100 Subject: [PATCH 19/22] Fix --- proposals/improved-resolution-expected-type.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 17247b9ef..23ea5c806 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -279,7 +279,7 @@ Furthermore, only two kinds of members are available: 2. No-argument callables, which must: - Be either properties or enumeration entries (including properties synthetized from interoperating with other languages, like Java), - Have no context receivers nor context parameters. - - Have no extension receiver, except for extension functions defined over the companion object of `T`. + - Have no extension receiver, except for extension properties defined over the companion object of `T`. ### Changes to [overload resolution](https://kotlinlang.org/spec/overload-resolution.html#overload-resolution) From 19a794932c9c54420ce2a7bb5afea81faa31ff33 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 29 Nov 2024 13:47:54 +0100 Subject: [PATCH 20/22] Clarification --- proposals/improved-resolution-expected-type.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 23ea5c806..96e88216e 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -297,9 +297,11 @@ The second change relates to any function call, which now must handle delayed ex 1. Resolve all non-lambda arguments, where some of those may become delayed. 2. During the _applicability_ step, do not introduce information about delayed arguments inside each of the constraint systems. - **Open question**: should we filter out those candidates for which the resolution of the delayed argument with the corresponding parameter type fails? + - If the answer to the open question above is affirmative, resolution of the delayed arguments is effectively done in this stage. 3. The choice of the most specific overload remains the same. -4. During _completion_, delayed arguments are resolved again using the expected type obtained from the overload chosen in step (3). - - If the answer to open question above is affirmative, such resolution would be done as part of the applicability step. +4. During _completion_ we store information about delayed arguments. + - If the answer to the open question above is negative, completion involves resolution using the expected type obtain from the chosen overload. + - If the answer to the open question above is affirmative, resolution of delayed arguments has already been performed. ## Design decisions From aae806f8e43c19e6764a8c7ce0cf8a71601fab2f Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 11 Dec 2024 15:09:28 +0100 Subject: [PATCH 21/22] Do not filter candidates --- proposals/improved-resolution-expected-type.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index 96e88216e..d3ffad7ec 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -296,12 +296,8 @@ The second change relates to any function call, which now must handle delayed ex 1. Resolve all non-lambda arguments, where some of those may become delayed. 2. During the _applicability_ step, do not introduce information about delayed arguments inside each of the constraint systems. - - **Open question**: should we filter out those candidates for which the resolution of the delayed argument with the corresponding parameter type fails? - - If the answer to the open question above is affirmative, resolution of the delayed arguments is effectively done in this stage. 3. The choice of the most specific overload remains the same. -4. During _completion_ we store information about delayed arguments. - - If the answer to the open question above is negative, completion involves resolution using the expected type obtain from the chosen overload. - - If the answer to the open question above is affirmative, resolution of delayed arguments has already been performed. +4. During _completion_ we perform resolution of delayed arguments using the expected type obtain from the chosen overload; in addition to any other tasks in this phase. ## Design decisions @@ -359,6 +355,8 @@ The problem is that at the expression `value == UNKNOWN`, the known type of `val - It is unclear in general how to treat intersection types, since other cases may not be as simple as dropping a type argument. - If in the future Kotlin gets a feature similar to GADTs, we may piggy back on the knowledge that `T` is equal to `Problem`, and face no problem in computing the single definite expected type. +**Additional filtering of candidates**: should we filter out those candidates for which the resolution of the delayed argument with the corresponding parameter expected type fails? At this point we have decided to go with a "no". This answer has the benefit that if we ever move to "yes", the change is backward compatible, as opposed to the other direction. + ### Risks One potential risk of this proposal is the difficulty of understanding _when_ exactly it is OK to drop the qualifier, which essentially corresponds to understanding the propagation of the expected type through the compiler. On the other hand, maybe this complete understanding is not required, as developers will be able to count on the main scenarios: the conditions on a `when` with subject, the immediate expression after a `return`, or the initializer of a property, given that the return type is known. From 6715c2be18e911bd75292a6471f11dcd76805ffa Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 18 Dec 2024 15:43:47 +0100 Subject: [PATCH 22/22] Add information about limitations --- .../improved-resolution-expected-type.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/proposals/improved-resolution-expected-type.md b/proposals/improved-resolution-expected-type.md index d3ffad7ec..014815387 100644 --- a/proposals/improved-resolution-expected-type.md +++ b/proposals/improved-resolution-expected-type.md @@ -18,11 +18,13 @@ We propose an improvement of the name resolution rules of Kotlin based on the ex * [Motivating example](#motivating-example) * [What is available in the contextual scope](#what-is-available-in-the-contextual-scope) * [No-argument callables](#no-argument-callables) + * [Chained inference](#chained-inference) * [Technical details](#technical-details) * [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) + * [Interaction with inference](#interaction-with-inference) * [Design decisions](#design-decisions) * [Risks](#risks) @@ -191,8 +193,48 @@ During the resolution of the body of `hello`, we proceed arguments-first. So we Up to this point, nothing seems to be against allowing a function call without arguments, like `background()`. Alas, a function call without any explicit arguments may still have optional ones. Furthermore, forbiding _any_ function call leads to more uniformity. +We acknowledge, though, that this no-argument rule may lead to some surprising behavior. Consider the following sealed hierarchy: + +```kotlin +sealed interface Tree { + data object Leaf: Tree + data class Node(val left: Tree, val value: Int, val right: Tree): Tree +} +``` + +In this case resolution is improved for constructing `Leaf` but not for `Node`, since the latter requires arguments. + +```kotlin +fun create(n: Int): Tree = when (n) { + 0 -> Leaf + else -> Tree.Node(Leaf, n, Leaf) +} +``` + 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. +### Chained inference + +Another limitation of this proposal is that some seemingly trivial refactorings require introducing qualification. + +```kotlin +val WEIRD: Problem = UNKNOWN // improved resolution kicks in +// T.also(block: (T) -> Unit): T +val WEIRD: Problem = Problem.UNKNOWN.also { println("weird!") } +``` + +The problem arises because once we introduce `also`, the expected type from the function can no longer "flow" to the receiver position. If instead of `also` we were calling a function with type `User.(() -> Unit): Problem`, resolving `UNKNOWN` in the context of `Problem` would no longer be valid. + +_Chained inference_ is a [known problem](https://youtrack.jetbrains.com/issue/KT-17115) in Kotlin. Another place were it surfaces is requiring more type information for generic calls, + +```kotlin +fun foo(): List = listOf() +// but if we call any function on it... +fun foo(): List = listOf().reversed() +``` + +We acknowledge this limitation of the current proposal. In this case tooling can provide additional help by, for example, qualifying a name like `UNKNOWN` above when a call is auto-completed. + ## Technical details We start by defining how the expected type is actually propagated, and then describe changes to the resolution for the new contextually-scoped identifiers. @@ -299,6 +341,20 @@ The second change relates to any function call, which now must handle delayed ex 3. The choice of the most specific overload remains the same. 4. During _completion_ we perform resolution of delayed arguments using the expected type obtain from the chosen overload; in addition to any other tasks in this phase. +### Interaction with inference + +As described in the [specification](https://kotlinlang.org/spec/overload-resolution.html#type-inference-and-overload-resolution), type inference is performed after overload resolution. As a result, the expected type may not be known at the moment in which improved resolution may kick in. + +```kotlin +val brightColor: Color = WHITE + +// run(block: () -> R): R +val darkColor: Color = run { Color.BLACK } // requires qualification + +// runColor(block: () -> Color): Color +val skyColor: Color = runColor { BLUE } +``` + ## Design decisions **Priority level**: the current proposal makes this new scope have the lowest priority. In practical terms, that means that even built-ins and automatically imported declarations have higher priority. In a previous iteration, we made it have the same level as `*`-imports; but added ambiguity where currently there is not.