Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 85 additions & 38 deletions resources/type-system/inference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment thread
eernstg marked this conversation as resolved.
inferrable at one level but not at another.
* Obvious types should be inferred.
* Inferred and annotated types should be treated the same.
Expand Down Expand Up @@ -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).*

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.


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.

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.


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;

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.


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

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


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

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.

- **futureValueTypeSchema**(`_`) = `_`.
- Otherwise, for all `S`, **futureValueTypeSchema**(`S`) = `Object?`.

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 make that _?
I'd argue that dynamic foo() async { ... } is more suggestive that the user actually want dynamic semantics, and Object foo() async {...} feels more like they don't care, and _ would be more relevant.

But in any case, I still don't see why we want to introduce _ at all.


_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

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

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

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


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

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.


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.

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

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.

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

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?


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
Expand Down Expand Up @@ -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
Expand Down
53 changes: 37 additions & 16 deletions specification/dartLangSpec.tex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
% - Remove the getter/setter signature mismatch error. It is no longer required
% that the getter return type and setter parameter type are related in any way.
%
% Mar 2026
% - Define the (a)synchronous element type of a type (so we can use that
% concept in feature specifications).
%
% Jan 2026
% - Specify that collection elements are allowed to have type `void` in a
% collection literal where the corresponding actual type argument is `void`.
Expand Down Expand Up @@ -2172,27 +2176,44 @@ \section{Functions}
}

\LMHash{}%
We define the
\IndexCustom{element type of a generator function}{%
function!generator!element type}
$f$ as follows:
%
Let $S$ be the union-free type derived from the declared return type of $f$.
We define two variants of the element type of a type $T$:
Let $S$ be the union-free type derived from $T$.
%
If $f$ is a synchronous generator and
$S$ implements \code{Iterable<$U$>} for some $U$
If $S$ implements \code{Iterable<$U$>} for some $U$
(\ref{interfaceSuperinterfaces})
then the element type of $f$ is $U$.
then the
\IndexCustom{synchronous element type}{%
type!synchronous element type}
of $T$ is $U$.
Similarly, if $S$ implements \code{Stream<$U$>} for some $U$
then the
\IndexCustom{asynchronous element type}{%
type!asynchronous element type}
of $T$ is $U$.
%
If $f$ is an asynchronous generator and
$S$ implements \code{Stream<$U$>} for some $U$
Otherwise, if $S$ is a supertype of \code{Object}
\commentary{(which includes \code{Object} itself)}
then the synchronous as well as the asynchronous
element type of $T$ is \DYNAMIC.
Otherwise, both kinds of element type is undefined.

\LMHash{}%
The
\IndexCustom{element type of a generator function}{%
function!generator!element type}
$f$ is defined as follows:
%
If $f$ is a synchronous generator function with return type $T$,
and the synchronous element type of $T$ is $U$,
then the element type of $f$ is $U$.
%
Otherwise, if $f$ is a generator (synchronous or asynchronous)
and $S$ is a supertype of \code{Object}
\commentary{(which includes \code{Object} itself)}
then the element type of $f$ is \DYNAMIC.
\commentary{No further cases are possible.}
Similarly, if $f$ is an asynchronous generator function with return type $T$,
and the asynchronous element type of $T$ is $U$,
then the element type of $f$ is $U$.
\commentary{%
The element type of a generator function declaration which is not
a compile-time error is always defined.%
}


\subsection{Function Declarations}
Expand Down