Skip to content

Commit

Permalink
Normative: Avoid microtask queue delay when resolved
Browse files Browse the repository at this point in the history
  • Loading branch information
littledan committed Mar 1, 2019
1 parent 1994eb9 commit 11a4bff
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 62 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,9 @@ Currently (in a world without top-level `await`), polyfills are synchronous. So,
#### Does the `Promise.all` happen even if none of the imported modules have a top-level `await`?
Yes. In particular, if none of the imported modules have a top-level `await`, there will still be a delay of some turns on the Promise job queue until the module body executes. The goal here is to avoid too much synchronous behavior, which would break if something turns out to be asynchronous in the future, or even alternate between those two depending on runtime conditions ("releasing Zalgo"). Similar considerations led to the decision that `await` should always be asynchronous, even if passed a non-Promise.
If the module's execution was synchronous (that is, if no top-level `await` is reached), there will be no entry in the `Promise.all` for that module. If a module has no dependencies which are asynchronous in this way, it will run synchronously.
Note, this is an observable change from current ES Module semantics, where the Evaluate phase is entirely synchronous. For a concrete example and further discussion, see [issue #43](https://github.com/tc39/proposal-top-level-await/issues/43) and [#47](https://github.com/tc39/proposal-top-level-await/issues/47).
These semantics preserve the current behavior of ES Modules, where, when top-level `await` is not used, the Evaluate phase is entirely synchronous. The semantics are a bit in contrast with uses of Promises elsewhere. For a concrete example and further discussion, see [issue #43](https://github.com/tc39/proposal-top-level-await/issues/43) and [#47](https://github.com/tc39/proposal-top-level-await/issues/47).
#### Does top-level `await` increase the risk of deadlocks?
Expand Down
95 changes: 35 additions & 60 deletions spec.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,8 @@ <h1>Introduction</h1>
<p>Top-Level Await allows the `await` keyword to be used at the top level of the module goal. See <a href="https://github.com/tc39/proposal-top-level-await/blob/master/README.md">the explainer</a> for the motivation, context, and high-level semantics.</p>
</emu-intro>

<emu-clause id="sec-async-functions-abstract-operations-async-function-start" aoid="AsyncFunctionStart">
<h1>AsyncFunctionStart ( _promiseCapability_, _asyncFunctionBody_ )</h1>
<emu-alg>
1. Let _runningContext_ be the running execution context.
1. Let _asyncContext_ be a copy of _runningContext_.
1. <ins>Perform ! AsyncBlockStart(_promiseCapability_, _asyncFunctionBody_, _asyncContext_).</ins>
1. <del>Set the code evaluation state of _asyncContext_ such that when evaluation is resumed for that execution context the following steps will be performed:</del>
1. <del>Let _result_ be the result of evaluating _asyncFunctionBody_.</del>
1. <del>Assert: If we return here, the async function either threw an exception or performed an implicit or explicit return; all awaiting is done.</del>
1. <del>Remove _asyncContext_ from the execution context stack and restore the execution context that is at the top of the execution context stack as the running execution context.</del>
1. <del>If _result_.[[Type]] is ~normal~, then</del>
1. <del>Perform ! Call(_promiseCapability_.[[Resolve]], *undefined*, &laquo;*undefined*&raquo;).</del>
1. <del>Else if _result_.[[Type]] is ~return~, then</del>
1. <del>Perform ! Call(_promiseCapability_.[[Resolve]], *undefined*, &laquo;_result_.[[Value]]&raquo;).</del>
1. <del>Else,</del>
1. <del>Assert: _result_.[[Type]] is ~throw~.</del>
1. <del>Perform ! Call(_promiseCapability_.[[Reject]], *undefined*, &laquo;_result_.[[Value]]&raquo;).</del>
1. <del>Return.</del>
1. <del>Push _asyncContext_ onto the execution context stack; _asyncContext_ is now the running execution context.</del>
1. <del>Resume the suspended evaluation of _asyncContext_. Let _result_ be the value returned by the resumed computation.</del>
1. <del>Assert: When we return here, _asyncContext_ has already been removed from the execution context stack and _runningContext_ is the currently running execution context.</del>
1. <del>Assert: _result_ is a normal completion with a value of *undefined*. The possible sources of completion values are Await or, if the async function doesn't await anything, the step 3.g above.</del>
1. Return.
</emu-alg>
</emu-clause>

<emu-clause id="sec-asyncblockstart" aoid="AsyncBlockStart">
<h1><ins>AsyncBlockStart ( _promiseCapability_, _asyncBody_, _asyncContext_ )</ins></h1>
<emu-clause id="sec-asyncmodulestart" aoid="AsyncModuleStart">
<h1><ins>AsyncModuleStart ( _promiseCapability_, _asyncBody_, _asyncContext_, _moduleRecord_ )</ins></h1>
<emu-alg>
1. Let _runningContext_ be the running execution context.
1. Set the code evaluation state of _asyncContext_ such that when evaluation is resumed for that execution context the following steps will be performed:
Expand All @@ -53,11 +27,13 @@ <h1><ins>AsyncBlockStart ( _promiseCapability_, _asyncBody_, _asyncContext_ )</i
1. Remove _asyncContext_ from the execution context stack and restore the execution context that is at the top of the execution context stack as the running execution context.
1. If _result_.[[Type]] is ~normal~, then
1. Perform ! Call(_promiseCapability_.[[Resolve]], *undefined*, &laquo;*undefined*&raquo;).
1. Else if _result_.[[Type]] is ~return~, then
1. Perform ! Call(_promiseCapability_.[[Resolve]], *undefined*, &laquo;_result_.[[Value]]&raquo;).
1. If _moduleRecord_.[[EvaluationResult]] is ~empty~, set _moduleRecord_.[[EvaluationResult]] to *undefined*.
1. Assert: Otherwise, _moduleRecord_.[[EvaluationResult]] is a Promise.
1. Else,
1. Assert: _result_.[[Type]] is ~throw~.
1. Perform ! Call(_promiseCapability_.[[Reject]], *undefined*, &laquo;_result_.[[Value]]&raquo;).
1. If _moduleRecord_.[[EvaluationResult]] is ~empty~, set _moduleRecord_.[[EvaluationResult]] to _result_.
1. Assert: Otherwise, _moduleRecord_.[[EvaluationResult]] is a Promise.
1. Return.
1. Push _asyncContext_ onto the execution context stack; _asyncContext_ is now the running execution context.
1. Resume the suspended evaluation of _asyncContext_. Let _result_ be the value returned by the resumed computation.
Expand Down Expand Up @@ -231,13 +207,13 @@ <h1>Source Text Module Records</h1>
</tr>
<tr>
<td>
<ins>[[ExecPromise]]</ins>
<ins>[[EvaluationResult]]</ins>
</td>
<td>
<ins>Promise | *undefined*</ins>
<ins>~empty~ | Promise | *undefined* | an abrupt completion</ins>
</td>
<td>
<ins>The evaluation promise for this Abstract Module Record, including any dependency evaluations.</ins>
<ins>The evaluation result for this Abstract Module Record, including any dependency evaluations. If the module has not started to execute, it will be ~empty~. If a top-level await was reached, the result will be a Promise.</ins>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -296,7 +272,7 @@ <h1>ParseModule ( _sourceText_, _realm_, _hostDefined_ )</h1>
1. Append _ee_ to _starExportEntries_.
1. Else,
1. Append _ee_ to _indirectExportEntries_.
1. Return Source Text Module Record { [[Realm]]: _realm_, [[Environment]]: *undefined*, [[Namespace]]: *undefined*, <ins>[[ExecPromise]]: *undefined*, </ins>[[Status]]: `"uninstantiated"`, <del>[[EvaluationError]]: *undefined*,</del> [[HostDefined]]: _hostDefined_, [[ECMAScriptCode]]: _body_, [[RequestedModules]]: _requestedModules_, [[ImportEntries]]: _importEntries_, [[LocalExportEntries]]: _localExportEntries_, [[IndirectExportEntries]]: _indirectExportEntries_, [[StarExportEntries]]: _starExportEntries_, [[DFSIndex]]: *undefined*, [[DFSAncestorIndex]]: *undefined* }.
1. Return Source Text Module Record { [[Realm]]: _realm_, [[Environment]]: *undefined*, [[Namespace]]: *undefined*, <ins>[[EvaluationResult]]: ~empty~, </ins>[[Status]]: `"uninstantiated"`, <del>[[EvaluationError]]: *undefined*,</del> [[HostDefined]]: _hostDefined_, [[ECMAScriptCode]]: _body_, [[RequestedModules]]: _requestedModules_, [[ImportEntries]]: _importEntries_, [[LocalExportEntries]]: _localExportEntries_, [[IndirectExportEntries]]: _indirectExportEntries_, [[StarExportEntries]]: _starExportEntries_, [[DFSIndex]]: *undefined*, [[DFSAncestorIndex]]: *undefined* }.
</emu-alg>
<emu-note>
<p>An implementation may parse module source text and analyse it for Early Error conditions prior to the evaluation of ParseModule for that module source text. However, the reporting of any errors must be deferred until the point where this specification actually performs ParseModule upon that source text.</p>
Expand All @@ -307,31 +283,28 @@ <h1>ParseModule ( _sourceText_, _realm_, _hostDefined_ )</h1>
<h1>Evaluate( ) Concrete Method</h1>

<p>The Evaluate concrete method of a Source Text Module Record implements the corresponding Module Record abstract method.</p>
<p>Evaluate transitions this module's [[Status]] from `"instantiated"` to `"evaluated"`<ins>, at which point the [[ExecPromise]] Promise field is populated to a promise resolving on completion of the module execution, including its dependency executions, or the associated execution error.</ins>.</p>
<p>Evaluate transitions this module's [[Status]] from `"instantiated"` to `"evaluated"`<ins>, at which point the [[EvaluationResult]] field is populated to a representation of the result of execution.</ins>.</p>

<p>If execution results in an exception, that exception is recorded in the <del>[[EvaluationError]]</del><ins>rejection of the [[ExecPromise]]</ins> field and rethrown by future invocations of Evaluate.</del></p>
<p>If execution results in a <ins>synchronous</ins> exception, that exception is recorded in the <del>[[EvaluationError]]</del><ins>[[EvaluationResult]]</ins> field and rethrown by future invocations of Evaluate.</del></p>

<p>This abstract method performs the following steps (most of the work is done by the auxiliary function InnerModuleEvaluation):</p>

<emu-alg>
1. Let _module_ be this Source Text Module Record.
1. Assert: _module_.[[Status]] is `"instantiated"` or `"evaluated"`.
1. Let _stack_ be a new empty List.
1. <del>Let _result_ be </del><ins>Perform ! </ins>InnerModuleEvaluation(_module_, _stack_, 0).
1. <ins>Assert: _stack_ is empty.</ins>
1. <ins>Assert: _module_.[[Status]] is `"evaluated"`.</ins>
1. <ins>Return _module_.[[ExecPromise]].</ins>
1. <del>If _result_ is an abrupt completion, then</del>
1. <del>For each module _m_ in _stack_, do</del>
1. <del>Assert: _m_.[[Status]] is `"evaluating"`.</del>
1. <del>Set _m_.[[Status]] to `"evaluated"`.</del>
1. <del>Set _m_.[[EvaluationError]] to _result_.</del>
1. <del>Assert: _module_.[[Status]] is `"evaluated"` and _module_.[[EvaluationError]] is _result_.</del>
1. <del>Return _result_.</del>
1. <del>Assert: _result_ is *undefined*.</del>
1. <del>Assert: _module_.[[Status]] is `"evaluated"` and _module_.[[EvaluationError]] is *undefined*</del>.
1. <del>Assert: _stack_ is empty.</del>
1. <del>Return *undefined*.</del>
1. Let _result_ be InnerModuleEvaluation(_module_, _stack_, 0).
1. If _result_ is an abrupt completion, then
1. For each module _m_ in _stack_, do
1. Assert: _m_.[[Status]] is `"evaluating"`.
1. Set _m_.[[Status]] to `"evaluated"`.
1. <ins>Assert: _m_.[[EvaluationResult]] is ~empty~.</ins>
1. Set _m_.[[Evaluation<del>Error</del><ins>Result</ins>]] to _result_.
1. Assert: _module_.[[Status]] is `"evaluated"` and _module_.[[Evaluation<del>Error</del><ins>Result</ins>]] is _result_.
1. Return _result_.
1. Assert: _module_.[[Status]] is `"evaluated"` and _module_.[[Evaluation<del>Error</del><ins>Result</ins>]] is <ins>either a Promise or</ins>*undefined*.
1. Assert: _stack_ is empty.
1. Return <del>*undefined*.</del> <ins>_module_.[[EvaluationResult]].</ins>
</emu-alg>

<emu-clause id="sec-innermoduleevaluation" aoid="InnerModuleEvaluation">
Expand All @@ -343,19 +316,16 @@ <h1>InnerModuleEvaluation( _module_, _stack_, _index_ )</h1>

<emu-alg>
1. If _module_ is not a Source Text Module Record, then
1. <ins>Set _module_.[[ExecPromise]] to </ins>_module_.Evaluate().
1. <ins>Set _module_.[[EvaluationResult]] to </ins>_module_.Evaluate().
1. Return _index_.
1. If _module_.[[Status]] is `"evaluated"`, then
1. <ins>Return _index_.</ins>
1. <del>If _module_.[[EvaluationError]] is *undefined*, return _index_.</del>
1. <del>Otherwise return _module_.[[EvaluationError]].</del>
1. If _module_.[[EvaluationError]] is *undefined*, return _index_.
1. Otherwise return _module_.[[Evaluation<del>Error</del><ins>Result</ins>]].
1. If _module_.[[Status]] is `"evaluating"`, return _index_.
1. Assert: _module_.[[Status]] is `"instantiated"`.
1. Set _module_.[[Status]] to `"evaluating"`.
1. Set _module_.[[DFSIndex]] to _index_.
1. Set _module_.[[DFSAncestorIndex]] to _index_.
1. <ins>Let _evalCapability_ be ! NewPromiseCapability(%Promise%).</ins>
1. <ins>Set _module_.[[ExecPromise]] to _evalCapability_.[[Promise]].
1. Set _index_ to _index_ + 1.
1. Append _module_ to _stack_.
1. <ins>Let _dependencyExecPromises_ be an empty List.</ins>
Expand All @@ -366,12 +336,16 @@ <h1>InnerModuleEvaluation( _module_, _stack_, _index_ )</h1>
1. Assert: _requiredModule_.[[Status]] is either `"evaluating"` or `"evaluated"`.
1. Assert: _requiredModule_.[[Status]] is `"evaluating"` if and only if _requiredModule_ is in _stack_.
1. <ins>If _requiredModule_.[[Status]] is `"evaluated"`, then</ins>
1. <ins>Add _requiredModule_.[[ExecPromise]] to the list _dependencyExecPromises_.</ins>
1. <ins>If _requiredModule_.[[EvaluationResult]] a Promise, then</ins>
1. <ins>Add _requiredModule_.[[EvaluationResult]] to the list _dependencyExecPromises_.</ins>
1. If _requiredModule_.[[Status]] is `"evaluating"`, then
1. Assert: _requiredModule_ is a Source Text Module Record.
1. Set _module_.[[DFSAncestorIndex]] to min(_module_.[[DFSAncestorIndex]], _requiredModule_.[[DFSAncestorIndex]]).
1. <del>Perform ? ModuleExecution(_module_).</del>
1. <ins>Let _evalCapability_ be ! NewPromiseCapability(%Promise%).</ins>
1. <ins>Perform ! ExecuteModuleWhenImportsReady(_module_, _dependencyExecPromises_, _evalCapability_).</ins>
1. <ins>If _module_.[[EvaluationResult]] is ~empty~,</ins>
1. <ins>Set _module_.[[EvaluationResult]] to _evalCapability_.[[Promise]].</ins>
1. Assert: _module_ occurs exactly once in _stack_.
1. Assert: _module_.[[DFSAncestorIndex]] is less than or equal to _module_.[[DFSIndex]].
1. If _module_.[[DFSAncestorIndex]] equals _module_.[[DFSIndex]], then
Expand All @@ -381,7 +355,8 @@ <h1>InnerModuleEvaluation( _module_, _stack_, _index_ )</h1>
1. Remove the last element of _stack_.
1. Set _requiredModule_.[[Status]] to `"evaluated"`.
1. If _requiredModule_ and _module_ are the same Module Record, set _done_ to *true*.
1. <ins>Otherwise, set _requiredModule_.[[ExecPromise]] to _module_.[[ExecPromise]].</ins>
1. <ins>Otherwise, if _module_.[[EvaluationResult]] is a Promise,</ins>
1. <ins>Set _requiredModule_.[[EvaluationResult]] to _module_.[[EvaluationResult]].</ins>
1. Return _index_.
</emu-alg>
<emu-note>
Expand Down Expand Up @@ -435,7 +410,7 @@ <h1>ModuleExecution( _module_, <ins>_capability_</ins>)</h1>
1. <del>Suspend _moduleCxt_ and remove it from the execution context stack.</del>
1. <del>Resume the context that is now on the top of the execution context stack as the running execution context.</del>
1. <del>Return Completion(_result_).</del>
1. <ins>Perform ! AsyncBlockStart(_capability_, _module_.[[ECMAScriptCode]], _moduleCxt_).</ins>
1. <ins>Perform ! AsyncModuleStart(_capability_, _module_.[[ECMAScriptCode]], _moduleCxt_, _module_).</ins>
1. <ins>Return.</ins>
</emu-alg>
</emu-clause>
Expand Down

0 comments on commit 11a4bff

Please sign in to comment.