-
Notifications
You must be signed in to change notification settings - Fork 239
Adjust the function literal return type inference section to handle "weird" types - #4673
Adjust the function literal return type inference section to handle "weird" types #4673eernstg wants to merge 10 commits into
Conversation
@stereotype441
stereotype441
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the direction this is heading. In addition to my comments below, there's one other thing that I would want to address before landing this. Since this is a potentially breaking change, it would be nice to test it out in google3 and go through the breaking change process before landing it. Once we agree on what spec text we're aiming for, I'd be happy to do a prototype implementation and run it through Google3 to make sure it doesn't cause unexpected breakages.
eernstg
commented
Apr 1, 2026
Made various adjustments, PTAL!
For readers who haven't followed the process in detail: Please note that this PR introduces some breaking changes, and it will not be landed unless and until a corresponding breaking change process has been conducted to the extent needed.
This covers a test case that I forgot to include in https://dart-review.googlesource.com/c/sdk/+/491706. As with that CL, this test is solely intended to ensure that the current behavior is adequately tested, so that we will avoid inadvertently changing it while doing refactors. Changes to the specification are being contemplated; see dart-lang/language#4673 and dart-lang/language#4672. Change-Id: I5a21bfc2555857542d0dc02b757672a36a6a6964 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/492660 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Erik Ernst <eernst@google.com>
@stereotype441
stereotype441
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning to this after a long time away (sorry!)
I have some suggestions about how to re-order things, and there are some cases which are not covered.
Once we get that taken care of, I still want to test the change in google3 to verify that it's not too breaking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you mean FutureOr<futureValueTypeSchema(R)>.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not save the reader the trouble of evaluating eraseDynamic(dynamic) and just say this?
- futureValueTypeSchema(
dynamic) =_.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 _ or dynamic, then actually writing dynamic as the return type may suggest that the user wanted the dynamic type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 FutureOr<futureValueTypeSchema(S)>?
I've addressed this in my comment on line 310.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two concerns here:
- The definition of synchronous/asynchronous element type behaves differently depending on whether the type in question is a supertype of
Object. When extending this definition to schemas, should we treat_as a type that extendsObjector a type that doesn't extendObject? - Synchronous/asynchronous element type of
Tis undefined in the case whereTneither implementsIterable<S>(respectivelyStream<S>) for someS, nor is a supertype ofObject. In your edit to the spec, when you use asynchronous/synchronous element type to define element type of a generator function, you've explained that the undefined case only occurs if there's a compile-time error. But I believe that during type inference of a function literal, that undefined case is possible to reach (because there are situations where it's ok for the static type of an expression to fail to satisfy the expression's schema). So we need to define how we handle that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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:
- Define
Ras follows:- If D is a function literal which is being inferred with context type schema
T:- If
Tis a function type schema, letRbe the return type ofT. - Otherwise let
Rbe_.
- If
- Otherwise, let
Rbe the return type of D. Note thatRmay contain references to type variables declared by D itself, which is not a problem.
- If D is a function literal which is being inferred with context type schema
- If D is a synchronous non-generator, the imposed return type schema of D is eraseDynamic(
R). - If D is an asynchronous non-generator, the imposed return type schema of D is
FutureOr<futureValueTypeSchema(R)>. - If D is a synchronous generator, the imposed return type schema of D is eraseDynamic(
S), whereSis the synchronous element type ofR. - If D is an asynchronous generator, the imposed return type schema of D is eraseDynamic(
S), whereSis the asynchronous element type ofR.
The function futureValueTypeSchema is defined as follows:
...
The helper function eraseDynamic is defined as follows:
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(What's the difference between a "function" and a "method"?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use a different name for different kinds of functions.
Instead of "imposed return type scheme", I'd have "return type" for a sync-non-generator, "future value type" for an async-non-generator, "iterable element type" for a sync-generator and "stream element type" for an async-generator.
Then refer to the relevant one at return or yield[*] statements.
That reduces the risk of accidentally mixing up things, or assuming something locally that isn't true globally.
And it avoids calling the yield-type a "return type", which it most certianly isn't.
(It's derived from a return type, but it's an element type.)
Which means this section isn't necessary.
At a return statement, "if the current function is not an async function, the context type is the return type. if the current function is an async function, the context type is FutureOr<T> where T is the future value type of the return type of the current function."
Similarly for yields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 _ or dynamic, then actually writing dynamic as the return type may suggest that the user wanted the dynamic type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is that not just eraseDynamic(S)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be ... will it? And what if it isn't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the function is generic, is that covered?
...weird" return types
d718e8b to
2f93210
Compare
Some background discussion can be found in #4672.
Consider the following example:
The rules in
inference.mddo not have a matching case for the computation of the context type forebecause the imposed return type schema isdynamic, which is a type that isn't of the formIterable<S1>for any typeS1. Given that thesync*case doesn't match, we proceed to the last case 'Otherwise, ...', but in this case the context type schema is specified asFutureOr<futureValueTypeSchema(S)>, which is clearly only intended to be used when the function body is markedasync.This PR inserts a couple of sanity checks such that we will recognize return types that are not of the form
Iterable<...>orStream<...>for generator functions, and we will only use theFutureOr<futureValueTypeSchema(S)>rule when the function is markedasync, and we will use the context type schema_when the imposed return type schema is completely off track (e.g., when async*function literal has context type ofint Function()even though it could never return anint). The new wording relies on the notion of 'the element type' of a generator function, which is specified in the language specification.