Skip to content

Commit ac2a70f

Browse files
authored
Update spec for yield*. (#4514)
* Update spec for yield\*. Was very under-specified. This change changes the responsiveness of a `yield*` to be that of `StreamController.addStream`, meaning that it *immediately* reacts to pause and cancel and forwards it to the inner stream. Unlike `await for`, a `yield*` is at the yield for the entire duration of the inner stream, and can cancel at any time, not only when control reaches a `yield` inside the loop. Experience has taught us that with `async*` functions, `await for` and `yield*`, back-pressure using `cancel` or `pause` *must* be acted on immediately, to avoid a stream computation continuing when it has nothing meaningful to do, and the caller knows that and tries to stop it. This text likely specifies *more* behavior than any current implementation does. The most important part is to wait for events to be delivered, pausing if necessary, forwarding pause/cancel immediately even between events, and to wait for the `cancel` future if cancelled. Checking for being cancelled before listening, instead of listeneing and immediately cancelling, is less important, but it's imperative to pause or cancel right after listening if the outer stream is paused or cancelled, because there may never come a first event. For example, if you do `stream.first`, it should cancel after the first event, before starting on the computation of the next event, because then it's too late to cancel. Canonical example: ```dart // Stream with one event and no done event. Stream<int> oneValue => Stream<int>.multi((c) => c.add(1)); Stream<int> clone1(Stream<int> stream) async* { await for (var event in stream) yield event; } Stream<int> clone2(Stream<int> stream) async* { yield* stream; } void main() async { print(await oneValue.first); // 1 print(await clone1(oneValue).first); print(await clone2(oneValue).first); print(await clone2(clone1(oneValue)).first); } ``` If the `first` code's cancel doesn't reach the `clone1` before it goes back to the `await for` loop, the cancel will never be processed because control never reaches a `yield` again.
1 parent f35c4ad commit ac2a70f

File tree

1 file changed

+71
-23
lines changed

1 file changed

+71
-23
lines changed

specification/dartLangSpec.tex

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19794,7 +19794,6 @@ \subsection{Yield-Each}
1979419794

1979519795
\LMHash{}%
1979619796
If $m$ is marked \code{\ASYNC*} (\ref{functions}), then:
19797-
1979819797
\begin{itemize}
1979919798
\item
1980019799
% This error can occur due to implicit casts.
@@ -19805,34 +19804,83 @@ \subsection{Yield-Each}
1980519804
The nearest enclosing asynchronous for loop (\ref{asynchronousFor-in}),
1980619805
if any, is paused.
1980719806
\item
19808-
The $o$ stream is listened to, creating a subscription $s$,
19809-
and for each event $x$, or error $e$ with stack trace $t$, of $s$:
19810-
19807+
If the stream subscription $u$ associated with this execution of $m$
19808+
has been paused, suspend execution of $m$ until $u$ has been resumed or
19809+
cancelled.
19810+
\item
19811+
If $u$ has been cancelled, execution of $s$ returns without a value.
19812+
\item
19813+
The $o$ stream is listened to by calling its \code{listen} method,
19814+
creating a subscription $r$.
19815+
\item
19816+
Execution of $m$ is suspended. Until execution of $s$ completes in one of
19817+
the ways specified below, execution of $m$ occurs in the following cases:
1981119818
\begin{itemize}
19812-
\item
19813-
If the stream $u$ associated with $m$ has been paused,
19814-
then execution of $m$ is suspended until $u$ is resumed or canceled.
19815-
\item
19816-
If the stream $u$ associated with $m$ has been canceled,
19817-
then $s$ is canceled by evaluating \code{\AWAIT{} v.cancel()}
19818-
where $v$ is a fresh variable referencing the stream subscription $s$.
19819-
Then, if the cancel completed normally,
19820-
the stream execution of $s$ returns without an object
19821-
(\ref{statementCompletion}).
19822-
\item
19823-
Otherwise, $x$, or $e$ with $t$, are added to
19824-
the stream associated with $m$ in the order they appear in $o$.
19819+
\item If $u$ is cancelled, whether while waiting for an event from $r$,
19820+
to deliver an event to $u$ or to be resumed from pause:
19821+
Cancel $r$ by invoking its \code{cancel} method with no arguments,
19822+
returning a future $d$.
1982519823
\commentary{%
19826-
Note that a dynamic error occurs if $x$ is added
19827-
and the dynamic type of $x$ is not a subtype of
19828-
the element type of said stream.%
19824+
A stream cannot become paused or resumed after being cancelled,
19825+
and a stream subscription must not emit events after its \code{cancel}
19826+
function has been called, so no items below may apply after this.
1982919827
}
19830-
The function $m$ may suspend.
19828+
Execution of $m$ is suspended until $d$ completes.
19829+
If $d$ completed with an error \metavar{err} and a stack trace \metavar{st},
19830+
execution of $s$ throws the error \metavar{err} and stack trace \metavar{st}.
19831+
Otherwise execution of $s$ completes by returning without a value.
19832+
\item If $u$ becomes paused, then $r$ is paused.
19833+
If $r$ is not already paused, then pause $r$ by invoking its \code{pause}
19834+
method with no arguments. If $r$ is already paused, then invoking
19835+
\code{pause} again is allowed, but not required.
19836+
Then suspend execution of $m$ again.
19837+
\commentary{%
19838+
A stream must not emit events while it's paused, so no event from $r$
19839+
may occur until $r$ is resumed.
19840+
The $r$ subscription may already be paused if it's delivering an
19841+
event asynchronously.
19842+
}
19843+
\item If $u$ resumes from being paused, and $r$ is not currently paused
19844+
while asynchronously delivering an event to $u$, then resume $r$
19845+
by invoking its \code{resume} method with no arguments.
19846+
If $r$ is also paused while delivering an event, then calling \code{resume}
19847+
is allowed, as long as that call does not make $r$ stop being paused.
19848+
\commentary{%
19849+
Stream subscriptions remember how many times they have been paused
19850+
by calling their \code{pause} method, and requires as many calls to
19851+
their \code{resume} method before they stop being paused.
19852+
}
19853+
Then suspend execution of $m$ again.
19854+
\item If $r$ emits a value event with value $v$, then $u$ emits
19855+
a value event with value $v$,
19856+
and if $r$ emits an error event with error \metavar{err}
19857+
and stack trace \metavar{st},
19858+
then $u$ emits an error event with error \metavar{err}
19859+
and stack trace \metavar{st}.
19860+
If the event of $u$ is not delivered \emph{synchronously} to the listener
19861+
of $u$, immediately when it is received from $r$, then:
19862+
\begin{itemize}
19863+
\item $r$ is paused by invoking its \code{pause} method with no arguments.
19864+
\item Execution of $m$ is suspended until the event has been delivered
19865+
or $u$ is cancelled.
19866+
\item When that event has been delievered, if $u$ is not paused or
19867+
cancelled then $r$ is resumed by invoking its \code{resume} method
19868+
with no arguments. \commentary{If the event is never delivered,
19869+
then $u$ is cancelled or perpetually paused, in which case this.}
19870+
\end{itemize}
19871+
Then suspend execution of $m$ again.
19872+
\item If $r$ emits a done event then $s$ completes normally.
1983119873
\end{itemize}
19832-
\item
19833-
If the stream $o$ is done, execution of $s$ completes normally.
1983419874
\end{itemize}
1983519875

19876+
\commentary{%
19877+
The semantics here propagates pause and cancel requests directly
19878+
to the nested stream subscription of the \code{\YIELD*}.
19879+
That ensures that a pause or cancel request is responded to as soon as possible,
19880+
to avoid the inner stream doing a larger computation to create a value that
19881+
the outer stream already knows it doesn't need, or if paused,
19882+
that it may not need.
19883+
}
1983619884

1983719885
\subsection{Assert}
1983819886
\LMLabel{assert}

0 commit comments

Comments
 (0)