Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
100 changes: 85 additions & 15 deletions test-parity/node-suite/vm/README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,89 @@
# 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 statically declared
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. The `context/properties/` group covers accessor forwarding,
descriptors, writable/configurable behavior, deletion, inheritance, circular
references, symbol keys, and own-key enumeration.
- `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` and `run-new-code-generation.ts` record
`eval`/`new Function` policy behavior, but Perry mismatches there are
deliberate runtime-generated-code AOT exclusions. They are 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.
- Proxy-backed sandboxes were evaluated from Node/Bun's selection. Node 26.5.0
completes deterministically, but Perry currently exits with `SIGSEGV`; the
crashing probe is excluded from this print-and-diff suite and requires
separate runtime crash coverage.
- 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);
15 changes: 15 additions & 0 deletions test-parity/node-suite/vm/compile-function/extension-precedence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as vm from "node:vm";

const context = vm.createContext({ fallback: "context" });
const first: any = { left: "left", shared: "first" };
const second: any = { right: "right", shared: "second" };
const fn: any = vm.compileFunction(
"return left + ':' + shared + ':' + right + ':' + fallback",
[],
{ parsingContext: context, contextExtensions: [first, second] },
);

console.log("precedence:", fn());
first.shared = "changed-first";
second.shared = "changed-second";
console.log("live precedence:", fn());
13 changes: 13 additions & 0 deletions test-parity/node-suite/vm/compile-function/syntax-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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("unexpected token", () => vm.compileFunction("return )"));
shape("unterminated body", () => vm.compileFunction("if (true) {"));
8 changes: 8 additions & 0 deletions test-parity/node-suite/vm/compile-function/to-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as vm from "node:vm";

const fn: any = vm.compileFunction("return left + right", ["left", "right"]);
const source = Function.prototype.toString.call(fn);

console.log("shape:", source.startsWith("function (left, right)"));
console.log("body:", source.includes("return left + right"));
console.log("metadata:", fn.name, fn.length, Object.hasOwn(fn, "prototype"));
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,
);
18 changes: 18 additions & 0 deletions test-parity/node-suite/vm/context/function-bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as vm from "node:vm";

const sandbox: any = {};
const context = vm.createContext(sandbox);
console.log(
"declare:",
vm.runInContext(
"function declared() { return 4; } var expression = function () { return 5; }; let lexical = () => 6; declared() + expression() + lexical()",
context,
),
);
console.log(
"properties:",
typeof sandbox.declared,
typeof sandbox.expression,
typeof sandbox.lexical,
);
console.log("repeat:", vm.runInContext("declared() + expression()", context));
17 changes: 17 additions & 0 deletions test-parity/node-suite/vm/context/global-object-forwarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as vm from "node:vm";

const sandbox: any = { value: 1 };
const context = vm.createContext(sandbox);
const contextGlobal: any = vm.runInContext("this", context);

console.log("identity:", contextGlobal === sandbox);
contextGlobal.value = 4;
contextGlobal.added = 5;
console.log("outer write:", sandbox.value, sandbox.added);
sandbox.value = 6;
console.log(
"inner read:",
contextGlobal.value,
vm.runInContext("value", context),
);
console.log("own:", Object.hasOwn(contextGlobal, "added"));
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);
Loading
Loading