diff --git a/README.md b/README.md index d515cb9..d3287db 100644 --- a/README.md +++ b/README.md @@ -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 is deterministically synchronous (that is, if it and its dependencies each contain no top-level `await`), there will be no entry in the `Promise.all` for that module. In this case, 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? diff --git a/spec.html b/spec.html index 52f402b..30cdb3b 100644 --- a/spec.html +++ b/spec.html @@ -130,18 +130,34 @@
For all module types defined in this specification, it's statically determined (given the set of parsed modules and the graph connecting them) whether a module will have an [[EvaluationPromise]] which is not *undefined* after Evaluate() is called. A Promise is only present if the module or a dependency has the [[Async]] flag set. As more module types are added, the intention is that this would remain the case. However, concepts like "dependency" are not defined on Abstract Module Records.
+|
-
If this module has already been evaluated successfully, return *undefined*; if it has already been evaluated unsuccessfully, throw the exception that was produced. Otherwise, Transitively evaluate all module dependencies of this module and then evaluate this module. Instantiate must have completed successfully prior to invoking this method. |
@@ -222,13 +238,13 @@ ||
|
- |
- |
- |
| + [[ModuleAsync]] + | ++ *true* or *false* + | ++ Whether this module is individually asynchronous. A module may have [[Async]] *true* and [[ModuleAsync]] *false* if dependencies are asynchronous, but this module is not. This field must not change after the module is parsed. + | +
The Instantiate concrete method of a Cyclic Module Record implements the corresponding Module Record abstract method.
+On success, Instantiate transitions this module's [[Status]] from `"uninstantiated"` to `"instantiated"`. On failure, an exception is thrown and this module's [[Status]] remains `"uninstantiated"`.
+ +This abstract method performs the following steps (most of the work is done by the auxiliary function InnerModuleInstantiation):
+ +The InnerModuleInstantiation abstract operation is used by Instantiate to perform the actual instantiation process for the Cyclic Module Record _module_, as well as recursively on all other modules in the dependency graph. The _stack_ and _index_ parameters, as well as a module's [[DFSIndex]] and [[DFSAncestorIndex]] fields, keep track of the depth-first search (DFS) traversal. In particular, [[DFSAncestorIndex]] is used to discover strongly connected components (SCCs), such that all modules in an SCC transition to `"instantiated"` together.
+ +This abstract operation performs the following steps:
+ +The Evaluate concrete method of a Cyclic Module Record implements the corresponding Module Record abstract method.
-Evaluate transitions this module's [[Status]] from `"instantiated"` to `"evaluated"`, 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..
+Evaluate transitions this module's [[Status]] from `"instantiated"` to `"evaluated"`.
+ +If execution results in a synchronous exception, that exception is recorded in the [[EvaluationError]] field and rethrown by future invocations of Evaluate.
-If execution results in an exception, that exception is recorded in the [[EvaluationError]]rejection of the [[ExecPromise]] field and rethrown by future invocations of Evaluate.
If the module is [[Async]], then
This abstract method performs the following steps (most of the work is done by the auxiliary function InnerModuleEvaluation):
@@ -313,21 +414,19 @@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.
@@ -451,7 +562,7 @@The ExecuteModule concrete method of a Source Text Module Record implements the corresponding Cyclic Module Record abstract method.
@@ -464,14 +575,18 @@Consider then cases involving instantiation errors. If InnerModuleInstantiation of _C_ succeeds but, thereafter, fails for _B_, for example because it imports something that _C_ does not provide, then the original _A_.Instantiate() will fail, and both _A_ and _B_'s [[Status]] remain `"uninstantiated"`. _C_'s [[Status]] has become `"instantiated"`, though.
-Finally, consider a case involving evaluation errors. If InnerModuleEvaluation of _C_ succeeds but, thereafter, fails for _B_, for example because _B_ contains code that throws an exception, then the original _A_.Evaluate() will fail, returning a Promise resolving to *undefined*. The resulting exception will be recorded in both _A_ and _B_'s [[EvaluationError]][[ExecPromise]] fields as a rejected Promise, and their [[Status]] will become `"evaluated"`. _C_ will also become `"evaluated"` but, in contrast to _A_ and _B_, will remain without an [[EvaluationError]]have its [[ExecPromise]] internal slot set to a Promise resolved to *undefined*, as it successfully completed evaluation. Storing the exception in [[ExecPromise]] as a Promse rejection ensures that any time a host tries to reuse _A_ or _B_ by calling their Evaluate() method, it will encounter the same exception. (Hosts are not required to reuse Source Text Module Records; similarly, hosts are not required to expose the exception objects thrown by these methods. However, the specification enables such uses.)
Finally, consider a case involving evaluation errors. If InnerModuleEvaluation of _C_ succeeds but, thereafter, fails for _B_, for example because _B_ contains code that throws an exception, then the original _A_.Evaluate() will fail, returning a Promise resolving to *undefined*. The resulting exception will be recorded in both _A_ and _B_'s [[EvaluationError]][[EvaluationResult]] fields, and their [[Status]] will become `"evaluated"`. _C_ will also become `"evaluated"` but, in contrast to _A_ and _B_, will remain without an [[EvaluationError]]have its [[EvaluationResult]] internal slot set to *undefined*, as it successfully completed evaluation. Storing the exception in [[EvaluationResult]] ensures that any time a host tries to reuse _A_ or _B_ by calling their Evaluate() method, it will encounter the same exception. (Hosts are not required to reuse Source Text Module Records; similarly, hosts are not required to expose the exception objects thrown by these methods. However, the specification enables such uses.)
The difference here between instantiation and evaluation errors is due to how evaluation must be only performed once, as it can cause side effects; it is thus important to remember whether evaluation has already been performed, even if unsuccessfully. (In the error case, it makes sense to also remember the exception because otherwise subsequent Evaluate() calls would have to synthesize a new one.) Instantiation, on the other hand, is side-effect-free, and thus even if it fails, it can be retried at a later time with no issues.
@@ -515,7 +630,7 @@Now consider a case where _A_ has an instantiation error; for example, it tries to import a binding from _C_ that does not exist. In that case, the above steps still occur, including the early return from the second call to InnerModuleInstantiation on _A_. However, once we unwind back to the original InnerModuleInstantiation on _A_, it fails during ModuleDeclarationEnvironmentSetup, namely right after _C_.ResolveExport(). The thrown *SyntaxError* exception propagates up to _A_.Instantiate, which resets all modules that are currently on its _stack_ (these are always exactly the modules that are still `"instantiating"`). Hence both _A_ and _B_ become `"uninstantiated"`. Note that _C_ is left as `"instantiated"`.
-Finally, consider a case where _A_ has an evaluation error; for example, its source code throws an exception. In that case, the evaluation-time analog of the above steps still occurs, including the early return from the second call to InnerModuleEvaluation on _A_. However, once we unwind back to the original InnerModuleEvaluation on _A_, it fails by assumption. The exception thrown propagates up to _A_.Evaluate() , which records the error in all modules that are currently on its _stack_ (i.e., the modules that are still `"evaluating"`)via Promise rejections, which form a chain through the whole dependency graph due to ExecuteModuleWhenImportsReady. Hence both _A_ and _B_ become `"evaluated"` and the exception is recorded in both _A_ and _B_'s [[EvaluationError]][[ExecPromise]] fields, while _C_ is left as `"evaluated"` with no [[EvaluationError]]its [[ExecPromise]] set to a Promise resolved to *undefined*.
Finally, consider a case where _A_ has an evaluation error; for example, its source code throws an exception. In that case, the evaluation-time analog of the above steps still occurs, including the early return from the second call to InnerModuleEvaluation on _A_. However, once we unwind back to the original InnerModuleEvaluation on _A_, it fails by assumption. The exception thrown propagates up to _A_.Evaluate() , which records the error in all modules that are currently on its _stack_ (i.e., the modules that are still `"evaluating"`)via Promise rejections, which form a chain through the whole dependency graph due to ExecuteModuleWhenImportsReady. Hence both _A_ and _B_ become `"evaluated"` and the exception is recorded in both _A_ and _B_'s [[EvaluationError]][[EvaluationResult]] fields, while _C_ is left as `"evaluated"` with no [[EvaluationError]]its [[EvaluationResult]] set to *undefined*.