Skip to content

Commit 4ba35d8

Browse files
committed
vm: make vm.Module.evaluate() conditionally synchronous
This patch: - Make sure that the vm.Module.evaluate() method is conditionally synchronous based on the specification. Previously, it's unconditionally asynchronous (even for synthetic modules and source text modules without top-level await). - Clarify the synchronicity of this method in the documentation - Add more tests for the synchronicity of this method.
1 parent 5237650 commit 4ba35d8

File tree

6 files changed

+364
-15
lines changed

6 files changed

+364
-15
lines changed

doc/api/vm.md

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -618,19 +618,43 @@ in the ECMAScript specification.
618618
work after that. **Default:** `false`.
619619
* Returns: {Promise} Fulfills with `undefined` upon success.
620620

621-
Evaluate the module.
622-
623-
This must be called after the module has been linked; otherwise it will reject.
624-
It could be called also when the module has already been evaluated, in which
625-
case it will either do nothing if the initial evaluation ended in success
626-
(`module.status` is `'evaluated'`) or it will re-throw the exception that the
627-
initial evaluation resulted in (`module.status` is `'errored'`).
628-
629-
This method cannot be called while the module is being evaluated
630-
(`module.status` is `'evaluating'`).
631-
632-
Corresponds to the [Evaluate() concrete method][] field of [Cyclic Module
633-
Record][]s in the ECMAScript specification.
621+
Evaluate the module and its depenendencies. Corresponds to the [Evaluate() concrete method][] field of
622+
[Cyclic Module Record][]s in the ECMAScript specification.
623+
624+
If the module is a `vm.SourceTextModule`,`evaluate()` must be called after the module has been at least linked;
625+
otherwise it will throw an error synchronously. The returned promise may be fulfilled either synchronously
626+
or asynchronously:
627+
628+
1. If the `vm.SourceTextModule` has no top-level `await` in itself or any of its dependencies, the promise will be
629+
fulfilled _synchronously_ after the module and all its dependencies have been evaluated.
630+
1. If the evaluation succeeds, the promise will be _synchronously_ resolved to `undefined`.
631+
2. If the evaluation results in an exception, the promise will be _synchronously_ rejected with the exception
632+
that causes the evaluation to fail.
633+
2. If the `vm.SourceTextModule` has top-level `await` in itself or any of its dependencies, the promise will be
634+
fulfilled _asynchronously_ after the module and all its dependencies have been evaluated.
635+
1. If the evaluation succeeds, the promise will be _asynchronously_ resolved to `undefined`.
636+
2. If the evaluation results in an exception, the promise will be _asynchronously_ rejected with the exception
637+
that causes the evaluation to fail.
638+
639+
If the module is a `vm.SyntheticModule`, `evaluate()` always returns a promise that fulfills synchronously, see
640+
the specification of [Evaluate() of a Synthetic Module Record][]:
641+
642+
1. If the `evaluateCallback` passed to its constructor throws an exception synchronously, `evaluate()` returns
643+
a promise that will be synchronously rejected with that exception.
644+
2. If the `evaluateCallback` does not throw an exception, `evaluate()` returns a promise that will be
645+
synchronously resolved to `undefined`. This is the case even if the `evaluateCallback` is an
646+
asynchronous function, any asynchronous operations it performs will only have effect after
647+
`evaluate()` has returned. If an `evaluateCallback` rejects asynchronously, that rejection will be
648+
not reflected in the promise returned by `evaluate()`.
649+
650+
`evaluate()` could also be called again after the module has already been evaluated, in which case:
651+
652+
1. If the initial evaluation ended in success (`module.status` is `'evaluated'`), it will do nothing
653+
and return a promise that resolves to `undefined`.
654+
2. If the initial evaluation resulted in an exception (`module.status` is `'errored'`), it will re-reject
655+
the exception that the initial evaluation resulted in.
656+
657+
This method cannot be called while the module is being evaluated (`module.status` is `'evaluating'`).
634658

635659
### `module.identifier`
636660

