Skip to content

Navigation Menu

Sign in
Sign up

Adjust the function literal return type inference section to handle "weird" types - #4673

Open
eernstg wants to merge 10 commits into
main from
inference_fix_4672_mar26
Open

Adjust the function literal return type inference section to handle "weird" types #4673
eernstg wants to merge 10 commits into
main from
inference_fix_4672_mar26

Conversation

@eernstg

@eernstg eernstg commented Mar 31, 2026

Copy link
Copy Markdown
Member

Some background discussion can be found in #4672.

Consider the following example:

void main() {
 dynamic Function() f = () sync* { yield e; };
}

The rules in inference.md do not have a matching case for the computation of the context type for e because the imposed return type schema is dynamic, which is a type that isn't of the form Iterable<S1> for any type S1. Given that the sync* case doesn't match, we proceed to the last case 'Otherwise, ...', but in this case the context type schema is specified as FutureOr<futureValueTypeSchema(S)>, which is clearly only intended to be used when the function body is marked async.

This PR inserts a couple of sanity checks such that we will recognize return types that are not of the form Iterable<...> or Stream<...> for generator functions, and we will only use the FutureOr<futureValueTypeSchema(S)> rule when the function is marked async, and we will use the context type schema _ when the imposed return type schema is completely off track (e.g., when a sync* function literal has context type of int Function() even though it could never return an int). The new wording relies on the notion of 'the element type' of a generator function, which is specified in the language specification.

@stereotype441 stereotype441 left a comment

Copy link
Copy Markdown
Member

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.

leafpetersen reacted with thumbs up emoji
Comment thread resources/type-system/inference.md Outdated
Comment thread resources/type-system/inference.md Outdated

eernstg commented Apr 1, 2026

Copy link
Copy Markdown
Member Author

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.

Comment thread resources/type-system/inference.md
copybara-service Bot pushed a commit to dart-lang/sdk that referenced this pull request Apr 9, 2026
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

Copy link
Copy Markdown
Member

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.

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)>`.

@stereotype441 stereotype441 May 1, 2026

Copy link
Copy Markdown
Member

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)>.

- **futureValueTypeSchema**(`FutureOr<S>`) = **eraseDynamic**(`S`), for all `S`.
- **futureValueTypeSchema**(`void`) = `void`.
- **futureValueTypeSchema**(`dynamic`) = `dynamic`.
- **futureValueTypeSchema**(`dynamic`) = **eraseDynamic**(`dynamic`).

@stereotype441 stereotype441 May 1, 2026

Copy link
Copy Markdown
Member

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) = _.

Copy link
Copy Markdown
Member

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.

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`.

@stereotype441 stereotype441 May 1, 2026

Copy link
Copy Markdown
Member

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.

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`.

@stereotype441 stereotype441 May 1, 2026

Copy link
Copy Markdown
Member

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.

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.

@stereotype441 stereotype441 May 1, 2026

Copy link
Copy Markdown
Member

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 extends Object or a type that doesn't extend Object?
  • Synchronous/asynchronous element type of T is undefined in the case where T neither implements Iterable<S> (respectively Stream<S>) for some S, nor is a supertype of Object. 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.

- **eraseDynamic**(`dynamic`) = `_`.
- Otherwise, for all `S`, **eraseDynamic**(`S`) = `S`.

Assume that _D_ is a top-level, static, or instance declaration of a

@stereotype441 stereotype441 May 1, 2026

Copy link
Copy Markdown
Member

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 R as follows:
    • If D is a function literal which is being inferred with context type schema T:
      • If T is a function type schema, let R be the return type of T.
      • Otherwise let R be _.
    • Otherwise, let R be the return type of D. Note that R may contain references to type variables declared by D itself, which is not a problem.
  • 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), where S is the synchronous element type of R.
  • If D is an asynchronous generator, the imposed return type schema of D is eraseDynamic(S), where S is the asynchronous element type of R.

The function futureValueTypeSchema is defined as follows:

...

The helper function eraseDynamic is defined as follows:

...

Copy link
Copy Markdown
Member

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.)

Comment thread resources/type-system/inference.md
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).*

Copy link
Copy Markdown
Member

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.

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;

Copy link
Copy Markdown
Member

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"?)

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`.

Copy link
Copy Markdown
Member

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.

- **futureValueTypeSchema**(`FutureOr<S>`) = **eraseDynamic**(`S`), for all `S`.
- **futureValueTypeSchema**(`void`) = `void`.
- **futureValueTypeSchema**(`dynamic`) = `dynamic`.
- **futureValueTypeSchema**(`dynamic`) = **eraseDynamic**(`dynamic`).

Copy link
Copy Markdown
Member

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.

- **eraseDynamic**(`dynamic`) = `_`.
- Otherwise, for all `S`, **eraseDynamic**(`S`) = `S`.

Assume that _D_ is a top-level, static, or instance declaration of a

Copy link
Copy Markdown
Member

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.)


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`.

Copy link
Copy Markdown
Member

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)?

problem.* In this case the imposed return type schema of _D_ is `S`.

Any imposed return type schema can be designated as the _imposed return
type_ in a situation where it is a type.

Copy link
Copy Markdown
Member

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?

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

Copy link
Copy Markdown
Member

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.

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`.

Copy link
Copy Markdown
Member

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?

eernstg force-pushed the inference_fix_4672_mar26 branch from d718e8b to 2f93210 Compare July 17, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@stereotype441 stereotype441 stereotype441 left review comments
@lrhn lrhn lrhn left review comments
@leafpetersen leafpetersen Awaiting requested review from leafpetersen

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /