Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 78 additions & 15 deletions test-parity/node-suite/vm/README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,82 @@
# node:vm parity fixtures
# `node:vm` granular parity

This directory covers the default Node `node:vm` surface that is visible
without `--experimental-vm-modules`: import and require shapes, callable export
metadata, `vm.constants`, `process.getBuiltinModule("vm")`, `vm.isContext({})`,
and the narrowed deterministic execution subset for `Script`, context-backed
sandbox mutation/isolation, and `compileFunction`.
This directory tests Perry's documented `node:vm` surface with independent,
print-and-diff fixtures. Node 26.5.0 is the oracle. Every executable source
passed to `Script`, `runIn*Context`, or `compileFunction` is a literal in the
fixture, so failures in the core groups are not hidden dependencies on runtime
source discovery.

The `modules/` fixtures opt into Node's `--experimental-vm-modules` flag and
Perry's matching `PERRY_EXPERIMENTAL_VM_MODULES=1` gate to cover deterministic
`SourceTextModule`/`SyntheticModule` lifecycle behavior separately from the
default surface.
## Coverage

Intentionally open leaves:
- `api/`, `imports/`, and `require/`: export identity, classes, constants,
callable metadata, property descriptors, ESM/CommonJS shape, and the builtin
module lookup path.
- `context/` and `validation/`: `createContext()`/`isContext()`, object and
array sandboxes, `DONT_CONTEXTIFY`, name/origin and code-generation
validation, string-code generation policy, and deterministic `microtaskMode`
draining.
- `execution/`: main-context bindings, isolated new contexts, persistent context
mutation/lexicals, receiver identity, repeated Script execution, and
`createScript()` construction.
- `script/` and `metadata/`: constructor/run option validation, stable error
class/code checks, filename/offset presence, `displayErrors`, source-map URL,
cached-data acceptance/rejection, and deprecated produced-cache properties.
- `compile-function/`: parameters, parsing contexts, context extensions,
arguments, receiver behavior, validation, and portable cache observations.
- `cross-context/`: built-in/prototype identity, structured values, descriptors,
errors, and promises without raw inspector or engine-specific stack output.
- `modules/`: the separately gated, documented `Module`, `SourceTextModule`, and
`SyntheticModule` lifecycle, validation, namespace, linking, evaluation, and
cached-data subset. These fixtures use `--experimental-vm-modules` and
`PERRY_EXPERIMENTAL_VM_MODULES=1` explicitly.

- Full VM module parsing/evaluation beyond deterministic lifecycle fixtures:
#3132, #3133
- vm.constants deeper context-loader behavior: #3283
- Exact V8-backed heap accounting for `measureMemory()` values
## Upstream selection evidence

The selection was reviewed on 2026-07-16 against these primary repositories:

- Node.js `v26.5.0` (`bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`):
`test/parallel/test-vm-*`, `test/es-module/test-vm-*`,
`test/sequential/test-vm-*`, and `doc/api/vm.md`.
- Deno main (`c99e6904d8e297712ba859a64bbe848532d8f90f`):
`tests/unit_node/vm_test.ts`, `tests/specs/node/vm_*`, and
`ext/node/polyfills/vm.js`.
- Bun main (`0ecd508247c7e99477717389a6cad44552cac023`): `test/js/node/vm/`, its
vendored `test/js/node/test/parallel/test-vm-*` selection, and
`src/js/node/vm.ts`.

The fixtures intentionally reduce those upstream assertions to stable semantic
output: booleans, values, property flags, error names, and error codes. They do
not compare absolute paths, raw error messages, full stacks, cache bytes, or
object inspection order.

## AOT boundary and stopping evidence

Perry documents a V8-free, narrowed constant-source VM evaluator. Consequently:

- `code-generation-strings.ts` records `eval`/`new Function` policy behavior,
but a Perry mismatch there is a deliberate runtime-generated-code AOT
exclusion. It is kept separate from literal-source context behavior.
- Literal-source mutation, isolation, receiver, lexical, option-validation,
error, compile-function, and cross-context failures are genuine gaps in the
currently claimed VM subset, not dynamic-source discovery failures.
- Dynamic import callbacks, `importModuleDynamically`, default/custom loaders,
network imports, and import-meta initialization require separate loader work.
- Inspector/debug breaks, signals, `breakOnSigint`, timeout races/infinite
loops, escaped promise timeouts, and parse-abort behavior are excluded because
they need process control or can hang the granular runner.
- Context `name` and `origin` type validation and acceptance are covered, but
their deeper metadata effects are inspector-facing and stay with inspector
integration work.
- `measureMemory()` exact values, GC/weak-reference/leak/stress tests, and code
cache byte identity are engine- or environment-specific. Only portable shapes
and acceptance state are covered.
- WebAssembly code-generation execution, code-cache corruption fuzzing, exact
syntax/stack strings, and source-map path formatting remain separate risky
categories.
- Node 26.5.0 aborts the process for the invalid `compileFunction()` parameter
name `"a-b"` on the tested macOS build, so that unsafe oracle case is
excluded; type validation around it remains covered.

The remaining Node/Deno/Bun cases are therefore redundant with these semantic
contracts, outside Perry's documented AOT model, experimental loader work, or
belong to one of the unreliable categories above.
30 changes: 30 additions & 0 deletions test-parity/node-suite/vm/api/descriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as vm from "node:vm";

function descriptor(target: object, key: string) {
let owner: any = target;
let depth = 0;
while (owner && !Object.prototype.hasOwnProperty.call(owner, key)) {
owner = Object.getPrototypeOf(owner);
depth++;
}
const value = owner && Object.getOwnPropertyDescriptor(owner, key);
console.log(
key + ":",
depth,
value?.enumerable,
value?.configurable,
value?.writable,
typeof value?.value,
);
}

descriptor(vm, "Script");
descriptor(vm, "createContext");
descriptor(vm, "constants");
const script = new vm.Script("");
descriptor(script, "runInContext");
descriptor(script, "createCachedData");
console.log(
"prototype constructor:",
vm.Script.prototype.constructor === vm.Script,
);
31 changes: 31 additions & 0 deletions test-parity/node-suite/vm/api/exports-and-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import vm, * as namespace from "node:vm";

const expected = [
"Script",
"compileFunction",
"constants",
"createContext",
"createScript",
"isContext",
"measureMemory",
"runInContext",
"runInNewContext",
"runInThisContext",
];

console.log(
"exports:",
expected.map((key) => key + ":" + typeof (vm as any)[key]).join(","),
);
console.log("namespace default:", namespace.default === vm);
console.log(
"script class:",
typeof vm.Script,
vm.Script.name,
vm.Script.length,
);
console.log("constants frozen:", Object.isFrozen(vm.constants));
console.log(
"constants null prototype:",
Object.getPrototypeOf(vm.constants) === null,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as vm from "node:vm";

const fn: any = vm.compileFunction(
"return arguments.length + ':' + a + ':' + b + ':' + this.marker",
["a", "b"],
);

console.log("length:", fn.length);
console.log("missing:", fn.call({ marker: "m" }, 1));
console.log("extra:", fn.call({ marker: "e" }, 1, 2, 3));
13 changes: 13 additions & 0 deletions test-parity/node-suite/vm/compile-function/context-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as vm from "node:vm";

const context = vm.createContext({ shared: 10 });
const extension = { shared: 20, extra: 3 };
const fn: any = vm.compileFunction("return shared + extra + arg", ["arg"], {
parsingContext: context,
contextExtensions: [extension],
});

console.log("result:", fn(4));
extension.extra = 5;
console.log("live extension:", fn(4));
console.log("length name:", fn.length, typeof fn.name, fn.name);
32 changes: 32 additions & 0 deletions test-parity/node-suite/vm/compile-function/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as vm from "node:vm";

function shape(label: string, fn: () => unknown) {
try {
fn();
console.log(label + ": ok");
} catch (error: any) {
console.log(label + ":", error.name, error.code || "-");
}
}

shape("missing code", () => (vm.compileFunction as any)());
shape("params null", () => vm.compileFunction("", null as any));
shape("param type", () => vm.compileFunction("", [1 as any]));
shape("options null", () => vm.compileFunction("", [], null as any));
shape("filename", () => vm.compileFunction("", [], { filename: null as any }));
shape(
"lineOffset",
() => vm.compileFunction("", [], { lineOffset: "1" as any }),
);
shape(
"columnOffset",
() => vm.compileFunction("", [], { columnOffset: null as any }),
);
shape(
"extensions null",
() => vm.compileFunction("", [], { contextExtensions: null as any }),
);
shape(
"extensions member",
() => vm.compileFunction("", [], { contextExtensions: [1 as any] }),
);
21 changes: 21 additions & 0 deletions test-parity/node-suite/vm/context/code-generation-strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as vm from "node:vm";

const disabled = vm.createContext({}, {
codeGeneration: { strings: false, wasm: true },
});
try {
console.log("eval:", vm.runInContext("eval('1')", disabled));
} catch (error: any) {
console.log("eval:", error.name, error.code || "-");
}
try {
console.log(
"Function:",
vm.runInContext("new Function('return 1')()", disabled),
);
} catch (error: any) {
console.log("Function:", error.name, error.code || "-");
}

const enabled = vm.createContext({}, { codeGeneration: { strings: true } });
console.log("enabled eval:", vm.runInContext("eval('2 + 3')", enabled));
11 changes: 11 additions & 0 deletions test-parity/node-suite/vm/context/create-context-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as vm from "node:vm";

const array: any[] = [1, 2];
console.log("identity:", vm.createContext(array) === array);
console.log("marker:", vm.isContext(array));
console.log("length:", array.length);
console.log(
"execution:",
vm.runInContext("length = length + 1; length", array),
array.length,
);
17 changes: 17 additions & 0 deletions test-parity/node-suite/vm/context/create-context-shapes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as vm from "node:vm";

const omitted: any = vm.createContext();
const object: any = { value: 1 };

console.log("omitted:", typeof omitted, vm.isContext(omitted));
console.log(
"object identity:",
vm.createContext(object) === object,
vm.isContext(object),
);
console.log(
"repeat identity:",
vm.createContext(object) === object,
vm.isContext(object),
);
console.log("plain markers:", vm.isContext({}), vm.isContext([]));
15 changes: 15 additions & 0 deletions test-parity/node-suite/vm/context/dont-contextify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as vm from "node:vm";

const context: any = vm.createContext(vm.constants.DONT_CONTEXTIFY);
console.log("marker:", vm.isContext(context));
console.log("identity:", context === globalThis);
console.log("receiver exposed:", vm.runInContext("this", context) === context);
console.log(
"global identity:",
vm.runInContext("this === globalThis", context) === true,
);
console.log(
"mutation:",
vm.runInContext("value = 3; value", context),
context.value,
);
13 changes: 13 additions & 0 deletions test-parity/node-suite/vm/context/microtask-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as vm from "node:vm";

const sandbox: any = { value: 1 };
vm.runInNewContext("Promise.resolve().then(() => value = 2)", sandbox, {
microtaskMode: "afterEvaluate",
});
console.log("after evaluate:", sandbox.value);

const context: any = vm.createContext({ value: 3 }, {
microtaskMode: "afterEvaluate",
});
vm.runInContext("Promise.resolve().then(() => value = 4)", context);
console.log("context option:", context.value);
39 changes: 39 additions & 0 deletions test-parity/node-suite/vm/context/options-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as vm from "node:vm";

function shape(label: string, fn: () => unknown) {
try {
fn();
console.log(label + ": ok");
} catch (error: any) {
console.log(label + ":", error.name, error.code || "-");
}
}

shape(
"codeGeneration null",
() => vm.createContext({}, { codeGeneration: null as any }),
);
shape(
"strings type",
() => vm.createContext({}, { codeGeneration: { strings: 1 as any } }),
);
shape(
"wasm type",
() => vm.createContext({}, { codeGeneration: { wasm: "no" as any } }),
);
shape(
"microtask bad",
() => vm.createContext({}, { microtaskMode: "bad" as any }),
);
shape(
"microtask type",
() => vm.createContext({}, { microtaskMode: 1 as any }),
);
shape(
"name valid",
() => vm.createContext({}, { name: "fixture" }),
);
shape(
"origin valid",
() => vm.createContext({}, { origin: "vm://fixture" }),
);
21 changes: 21 additions & 0 deletions test-parity/node-suite/vm/cross-context/builtin-identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as vm from "node:vm";

const context = vm.createContext({});
const names = [
"Object",
"Array",
"Error",
"TypeError",
"Promise",
"Map",
"Set",
];
for (const name of names) {
const value = vm.runInContext(name, context);
console.log(name + ":", typeof value, value === (globalThis as any)[name]);
}
console.log(
"prototype identity:",
vm.runInContext("Object.prototype", context) === Object.prototype,
vm.runInContext("Array.prototype", context) === Array.prototype,
);
21 changes: 21 additions & 0 deletions test-parity/node-suite/vm/cross-context/descriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as vm from "node:vm";

const context = vm.createContext({});
const descriptor: any = vm.runInContext(
"Object.getOwnPropertyDescriptor({ value: 1 }, 'value')",
context,
);

console.log(
"flags:",
descriptor?.enumerable,
descriptor?.configurable,
descriptor?.writable,
);
console.log("value:", typeof descriptor?.value, descriptor?.value);
console.log(
"prototype:",
descriptor === undefined
? false
: Object.getPrototypeOf(descriptor) === Object.prototype,
);
13 changes: 13 additions & 0 deletions test-parity/node-suite/vm/cross-context/promises.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as vm from "node:vm";

const context = vm.createContext({});
const promise: any = vm.runInContext("Promise.resolve({ value: 7 })", context);
console.log("thenable:", typeof promise?.then, promise instanceof Promise);
const value = await promise;
console.log(
"resolved:",
value?.value,
value === undefined
? false
: Object.getPrototypeOf(value) === Object.prototype,
);
Loading
Loading