Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the purpose of performing the Link() algorithm many times? #3474

Open
dSalieri opened this issue Nov 11, 2024 · 2 comments
Open

What is the purpose of performing the Link() algorithm many times? #3474

dSalieri opened this issue Nov 11, 2024 · 2 comments

Comments

@dSalieri
Copy link

If you look at the history of modules in the ECMAScript specification, it has been constantly changing. Although some things have remained as they were originally invented in ES6.

My question is about the architecture of modules, and specifically about why the Link() algorithm can change the [[Status]] field of a module from linking to unlinked? This happens in the only case when a Syntax Error occurs in the InitializeEnvironment() method of a module, due to a binding resolution error.

  1. Let result be Completion(InnerModuleLinking(module, stack, 0)).

  2. If result is an abrupt completion, then
    a. For each Cyclic Module Record m of stack, do

    1. Assert: m.[[Status]] is linking.
    2. Set m.[[Status]] to unlinked.

    b. Assert: module.[[Status]] is unlinked.
    c. Return ? result.

If you look at ES6, there, if a module received the Module Environment Record value in the [[Environment]] field once, then the algorithm noticed this and reset further ModuleDeclarationInstantiation() steps the next time. This happened both in the case of a binding resolution error for a module and in the case when ModuleDeclarationInstantiation() was successful the algorithm simply did not reach the second binding resolution.

  1. If module.[[Environment]] is not undefined, return NormalCompletion(empty).
  2. Let env be NewModuleEnvironment(realm.[[globalEnv]]).
  3. Set module.[[Environment]] to env.

Now this algorithm ModuleDeclarationInstantiation() is broken down into Link() -> InnerModuleLinking(module, stack, index) -> InitializeEnvironment(). And if before it did not execute the algorithm more than once in any case, now it has a loophole that allows you to execute Link() again in full if an error is detected in resolving bindings.

Why was this done? What was achieved this way? Recalculate the error result, which we already know from the first calculation, what's the point?

@nicolo-ribaudo
Copy link
Member

linking and evaluating states are meant to be "internal states" of the link and evaluate algorithm. When those algorithms are not running, all modules are guaranteed to be in one of these states:

  • new
  • unlinked
  • linked
  • evaluating-async
  • evaluated

This means that consumers have less states to think about, and that all transitions across the possible states that a consumer can see are easily definable:

  • from new to unlinked you need to load the dependencies
  • from unlinked to linked you call Link()
  • from linked to evaluated or evaluating-async you call Evaluate()
  • from evaluating-async to evaluated you wait on the promise

Intermediate states would be impossible to handle for consumers: for example, what do you do with a module that is linking? How do you upgrade it to linked? You cannot just call Link() on it, because what if you would be causing re-entrancy by doing so?

Recalculate the error result, which we already know from the first calculation, what's the point?

It's conceptually easier to not have a cache than to have one, if the cache is not observable.

Implementations can instead optimize the spec algorithms by adding caches wherever they want, as long as they have the same observable behavior of the spec (i.e. the cached-and-rethrown linking error must not be === to the previous one)

@dSalieri
Copy link
Author

@nicolo-ribaudo

Intermediate states would be impossible to handle for consumers: for example, what do you do with a module that is linking? How do you upgrade it to linked? You cannot just call Link() on it, because what if you would be causing re-entrancy by doing so?

You can just leave it as is, but borrow the strategy from the Evaluate() phase:
Add a [[LinkingError]] field and store throw completion there (or make the [[EvaluatingError]] field common for both phases, if this does not conflict), and then when calling Link(), check if there was an error and exit the algorithm. This looks more logical and corresponds to the logic from ES6, when only one initialization was performed for the module.

What do you think? Wouldn't it be simpler?

As for the observed behavior and caches that implementations can implement, it is clear.

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

No branches or pull requests

2 participants