Skip to content

Conversation

@nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented Jul 3, 2025

This PR separates the logic to perform a depth-first traversal on module graph out of InnerModuleLinking/InnerModuleEvaluation. It has two advantages:

  • it makes it obvious that traversal for linking and evaluation work the same way, since it's not copy&pasted code that needs to be kept in sync
  • especially for evaluation, it disentangles the traversal logic from the "keeping track of top-level await consequences" logic.

Whether this is an improvement or not is subjective (it adds a layer of indirection), so I'm
curious to read the editors opinion about it.

From an editorial/formatting point of view, ModuleGraphDFS is an AO that takes a bunch of callbacks. I've declared them inline in the Link()/Evaluate() concrete methods as abstract closures, even though they don't actually close over anything. I think having them inline rather than as separate AOs makes the reading flow simpler. Ideally, my preferred approach would be something that lets us write

1. Perform ModuleGraphDFS(_module_, _initialStatus_, _pendingStatus_) with
     Action(_m_):
       1. ...
     SCCCompletion(_m_, _sccRoot_):
       1. ...
     PostErrorCleanup(_m_, _errorCompletion_):
       1. ...

I suggest reading this commit-by-commit. The first two commits just move logic around so that what would go in the same AC of the third commit is already all in the same place.

This PR preserves the bugs that would be fixed by #3613 and #3583, but by implementing this PR + those two in engine262 all the tests I have still pass.

The import defer proposal would add a new callback to ModuleGraphDFS, _getModuleDescendants_, that for linking just returns the RequestedModules while for Evaluation it uses import defer's step 11 of https://tc39.es/proposal-defer-import-eval/#sec-innermoduleevaluation (with GatherAsynchronousTransitiveDependencies).

@nicolo-ribaudo nicolo-ribaudo marked this pull request as ready for review July 3, 2025 12:19
@nicolo-ribaudo nicolo-ribaudo force-pushed the extract-dfs-logic-2 branch 2 times, most recently from a4fc0b9 to e2975da Compare July 3, 2025 13:57
@nicolo-ribaudo
Copy link
Member Author

nicolo-ribaudo commented Aug 7, 2025

Marking as draft as:

@nicolo-ribaudo nicolo-ribaudo marked this pull request as draft August 7, 2025 20:53
@bakkot bakkot added the editor call to be discussed in the next editor call label Sep 22, 2025
@nicolo-ribaudo nicolo-ribaudo force-pushed the extract-dfs-logic-2 branch 2 times, most recently from 64f28a0 to aa7eab7 Compare September 26, 2025 14:33
@github-actions
Copy link

The rendered spec for this PR is available at https://tc39.es/ecma262/pr/3635.

@nicolo-ribaudo nicolo-ribaudo marked this pull request as ready for review September 26, 2025 14:41
@nicolo-ribaudo nicolo-ribaudo force-pushed the extract-dfs-logic-2 branch 4 times, most recently from f945022 to 21a4aab Compare September 26, 2025 14:47
Copy link
Collaborator

@jmdyck jmdyck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this PR un-defines the following ids:

  • sec-InnerModuleLinking
  • sec-innermoduleevaluation
  • sec-innermoduleinstantiation

which should maybe be retained in oldids attributes.

@nicolo-ribaudo
Copy link
Member Author

Any preference on whether the oldid for sec-InnerModuleLinking and sec-innermoduleevaluation should point to Link/Evaluate or to InnerModuleGraphDFS?

@jmdyck
Copy link
Collaborator

jmdyck commented Oct 2, 2025

Any preference on whether the oldid for sec-InnerModuleLinking and sec-innermoduleevaluation should point to Link/Evaluate or to InnerModuleGraphDFS?

Me? Maybe a slight pref for InnerModuleGraphDFS.

BTW, just noticed that the PR leaves lots of dangling references to InnerModuleLinking and InnerModuleEvaluation, mostly in Example Cyclic Module Record Graphs but elsewhere too.

…loop

Rather than doing it in the DFS recursion loop.