@@ -2221,6 +2245,7 @@ const { Script, SyntheticModule } = require('node:vm');
22212245
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records
22222246
[ECMAScript Module Loader]: esm.md#modules-ecmascript-modules
22232247
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
2248+
[Evaluate() of a Synthetic Module Record]: https://tc39.es/ecma262/#sec-smr-Evaluate
22242249
[FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
22252250
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
22262251
[HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule

lib/internal/vm/module.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
ObjectPrototypeHasOwnProperty,
1414
ObjectSetPrototypeOf,
1515
PromisePrototypeThen,
16+
PromiseReject,
1617
PromiseResolve,
1718
ReflectApply,
1819
SafePromiseAllReturnArrayLike,
@@ -208,7 +209,7 @@ class Module {
208209
this[kWrap].instantiate();
209210
}
210211

211-
async evaluate(options = kEmptyObject) {
212+
evaluate(options = kEmptyObject) {
212213
validateThisInternalField(this, kWrap, 'Module');
213214
validateObject(options, 'options');
214215

@@ -228,7 +229,11 @@ class Module {
228229
'must be one of linked, evaluated, or errored',
229230
);
230231
}
231-
await this[kWrap].evaluate(timeout, breakOnSigint);
232+
try {
233+
return this[kWrap].evaluate(timeout, breakOnSigint);
234+
} catch (e) {
235+
return PromiseReject(e);
236+
}
232237
}
233238

234239
[customInspectSymbol](depth, options) {
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Flags: --experimental-vm-modules
2+
'use strict';
3+
4+
// This tests the result of evaluating a vm.SourceTextModule.
5+
const common = require('../common');
6+
7+
const assert = require('assert');
8+
// To make testing easier we just use the public inspect API. If the output format
9+
// changes, update this test accordingly.
10+
const { inspect } = require('util');
11+
const vm = require('vm');
12+
13+
globalThis.callCount = {};
14+
common.allowGlobals(globalThis.callCount);
15+
16+
// Synchronous error during evaluation results in a synchronously rejected promise.
17+
{
18+
globalThis.callCount.syncError = 0;
19+
const mod = new vm.SourceTextModule(`
20+
globalThis.callCount.syncError++;
21+
throw new Error("synchronous source text module");
22+
export const a = 1;
23+
`);
24+
mod.linkRequests([]);
25+
mod.instantiate();
26+
const promise = mod.evaluate();
27+
assert.strictEqual(globalThis.callCount.syncError, 1);
28+
assert.match(inspect(promise), /rejected/);
29+
30+
promise.catch(common.mustCall((err) => {
31+
assert.strictEqual(err.message, 'synchronous source text module');
32+
// Calling evaluate() again results in the same rejection synchronously.
33+
const promise2 = mod.evaluate();
34+
assert.match(inspect(promise2), /rejected/);
35+
promise2.catch(common.mustCall((err2) => {
36+
assert.strictEqual(err, err2);
37+
// The module is only evaluated once.
38+
assert.strictEqual(globalThis.callCount.syncError, 1);
39+
}));
40+
}));
41+
}
42+
43+
// Successful evaluation of a module without top-level await results in a
44+
// promise synchronously resolved to undefined.
45+
{
46+
globalThis.callCount.syncNamedExports = 0;
47+
const mod = new vm.SourceTextModule(`
48+
globalThis.callCount.syncNamedExports++;
49+
export const a = 1, b = 2;
50+
`);
51+
mod.linkRequests([]);
52+
mod.instantiate();
53+
const promise = mod.evaluate();
54+
assert.match(inspect(promise), /Promise { undefined }/);
55+
assert.strictEqual(mod.namespace.a, 1);
56+
assert.strictEqual(mod.namespace.b, 2);
57+
assert.strictEqual(globalThis.callCount.syncNamedExports, 1);
58+
promise.then(common.mustCall((value) => {
59+
assert.strictEqual(value, undefined);
60+
61+
// Calling evaluate() again results in the same resolved promise synchronously.
62+
const promise2 = mod.evaluate();
63+
assert.match(inspect(promise2), /Promise { undefined }/);
64+
assert.strictEqual(mod.namespace.a, 1);
65+
assert.strictEqual(mod.namespace.b, 2);
66+
promise2.then(common.mustCall((value) => {
67+
assert.strictEqual(value, undefined);
68+
// The module is only evaluated once.
69+
assert.strictEqual(globalThis.callCount.syncNamedExports, 1);
70+
}));
71+
}));
72+
}
73+
74+
{
75+
globalThis.callCount.syncDefaultExports = 0;
76+
// Modules with either named and default exports have the same behaviors.
77+
const mod = new vm.SourceTextModule(`
78+
globalThis.callCount.syncDefaultExports++;
79+
export default 42;
80+
`);
81+
mod.linkRequests([]);
82+
mod.instantiate();
83+
const promise = mod.evaluate();
84+
assert.match(inspect(promise), /Promise { undefined }/);
85+
assert.strictEqual(mod.namespace.default, 42);
86+
assert.strictEqual(globalThis.callCount.syncDefaultExports, 1);
87+
88+
promise.then(common.mustCall((value) => {
89+
assert.strictEqual(value, undefined);
90+
91+
// Calling evaluate() again results in the same resolved promise synchronously.
92+
const promise2 = mod.evaluate();
93+
assert.match(inspect(promise2), /Promise { undefined }/);
94+
assert.strictEqual(mod.namespace.default, 42);
95+
promise2.then(common.mustCall((value) => {
96+
assert.strictEqual(value, undefined);
97+
// The module is only evaluated once.
98+
assert.strictEqual(globalThis.callCount.syncDefaultExports, 1);
99+
}));
100+
}));
101+
}
102+
103+
// Successful evaluation of a module with top-level await results in a promise
104+
// that is fulfilled asynchronously with undefined.
105+
{
106+
globalThis.callCount.asyncEvaluation = 0;
107+
const mod = new vm.SourceTextModule(`
108+
globalThis.callCount.asyncEvaluation++;
109+
await Promise.resolve();
110+
export const a = 1;
111+
`);
112+
mod.linkRequests([]);
113+
mod.instantiate();
114+
const promise = mod.evaluate();
115+
assert.match(inspect(promise), /<pending>/);
116+
// Accessing the namespace before the promise is fulfilled throws ReferenceError.
117+
assert.throws(() => mod.namespace.a, { name: 'ReferenceError' });
118+
promise.then(common.mustCall((value) => {
119+
assert.strictEqual(value, undefined);
120+
assert.strictEqual(globalThis.callCount.asyncEvaluation, 1);
121+
122+
// Calling evaluate() again results in a promise synchronously resolved to undefined.
123+
const promise2 = mod.evaluate();
124+
assert.match(inspect(promise2), /Promise { undefined }/);
125+
assert.strictEqual(mod.namespace.a, 1);
126+
promise2.then(common.mustCall((value) => {
127+
assert.strictEqual(value, undefined);
128+
// The module is only evaluated once.
129+
assert.strictEqual(globalThis.callCount.asyncEvaluation, 1);
130+
}));
131+
}));
132+
}
133+
134+
// Rejection of a top-level await promise results in a promise that is
135+
// rejected asynchronously with the same reason.
136+
{
137+
globalThis.callCount.asyncRejection = 0;
138+
const mod = new vm.SourceTextModule(`
139+
globalThis.callCount.asyncRejection++;
140+
await Promise.reject(new Error("asynchronous source text module"));
141+
export const a = 1;
142+
`);
143+
mod.linkRequests([]);
144+
mod.instantiate();
145+
const promise = mod.evaluate();
146+
assert.match(inspect(promise), /<pending>/);
147+
// Accessing the namespace before the promise is fulfilled throws ReferenceError.
148+
assert.throws(() => mod.namespace.a, { name: 'ReferenceError' });
149+
promise.catch(common.mustCall((err) => {
150+
assert.strictEqual(err.message, 'asynchronous source text module');
151+
assert.strictEqual(globalThis.callCount.asyncRejection, 1);
152+
153+
// Calling evaluate() again results in a promise synchronously rejected
154+
// with the same reason.
155+
const promise2 = mod.evaluate();
156+
assert.match(inspect(promise2), /rejected/);
157+
promise2.catch(common.mustCall((err2) => {
158+
assert.strictEqual(err, err2);
159+
// The module is only evaluated once.
160+
assert.strictEqual(globalThis.callCount.asyncRejection, 1);
161+
}));
162+
}));
163+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Flags: --experimental-vm-modules
2+
'use strict';
3+
4+
// This tests the result of evaluating a vm.SyntheticModule with an async rejection
5+
// in the evaluation step.
6+
7+
const common = require('../common');
8+
9+
const assert = require('assert');
10+
// To make testing easier we just use the public inspect API. If the output format
11+
// changes, update this test accordingly.
12+
const { inspect } = require('util');
13+
const vm = require('vm');
14+
15+
// The promise _synchronously_ resolves to undefined, because for a synthethic module,
16+
// the evaluation operation can only either resolve or reject immediately.
17+
// In this case, the asynchronously rejected promise can't be handled from the outside,
18+
// so we'll catch it with the isolate-level unhandledRejection handler.
19+
// See https://tc39.es/ecma262/#sec-smr-Evaluate
20+
process.on('unhandledRejection', common.mustCall((err) => {
21+
assert.strictEqual(err.message, 'asynchronous source text module');
22+
}));
23+
24+
const mod = new vm.SyntheticModule(['a'], common.mustCall(async () => {
25+
await Promise.reject(new Error('asynchronous source text module'));
26+
}));
27+
28+
const promise = mod.evaluate();
29+
assert.match(inspect(promise), /Promise { undefined }/);
30+
// Accessing the uninitialized export of a synthetic module returns undefined.
31+
assert.strictEqual(mod.namespace.a, undefined);
32+
33+
promise.then(common.mustCall((value) => {
34+
assert.strictEqual(value, undefined);
35+
}));
36+
37+
// Calling evaluate() again results in a promise _synchronously_ resolved to undefined again.
38+
const promise2 = mod.evaluate();
39+
assert.match(inspect(promise2), /Promise { undefined }/);
40+
promise2.then(common.mustCall((value) => {
41+
assert.strictEqual(value, undefined);
42+
}));
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Flags: --experimental-vm-modules
2+
'use strict';
3+
4+
// This tests the result of evaluating a vm.SynthethicModule.
5+
// See https://tc39.es/ecma262/#sec-smr-Evaluate
6+
const common = require('../common');
7+
8+
const assert = require('assert');
9+
// To make testing easier we just use the public inspect API. If the output format
10+
// changes, update this test accordingly.
11+
const { inspect } = require('util');
12+
const vm = require('vm');
13+
14+
// Synthetic modules with a synchronous evaluation step evaluate to a promise synchronously
15+
// resolved to undefined.
16+
{
17+
const mod = new vm.SyntheticModule(['a'], common.mustCall(() => {
18+
mod.setExport('a', 42);
19+
}));
20+
const promise = mod.evaluate();
21+
assert.match(inspect(promise), /Promise { undefined }/);
22+
assert.strictEqual(mod.namespace.a, 42);
23+
24+
promise.then(common.mustCall((value) => {
25+
assert.strictEqual(value, undefined);
26+
27+
// Calling evaluate() again results in a promise synchronously resolved to undefined.
28+
const promise2 = mod.evaluate();
29+
assert.match(inspect(promise2), /Promise { undefined }/);
30+
promise2.then(common.mustCall((value) => {
31+
assert.strictEqual(value, undefined);
32+
}));
33+
}));
34+
}
35+
36+
// Synthetic modules with an asynchronous evaluation step evaluate to a promise
37+
// _synchronously_ resolved to undefined.
38+
{
39+
const mod = new vm.SyntheticModule(['a'], common.mustCall(async () => {
40+
const result = await Promise.resolve(42);
41+
mod.setExport('a', result);
42+
return result;
43+
}));
44+
const promise = mod.evaluate();
45+
assert.match(inspect(promise), /Promise { undefined }/);
46+
// Accessing the uninitialized export of a synthetic module returns undefined.
47+
assert.strictEqual(mod.namespace.a, undefined);
48+
49+
promise.then(common.mustCall((value) => {
50+
assert.strictEqual(value, undefined);
51+
52+
// Calling evaluate() again results in a promise _synchronously_ resolved to undefined again.
53+
const promise2 = mod.evaluate();
54+
assert.match(inspect(promise2), /Promise { undefined }/);
55+
promise2.then(common.mustCall((value) => {
56+
assert.strictEqual(value, undefined);
57+
}));
58+
}));
59+
}
60+
61+
// Synchronous error during the evaluation step of a synthetic module results
62+
// in a _synchronously_ rejected promise.
63+
{
64+
const mod = new vm.SyntheticModule(['a'], common.mustCall(() => {
65+
throw new Error('synchronous synthethic module');
66+
}));
67+
const promise = mod.evaluate();
68+
assert.match(inspect(promise), /rejected/);
69+
promise.catch(common.mustCall((err) => {
70+
assert.strictEqual(err.message, 'synchronous synthethic module');
71+
72+
// Calling evaluate() again results in a promise _synchronously_ rejected
73+
// with the same reason.
74+
const promise2 = mod.evaluate();
75+
assert.match(inspect(promise2), /rejected/);
76+
promise2.catch(common.mustCall((err2) => {
77+
assert.strictEqual(err, err2);
78+
}));
79+
}));
80+
}

0 commit comments

Comments
 (0)