Skip to content

Instantly share code, notes, and snippets.

@rauschma
Last active May 24, 2019 07:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rauschma/8443b5eb3a3e00ca52e174e909cd5e21 to your computer and use it in GitHub Desktop.
Save rauschma/8443b5eb3a3e00ca52e174e909cd5e21 to your computer and use it in GitHub Desktop.

ESM’s transparent support for cyclic imports

How do ES modules handle cyclic imports transparently? Normally, imports form a directed acyclic graph (DAG): think tree, but a single node can have multiple parents. That is: A module at the root imports one or more modules, which themselves import other modules, etc. If there is a cycle, we have a directed cyclic graph (DCG). For example, a module P may import a module M that already appears earlier in the DCG:

╭─────> M
│     ╱   ╲
│   N       O
│  ╱ ╲     ╱ ╲
╰─P   Q   R   S

Every module goes through the following life cycle stages (which are encoded as strings in the specification):

  • "uninstantiated"
  • "instantiating"
  • "instantiated"
  • "evaluating"
  • "evaluated"

After parsing, setting up modules involves the following steps (I’m using names and structure of the ECMAScript specification):

  • Instantiation: connects imports to exports. It visits the complete DCG. Each module is fully instantiated once all of its children are instantiated. Cycles make that more complicated: P is only considered to be "instantiated" once M and all of its children are instantiated. The same is true for its parent N. Instantiation accounts for cycles by detecting roots of strongly connected components (such as the DCG whose root is M). Every member of the component is only "instantiated" once all members are instantiated.
    • Environment initialization: This step sets up the environment of a module. An environment stores the variables (imports and local variables) of the module. It is structured as a set of bindings (name-value pairs).
      • Export resolution: This step adds bindings to the module environment that point to exports, via a pair (module, export name). The latter is the internal name of the export. Since an export of a module may be a re-export, export resolution searches the module and its children until it finds a direct export. During this step, cycles are not allowed, because then we’d never have a real (direct) export.
  • Evaluation: executes module bodies.

This way of handling cyclic imports is enabled by the following two ES module features:

  • The static structure of ES modules means that export resolution works before evaluation.

  • When P is evaluated, M hasn’t been evaluated, yet. Entities in P can mention imports from M. They only can’t use them, yet, because the imported values are filled in later. Imports being filled in later is made possible by them being “live immutable views” on exports.

    For example, a function in P can access an import from M. The only limitation is that we must wait until after the evaluation of M, before calling that function.

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