-
Notifications
You must be signed in to change notification settings - Fork 238
Adjust the function literal return type inference section to handle "weird" types #4673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
457a905
88a5a70
3342a86
98cc726
fa47fe0
363606f
573d24c
e7923c8
d3fc0fb
4e81da5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,12 @@ Status: Draft | |
|
|
||
| ## CHANGELOG | ||
|
|
||
| 2026.03.31 | ||
| - Change the function literal return type inference rules to handle | ||
| generator return types which are not of the form `Iterable<...>` or | ||
| `Stream<...>`. To do this, the definition of the imposed return type | ||
| schema was moved to a separate section. | ||
|
|
||
| 2024.12.17 | ||
| - Change the function literal return type inference rules to ignore | ||
| `return;` statements in generators (it doesn't actually cause null to be | ||
|
|
@@ -104,7 +110,7 @@ and which this proposal is designed to satisfy: | |
| violates this principle. | ||
| * The inference for local variables, top-level variables, and fields, should | ||
| either agree or error out. The same expression should not be inferred | ||
| differently at different syntactic positions. It’s ok for an expression to be | ||
| differently at different syntactic positions. It's ok for an expression to be | ||
| inferrable at one level but not at another. | ||
| * Obvious types should be inferred. | ||
| * Inferred and annotated types should be treated the same. | ||
|
|
@@ -260,62 +266,103 @@ Note that `late` fields are inferred exactly as non-`late` fields. However, | |
| unlike normal fields, the initializer for a `late` field may reference `this`. | ||
|
|
||
|
|
||
| ## Function literal return type inference. | ||
| ## The imposed return type schema | ||
|
|
||
| Function literals which are inferred in an empty typing context (see below) are | ||
| inferred using the declared type for all of their parameters. If a parameter | ||
| has no declared type, it is treated as if it was declared with type `dynamic`. | ||
| Inference for each returned expression in the body of the function literal is | ||
| done in an empty typing context (see below). | ||
| *This section assumes that Dart has null safety (older versions of the | ||
| language are ignored).* | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's no longer necessary to say. Those older versions are no longer supported at all. |
||
|
|
||
| Function literals which are inferred in an non-empty typing context where the | ||
| context type is a function type are inferred as described below. | ||
| In the following we refer to the asynchronous or synchronous element type | ||
| schema of a type. This is defined from the asynchronous respectively | ||
| synchronous element type of a type by treating `_` as a type. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two concerns here:
|
||
|
|
||
| Each parameter is assumed to have its declared type if present. If no type is | ||
| declared for a parameter and there is a corresponding parameter in the context | ||
| type schema with type schema `K`, the parameter is given an inferred type `T` | ||
| where `T` is derived from `K` as follows. If the greatest closure of `K` is `S` | ||
| and `S` is a subtype of `Null`, then without null safety `T` is `dynamic`, and | ||
| with null safety `T` is `Object?`. Otherwise, `T` is `S`. If there is no | ||
| corresponding parameter in the context type schema, the variable is treated as | ||
| having type `dynamic`. | ||
| Assume that _D_ is a top-level, static, or instance declaration of a | ||
| synchronous non-generator function, method, getter, setter, or constructor; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (What's the difference between a "function" and a "method"?) |
||
| or it is a local synchronous non-generator function declaration; assume | ||
| that the return type of _D_ is `R`. If `R` is `dynamic`, the _imposed | ||
| return type schema_ of _D_ is `_`. Otherwise, the imposed return type | ||
| schema of _D_ is `R`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would use a different name for different kinds of functions. Then refer to the relevant one at That reduces the risk of accidentally mixing up things, or assuming something locally that isn't true globally. Which means this section isn't necessary. Similarly for |
||
|
|
||
| The return type of the context function type is used at several points during | ||
| inference. We refer to this type as the **imposed return type | ||
| schema**. Inference for each returned or yielded expression in the body of the | ||
| function literal is done using a context type derived from the imposed return | ||
| type schema `S` as follows: | ||
| - If the function expression is neither `async` nor a generator, then the | ||
| context type is `S`. | ||
| - If the function expression is declared `async*` and `S` is of the form | ||
| `Stream<S1>` for some `S1`, then the context type is `S1`. | ||
| - If the function expression is declared `sync*` and `S` is of the form | ||
| `Iterable<S1>` for some `S1`, then the context type is `S1`. | ||
| - Otherwise, without null safety, the context type is `FutureOr<flatten(T)>` | ||
| where `T` is the imposed return type schema; with null safety, the context | ||
| type is `FutureOr<futureValueTypeSchema(S)>`. | ||
| Assume that _D_ is a top-level, static, or instance declaration of an | ||
| asynchronous non-generator function, method, or getter; or it is a local | ||
| asynchronous non-generator function declaration; assume that the return | ||
| type of _D_ is `R`. The imposed return type schema of _D_ is | ||
| `FutureOr<futureValueTypeSchema(S)>`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you mean |
||
|
|
||
| The function **futureValueTypeSchema** is defined as follows: | ||
|
|
||
| - **futureValueTypeSchema**(`S?`) = **futureValueTypeSchema**(`S`), for all `S`. | ||
| - **futureValueTypeSchema**(`S*`) = **futureValueTypeSchema**(`S`), for all `S`. | ||
| - **futureValueTypeSchema**(`Future<S>`) = `S`, for all `S`. | ||
| - **futureValueTypeSchema**(`FutureOr<S>`) = `S`, for all `S`. | ||
| - **futureValueTypeSchema**(`Future<S>`) = **eraseDynamic**(`S`), for all `S`. | ||
| - **futureValueTypeSchema**(`FutureOr<S>`) = **eraseDynamic**(`S`), for all `S`. | ||
| - **futureValueTypeSchema**(`void`) = `void`. | ||
| - **futureValueTypeSchema**(`dynamic`) = `dynamic`. | ||
| - **futureValueTypeSchema**(`dynamic`) = **eraseDynamic**(`dynamic`). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not save the reader the trouble of evaluating eraseDynamic(
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why make this change? If it makes any difference whether the future-value-type-schema is |
||
| - **futureValueTypeSchema**(`_`) = `_`. | ||
| - Otherwise, for all `S`, **futureValueTypeSchema**(`S`) = `Object?`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not make that But in any case, I still don't see why we want to introduce |
||
|
|
||
| _Note that it is a compile-time error unless the return type of an asynchronous | ||
| non-generator function is a supertype of `Future<Never>`, which means that | ||
| the last case will only be applied when `S` is `Object` or a top type._ | ||
|
|
||
| The helper function **eraseDynamic** is defined as follows: | ||
|
|
||
| - **eraseDynamic**(`dynamic`) = `_`. | ||
| - Otherwise, for all `S`, **eraseDynamic**(`S`) = `S`. | ||
|
|
||
| Assume that _D_ is a top-level, static, or instance declaration of a | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The order is a bit confusing here, because we've interrupted the list of cases that define imposed return type schema in order to define futureValueTypeSchema and eraseDynamic, and now we're returning to add more cases to imposed return type schema. I'd recommend defining each term in its own section, with bullets for cases, e.g.: Assume that D is a top-level, static, or instance declaration of a function, method, getter, setter, or constructor; or it is a local function declaration or a function literal. Then the imposed return type schema of D is defined as follows:
The function futureValueTypeSchema is defined as follows: ... The helper function eraseDynamic is defined as follows: ...
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Also shows the incongruence going from "synchronous element type", which is very precise and understandable, to "imposed return type", which has nothing to do with returns.) |
||
| synchronous respectively asynchronous generator function, method, or | ||
| getter; or it is a local synchronous respectively asynchronous generator | ||
| function declaration; assume that the return type of _D_ is `R`. | ||
|
|
||
| Let `S` be the synchronous respectively asynchronous element type of `R`. | ||
| If `S` is `dynamic`, the imposed return type schema of _D_ is `_`. | ||
| Otherwise, the imposed return type schema of _D_ is `S`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is that not just eraseDynamic( |
||
|
|
||
| Assume that _D_ is a non-generator function literal which is being inferred | ||
| with context type schema `T`. | ||
|
|
||
| If `T` is not a function type schema, or `T` is a function type schema with | ||
| return type `dynamic`, the imposed return type schema of _D_ is `_`. | ||
| Otherwise, let `S` be the return type schema of `T`. *Note that `S` may | ||
| contain references to type variables declared by _D_ itself, which is not a | ||
| problem.* In this case the imposed return type schema of _D_ is `S`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The paragraph above doesn't say anything about asynchronous vs synchronous, so I'm assuming that this paragraph is intended to apply to both asynchronous and synchronous non-generator function literals. In the case of asynchronous non-generator function literals, shouldn't the imposed return type schema be I've addressed this in my comment on line 310. |
||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any definition of imposed return type schema for a generator function literal. I've addressed this in my comment on line 310. |
||
| Any imposed return type schema can be designated as the _imposed return | ||
| type_ in a situation where it is a type. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can be ... will it? And what if it isn't? |
||
|
|
||
|
|
||
| ## Function literal return type inference | ||
|
|
||
| Function literals which are inferred in an empty typing context (see below) | ||
| are inferred using the declared type for all of their parameters. If a | ||
| parameter has no declared type, it is treated as if it was declared with | ||
| type `dynamic`. Inference for each returned or yielded expression in the | ||
| body of the function literal is done in an empty typing context (see | ||
| below). | ||
|
|
||
| Function literals which are inferred in a non-empty typing context where the | ||
| context type is not a function type are inferred in the empty typing context. | ||
|
|
||
| Function literals which are inferred in an non-empty typing context where the | ||
| context type is a function type are inferred as described below. | ||
|
|
||
| Each parameter is assumed to have its declared type if present. If no type is | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "if present" -> "if it has a declared type". It's not clear whether it's the declared type or the parameter which must be present. |
||
| declared for a parameter and there is a corresponding parameter in the context | ||
| type schema with type schema `K`, the parameter is given an inferred type `T` | ||
| where `T` is derived from `K` as follows. If the greatest closure of `K` is `S` | ||
| and `S` is a subtype of `Null`, then without null safety `T` is `dynamic`, and | ||
| with null safety `T` is `Object?`. Otherwise, `T` is `S`. If there is no | ||
| corresponding parameter in the context type schema, the variable is treated as | ||
| having type `dynamic`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the function is generic, is that covered? |
||
|
|
||
| Inference for each returned or yielded expression in the body of the | ||
| function literal is done using the imposed return type schema of the | ||
| function literal. | ||
|
|
||
| In order to infer the return type of a function literal, we first infer the | ||
| **actual returned type** of the function literal. | ||
|
|
||
| The actual returned type of a function literal with an expression body is the | ||
| inferred type of the expression body, using the local type inference algorithm | ||
| described below with a typing context as computed above. | ||
| described below with a context type schema `K` as computed above. | ||
|
|
||
| The actual returned type of a function literal with a block body is computed as | ||
| follows. Let `T` be `Never` if every control path through the block exits the | ||
|
|
@@ -353,8 +400,8 @@ body. | |
| The **actual returned type** of the function literal is the value of `T` after | ||
| all `return` and `yield` statements in the block body have been considered. | ||
|
|
||
| Let `T` be the **actual returned type** of a function literal as computed above. | ||
| Let `R` be the greatest closure of the typing context `K` as computed above. | ||
| Let `T` be the **actual returned type** of a function literal as computed | ||
| above. Let `R` be the greatest closure of the context type schema `K`. | ||
|
|
||
| With null safety: if `R` is `void`, or the function literal is marked `async` | ||
| and `R` is `FutureOr<void>`, let `S` be `void` (without null-safety: no special | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.