This changes the order in which modules are pushed in [[PendingAsyncDependencies]]:
now they automatically are sorted by their [[AsyncEvaluationOrder]]. It does not
actually matter though, because GatherAvailableAncestors will merge various
[[PendingAsyncDependencies]] lists in a non-order-preserving way so
AsyncModuleExecutionFulfilled still needs to explicitly re-sort the resulting list.
This commit deduplicates the logic that is currently both in InnerModuleLinking
and InnerModuleEvaluation, clearly splitting what logic is relative to traversing
the graph and what is relative to actually linking/evaluating modules.
@michaelficarra michaelficarra removed the editor call to be discussed in the next editor call label Dec 9, 2025
<p>An analogous story occurs for the evaluation phase of a cyclic module graph, in the success case.</p>

<p>Now consider a case where _A_ has a linking 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 InnerModuleLinking on _A_. However, once we unwind back to the original InnerModuleLinking on _A_, it fails during InitializeEnvironment, namely right after _C_.ResolveExport(). The thrown *SyntaxError* exception propagates up to _A_.Link, which resets all modules that are currently on its _stack_ (these are always exactly the modules that are still ~linking~). Hence both _A_ and _B_ become ~unlinked~. Note that _C_ is left as ~linked~.</p>
<p>Now consider a case where _A_ has a linking 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 InnerModuleGraphDFS on _A_. However, once we unwind back to the original InnerModuleGraphDFS on _A_, it fails during _action_'s _A_.InitializeEnvironment, namely right after _C_.ResolveExport(). The thrown *SyntaxError* exception propagates up to _A_.Link, which resets all modules that are currently on its _stack_ (these are always exactly the modules that are still ~linking~) thourh ModuleGraphDFS's _postErrorCleanup_. Hence both _A_ and _B_ become ~unlinked~. Note that _C_ is left as ~linked~.</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<p>Now consider a case where _A_ has a linking 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 InnerModuleGraphDFS on _A_. However, once we unwind back to the original InnerModuleGraphDFS on _A_, it fails during _action_'s _A_.InitializeEnvironment, namely right after _C_.ResolveExport(). The thrown *SyntaxError* exception propagates up to _A_.Link, which resets all modules that are currently on its _stack_ (these are always exactly the modules that are still ~linking~) thourh ModuleGraphDFS's _postErrorCleanup_. Hence both _A_ and _B_ become ~unlinked~. Note that _C_ is left as ~linked~.</p>
<p>Now consider a case where _A_ has a linking 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 InnerModuleGraphDFS on _A_. However, once we unwind back to the original InnerModuleGraphDFS on _A_, it fails during _action_'s _A_.InitializeEnvironment, namely right after _C_.ResolveExport(). The thrown *SyntaxError* exception propagates up to _A_.Link, which resets all modules that are currently on its _stack_ (these are always exactly the modules that are still ~linking~) through ModuleGraphDFS's _postErrorCleanup_. Hence both _A_ and _B_ become ~unlinked~. Note that _C_ is left as ~linked~.</p>

1. NOTE: _module_.[[AsyncEvaluationOrder]] is ~done~ if and only if _module_ had already been evaluated and that evaluation was asynchronous.
1. Perform ! Call(_capability_.[[Resolve]], *undefined*, « *undefined* »).
1. Assert: _stack_ is empty.
1. Let _result_ be Completion(ModuleGraphDFS(_module_, ~linked~, ~evaluating~, EvaluateDFSAction, EvaluateDFSCompleteSCC, EvaluateDFSPostErrorCleanup)).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Evaluate* AOs should be ACs.

1. Let _dfsPostErrorCleanup_ be a new Abstract Closure with parameters (_cyclicModule_, _errorCompletion_) that captures nothing and performs the following steps when called:
1. Set _cyclicModule_.[[Status]] to ~unlinked~.
1. Return ~unused~.
1. Perform ? ModuleGraphDFS(_module_, ~unlinked~, ~linking~, _dfsAction_, _dfsSCCcompletion_, _dfsPostErrorCleanup_).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ModuleGraphDFS is only called in 2 places: here, where it passes only ACs that capture nothing, and in Evaluate below where it passes AOs (which obviously also capture nothing). That feels like it should just be a flag (a 2-state spec enum) to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants