From 42d33bb33ba520f78b64830b6ed014559534543c Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Fri, 6 Oct 2023 19:04:42 +0200 Subject: [PATCH 1/4] Add updated inference rules for `num` types. No changes intended, but the existing rules are phrased based on assumptions that are no longer true with extension types. Adds more restrictions on when the rules apply, to rule out things that previously couldn't happen. --- .../extension-types/feature-specification.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/accepted/future-releases/extension-types/feature-specification.md b/accepted/future-releases/extension-types/feature-specification.md index 0ef63a337b..d2163f7196 100644 --- a/accepted/future-releases/extension-types/feature-specification.md +++ b/accepted/future-releases/extension-types/feature-specification.md @@ -15,6 +15,10 @@ information about the process, including in their change logs. [1]: https://github.com/dart-lang/language/blob/master/working/1426-extension-types/feature-specification-views.md [2]: https://github.com/dart-lang/language/blob/master/working/extension_structs/overview.md +2023.10.06 + - Added updated special-case inference rules for number types, + which are extension-type aware. + 2023.09.11 - Add missing rule about getter/setter signature correspondence. @@ -602,6 +606,85 @@ If _Dm_ is a method with function type `F`, and `args` exists, the static analysis of the extension type member invocation is the same as that of an invocation with argument part `args` of a function with the given type. +#### Static analysis of extension member invocations on numbers + +The specification have special rules for some operations on numbers, +a hard-coded set of dependent typing rules that makes it much more +convenient to work with `num` subtypes. + +Those rules are updated, because extension types break two assumptions +that the existing rules were written under: +* Invoking a member named, for example, `+` on a non-bottom subtype of `int` + always invokes the known and trusted `int` member, + which always has the same signature. +* There is no non-bottom type which is a subtype of both `int` and `double`. + +Extension type methods should not be special-cased, +the rules changes are to *avoid* that happening. + +The rules are changed to the following phrasing: + +> **Binary operators**: +> Let `e` be an expression of one of the forms `e1 + e2`, `e1 - e2`, `e1 * e2`, +> `e1 % e2` or `e1.remainder(e2)`, where the static type of `e1` is a +> non-bottom type *T* with *T* <: `num`, and the corresponding operator or +> method of *T* is not an extension type member, and has a function signature +> of `num Function(num)`. +> +> Let *C* be the context type of `e`. Then: +> * If `int` \<: *C*, not `double` \<: *C*, +> *T* \<: `int`, and not *T* \<: `double`, +> then the context type of `e2` is `int`. +> * If `double` \<: *C*, not `int` \<: *C*, and not *T* \<: `double`, +> then the context type of `e2` is `double`. +> * Otherwise, the context type of `e2` is `num`. +> +> Let *S* be the static type of `e2`. If *S* is assignable to `num`, then: +> * If *T* \<: `double` and not *T* \<: `int` then the static type +> of `e` is `double`. +> _This includes *S* being `dynamic` or `Never`._ +> * If *T* \<: `int`, not *T* \<: `double`, +> *S* \<: `int`, and not *S* \<: `double`, +> then the static type of `e` is `int`. +> * Otherwise, if *S* \<: `double` and not *S* \<:`int`, +> then the static type of `e` is `double`. +> * Otherwise the static type of *e* is `num`. +> +> **Clamp**: +> Let `e` be a normal invocation of the form `e1.clamp(e2, e3)`, +> where the static type of `e1` is *T*1, +> *T*1 is a non-bottom subtype of `num`, +> *T*1.clamp is not an extension type member, +> and has a method signature of `num Function(num, num)`. +> +> Let *C* be the context type of `e`. Then: +> * If `int` \<: *C*, not `double` \<: *C*, *T*1 \<: `int`, +> and not *T*1 \<: `double`, +> then the context type of both `e2` and `e3` is `int`. +> * If `double` \<: *C*, not `int` \<: *C*, *T*1 \<: `double` +> and not *T*1 \<: `int`, +> then the context type of both `e2` and `e3` is `double`. +> * Otherwise the context type of `e2` and `e3` is `num`. +> +> Let *T*2 and *T*3 be the static types of `e2` and +> `e3` respectively. If *T*2 and *T*3 are both +> non-bottom subtypes of `num`, then: +> * If *T*1, *T*2 and *T*3 are all +> subtypes of `int`, and are not subtypes of `double`, +> the static type of `e` is `int`. +> * If *T*1, *T*2 and *T*3 are all +> subtypes of `double`, and are not subtypes of `int`, +> the static type of `e` is `double`. +> * Otherwise the static type of `e` is `num`. + +_These changes preserve the current behavior of all existing code, +because the new restrictions are only excluding cases that couldn't +happen before extension types. +An extension type which subtypes both `int` and `double` gets no promotion, +nor does one which subtypes, for example, `int` and another interface in a way +which changes the signature of the relevant members in the combined interface. +Such extension types would necessarily have `Never` as representation type, +so they are not be useful for anything except complicating type inference._ ### Dynamic Semantics of an Extension Type Member Invocation From c281e6f68137cbf1b97206c87b8b34ab2f5d6cd1 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Sat, 7 Oct 2023 12:33:18 +0200 Subject: [PATCH 2/4] Tweak phrasing. Add `dynamic`-awareness. --- .../extension-types/feature-specification.md | 66 +++++++++++-------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/accepted/future-releases/extension-types/feature-specification.md b/accepted/future-releases/extension-types/feature-specification.md index d2163f7196..fd989ff70d 100644 --- a/accepted/future-releases/extension-types/feature-specification.md +++ b/accepted/future-releases/extension-types/feature-specification.md @@ -626,25 +626,31 @@ The rules are changed to the following phrasing: > **Binary operators**: > Let `e` be an expression of one of the forms `e1 + e2`, `e1 - e2`, `e1 * e2`, -> `e1 % e2` or `e1.remainder(e2)`, where the static type of `e1` is a -> non-bottom type *T* with *T* <: `num`, and the corresponding operator or -> method of *T* is not an extension type member, and has a function signature -> of `num Function(num)`. +> or `e1 % e2`, or a normal invocation of the form `e1.remainder(e2)`, +> where the static type of `e1` is a type *T*, +> and *C* is the greatest closure of the context type scheme of `e`. +> +> If the corresponding member of *T* is not an extension type member, +> and: +> * either *T* \<: `num`, not *T* \<: `double`, and the function signature +> of the corresponding member of *T* is `num Function(num)`, +> * or *T* \<: `double`, not *T* \<: `int`, and the function signature +> of the corresponding member of *T* is `double Function(num)`, > -> Let *C* be the context type of `e`. Then: -> * If `int` \<: *C*, not `double` \<: *C*, -> *T* \<: `int`, and not *T* \<: `double`, +> then: +> +> * If `int` \<: *C*, not `double` \<: *C*, and *T* \<: `int` > then the context type of `e2` is `int`. > * If `double` \<: *C*, not `int` \<: *C*, and not *T* \<: `double`, > then the context type of `e2` is `double`. > * Otherwise, the context type of `e2` is `num`. > -> Let *S* be the static type of `e2`. If *S* is assignable to `num`, then: -> * If *T* \<: `double` and not *T* \<: `int` then the static type -> of `e` is `double`. +> Let *S* be the static type of `e2`. +> If *S* is the type `dynamic`, instead let *S* be the context type of `e2`. +> If *S* \<: `num`, then: +> * If *T* \<: `double` then the static type of `e` is `double`. > _This includes *S* being `dynamic` or `Never`._ -> * If *T* \<: `int`, not *T* \<: `double`, -> *S* \<: `int`, and not *S* \<: `double`, +> * If *T* \<: `int`, *S* \<: `int`, and not *S* \<: `double`, > then the static type of `e` is `int`. > * Otherwise, if *S* \<: `double` and not *S* \<:`int`, > then the static type of `e` is `double`. @@ -653,38 +659,42 @@ The rules are changed to the following phrasing: > **Clamp**: > Let `e` be a normal invocation of the form `e1.clamp(e2, e3)`, > where the static type of `e1` is *T*1, -> *T*1 is a non-bottom subtype of `num`, +> and *C* is the greatest closure of the context type scheme of `e`. +> +> If *T*1 \<: `num`, and not both +> *T*1 \<: `int` and *T*1 \<: `double`, > *T*1.clamp is not an extension type member, -> and has a method signature of `num Function(num, num)`. +> and its function signature is `num Function(num, num)`, then: > -> Let *C* be the context type of `e`. Then: -> * If `int` \<: *C*, not `double` \<: *C*, *T*1 \<: `int`, -> and not *T*1 \<: `double`, +> * If `int` \<: *C*, not `double` \<: *C*, and *T*1 \<: `int`, > then the context type of both `e2` and `e3` is `int`. -> * If `double` \<: *C*, not `int` \<: *C*, *T*1 \<: `double` -> and not *T*1 \<: `int`, +> * If `double` \<: *C*, not `int` \<: *C*, *T*1 \<: `double`, > then the context type of both `e2` and `e3` is `double`. > * Otherwise the context type of `e2` and `e3` is `num`. > > Let *T*2 and *T*3 be the static types of `e2` and -> `e3` respectively. If *T*2 and *T*3 are both -> non-bottom subtypes of `num`, then: +> `e3` respectively. +> If any of *T*2 and *T*3 are the type `dynamic`, +> instead let those types be the context type of `e2` and `e3` respectively. +> If *T*2 and *T*3 are both +> subtypes of `num`, and neither is both a subtype of `int` and `double`, then: > * If *T*1, *T*2 and *T*3 are all -> subtypes of `int`, and are not subtypes of `double`, -> the static type of `e` is `int`. +> subtypes of `int`, then the static type of `e` is `int`. > * If *T*1, *T*2 and *T*3 are all -> subtypes of `double`, and are not subtypes of `int`, -> the static type of `e` is `double`. +> subtypes of `double`, then the static type of `e` is `double`. > * Otherwise the static type of `e` is `num`. _These changes preserve the current behavior of all existing code, because the new restrictions are only excluding cases that couldn't -happen before extension types. -An extension type which subtypes both `int` and `double` gets no promotion, +exist before extension types._ +_An extension type which subtypes both `int` and `double` gets no promotion, nor does one which subtypes, for example, `int` and another interface in a way which changes the signature of the relevant members in the combined interface. Such extension types would necessarily have `Never` as representation type, -so they are not be useful for anything except complicating type inference._ +so they are not useful for anything except complicating type inference._ +_The changed rules are also `dynamic`-aware, in that they apply the downcast +from `dynamic` to the context type that the rules themselves have ensured, +before determining the result type._ ### Dynamic Semantics of an Extension Type Member Invocation From f66279a4850dd5d90bad4bb8a599ec91ebe175b4 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Sat, 7 Oct 2023 12:34:43 +0200 Subject: [PATCH 3/4] Remove dynamic-awareness. It's a change, and it may require inserting a new dynamic downcast, which the current semantics does not. (We should decide whether we want this to act like real "overloading", and if so, how to resolve, or if we should just have rules that are "good enough" for most common use-cases.) --- .../extension-types/feature-specification.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/accepted/future-releases/extension-types/feature-specification.md b/accepted/future-releases/extension-types/feature-specification.md index fd989ff70d..2c96e626c8 100644 --- a/accepted/future-releases/extension-types/feature-specification.md +++ b/accepted/future-releases/extension-types/feature-specification.md @@ -646,8 +646,7 @@ The rules are changed to the following phrasing: > * Otherwise, the context type of `e2` is `num`. > > Let *S* be the static type of `e2`. -> If *S* is the type `dynamic`, instead let *S* be the context type of `e2`. -> If *S* \<: `num`, then: +> If *S* is assignable to `num`, then: > * If *T* \<: `double` then the static type of `e` is `double`. > _This includes *S* being `dynamic` or `Never`._ > * If *T* \<: `int`, *S* \<: `int`, and not *S* \<: `double`, @@ -674,10 +673,9 @@ The rules are changed to the following phrasing: > > Let *T*2 and *T*3 be the static types of `e2` and > `e3` respectively. -> If any of *T*2 and *T*3 are the type `dynamic`, -> instead let those types be the context type of `e2` and `e3` respectively. > If *T*2 and *T*3 are both -> subtypes of `num`, and neither is both a subtype of `int` and `double`, then: +> assignable of `num`, and neither is both a subtype of `int` and `double`, +> then: > * If *T*1, *T*2 and *T*3 are all > subtypes of `int`, then the static type of `e` is `int`. > * If *T*1, *T*2 and *T*3 are all @@ -692,9 +690,6 @@ nor does one which subtypes, for example, `int` and another interface in a way which changes the signature of the relevant members in the combined interface. Such extension types would necessarily have `Never` as representation type, so they are not useful for anything except complicating type inference._ -_The changed rules are also `dynamic`-aware, in that they apply the downcast -from `dynamic` to the context type that the rules themselves have ensured, -before determining the result type._ ### Dynamic Semantics of an Extension Type Member Invocation From cfc6ddb006b5d27d917af49d13c875a772582777 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 9 Oct 2023 13:09:38 +0200 Subject: [PATCH 4/4] Improve phrasing to say what happens if special case doesn't apply. --- .../future-releases/extension-types/feature-specification.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/accepted/future-releases/extension-types/feature-specification.md b/accepted/future-releases/extension-types/feature-specification.md index 2c96e626c8..a5c928e687 100644 --- a/accepted/future-releases/extension-types/feature-specification.md +++ b/accepted/future-releases/extension-types/feature-specification.md @@ -637,7 +637,7 @@ The rules are changed to the following phrasing: > * or *T* \<: `double`, not *T* \<: `int`, and the function signature > of the corresponding member of *T* is `double Function(num)`, > -> then: +> then use the following rules for type inference instead of the default rules: > > * If `int` \<: *C*, not `double` \<: *C*, and *T* \<: `int` > then the context type of `e2` is `int`. @@ -663,7 +663,8 @@ The rules are changed to the following phrasing: > If *T*1 \<: `num`, and not both > *T*1 \<: `int` and *T*1 \<: `double`, > *T*1.clamp is not an extension type member, -> and its function signature is `num Function(num, num)`, then: +> and its function signature is `num Function(num, num)`, then +> use the following rules for type inference instead of the default rules: > > * If `int` \<: *C*, not `double` \<: *C*, and *T*1 \<: `int`, > then the context type of both `e2` and `e3` is `int`.