Description
The analyzer and front end disagree on how to convert the context of a function expression into the context for the operands of return
and yield
inside the function expression, and they both disagree with the spec.
Paraphrasing from here, and ignoring legacy logic, the spec says to do this:
- Let
K
be the context of the function expression. - If the function is
sync
(not a generator and not asynchronous), then the context for operands ofreturn
in the function expression isK
. - If the function is
async*
andK
isStream<S>
for someS
, then the context for operands ofyield
in the function expression isS
. - Otherwise, if the function is
sync*
andK
isIterable<S>
for someS
, then the context for operands ofyield
in the function expression isS
. - Otherwise, the context for operands of
return
/yield
in the function expression isFutureOr<futureValueTypeSchema(K)>
(wherefutureValueTypeSchema
is defined here).
The analyzer behavior differs from the spec in the following ways:
- If
K
is_
ordynamic
, then the context for operands ofreturn
andyield
in the function expression is_
, regardless of the function expression's async/generator marker. - Matching of
Stream
andIterable
is done by ignoring trailing?
s, replacing type parameters with their bounds, and then using "as instance of" semantics (e.g. ifK
isT&MyStream?
, whereMyStream
extendsStream<int>
, then that produces the same result thatK=Stream<int>
would).
The front end behavior differs from the spec in the following ways:
- Matching of
Stream
andIterable
is done using "union free" semantics (ignoring trailing?
s and unwrappingFutureOr<S>
toS
), but otherwise requiring a precise match (e.g. ifK
isFutureOr<Stream<int>?>?
, then that produces the same result thatK=Stream<int>
would, but ifK
isMyStream
, whereMyStream
extendsStream<int>
, then that is considered not to match). - If the function is
async*
orsync*
andK
doesn't matchStream
(or, respectively,Iterable
), then the context for operands ofyield
in the function expression is_
(notFutureOr<futureValueTypeSchema(K)>
). - If the function is
async
, then the context for operands ofreturn
in the function expression iswrapFutureOr(futureValueTypeSchema(K))
, wherewrapFutureOr
is defined as follows:wrapFutureOr(FutureOr<S>?) = FutureOr<S>?
wrapFutureOr(FutureOr<S>) = FutureOr<S>
- Otherwise,
wrapFutureOr(S) = FutureOr(S)
That's a lot of behavioral differences! We should pick a behavior to standardize on, and update spec, CFE, and analyzer to all match.
My gut feeling is that the behavior we want is probably a mixture of all three. Perhaps something like this:
- Let
K
be the context of the function expression. - If the function is
async*
:- If
unionFree(K)
isStream<S>
for someS
, then the context for operands ofyield
in the function expression isS
. Otherwise, it's_
. - Where
unionFree
is defined as follows:unionFree(S?) = unionFree(S)
unionFree(FutureOr<S>) = unionFree(S)
- Otherwise,
unionFree(S) = S
.
- If
- If the function is
sync*
:- If
unionFree(K)
isIterable<S>
for someS
, then the context for operands ofyield
in the function expression isS
. Otherwise, it's_
.
- If
- If the function is
async
:- Let
S
befutureValueTypeSchema(K)
. - If
S
is_
ordynamic
, then the context for operands ofreturn
in the function expression is_
. - Otherwise, it's
FutureOr<S>
.
- Let
But I think that before deciding for sure, it would be worth doing some investigation to see how breaking this would be.
@dart-lang/language-team any thoughts?