Skip to content

Spec and implementations disagree about context for return/yield in function expressions #3664

Open
@stereotype441

Description

@stereotype441

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 of return in the function expression is K.
  • If the function is async* and K is Stream<S> for some S, then the context for operands of yield in the function expression is S.
  • Otherwise, if the function is sync* and K is Iterable<S> for some S, then the context for operands of yield in the function expression is S.
  • Otherwise, the context for operands of return/yield in the function expression is FutureOr<futureValueTypeSchema(K)> (where futureValueTypeSchema is defined here).

The analyzer behavior differs from the spec in the following ways:

  • If K is _ or dynamic, then the context for operands of return and yield in the function expression is _, regardless of the function expression's async/generator marker.
  • Matching of Stream and Iterable is done by ignoring trailing ?s, replacing type parameters with their bounds, and then using "as instance of" semantics (e.g. if K is T&MyStream?, where MyStream extends Stream<int>, then that produces the same result that K=Stream<int> would).

The front end behavior differs from the spec in the following ways:

  • Matching of Stream and Iterable is done using "union free" semantics (ignoring trailing ?s and unwrapping FutureOr<S> to S), but otherwise requiring a precise match (e.g. if K is FutureOr<Stream<int>?>?, then that produces the same result that K=Stream<int> would, but if K is MyStream, where MyStream extends Stream<int>, then that is considered not to match).
  • If the function is async* or sync* and K doesn't match Stream (or, respectively, Iterable), then the context for operands of yield in the function expression is _ (not FutureOr<futureValueTypeSchema(K)>).
  • If the function is async, then the context for operands of return in the function expression is wrapFutureOr(futureValueTypeSchema(K)), where wrapFutureOr 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) is Stream<S> for some S, then the context for operands of yield in the function expression is S. Otherwise, it's _.
    • Where unionFree is defined as follows:
      • unionFree(S?) = unionFree(S)
      • unionFree(FutureOr<S>) = unionFree(S)
      • Otherwise, unionFree(S) = S.
  • If the function is sync*:
    • If unionFree(K) is Iterable<S> for some S, then the context for operands of yield in the function expression is S. Otherwise, it's _.
  • If the function is async:
    • Let S be futureValueTypeSchema(K).
    • If S is _ or dynamic, then the context for operands of return in the function expression is _.
    • Otherwise, it's FutureOr<S>.

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    requestRequests to resolve a particular developer problem

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions