Files
scriptc/tests/harness/errors.test.ts
T

1988 lines
71 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Error-object behavior Node cannot oracle — scriptc-only.
*
* The differential corpus (1300–1306) covers everything observable in
* lockstep with Node: construction/fields/toString, subclasses, typed-catch
* narrowing, async rejections, runtime failures caught as instances. What
* lives here is the uncaught STDERR contract: Node prints a source excerpt
* and a stack trace (and its display shows the CONSTRUCTOR's name where it
* differs from .name); scriptc prints one `Uncaught <toString>` line —
* SEMANTICS.md divergence 11. These pins keep that line's shape exact.
*
* SCRIPTC_SAN=1 builds these with ASan + the RC audit like every harness lane.
*/
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { describe, expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
const execFileAsync = promisify(execFile);
const repoRoot = join(import.meta.dirname, "../..");
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
const sanitize = process.env["SCRIPTC_SAN"] === "1";
interface RunResult {
stdout: string;
stderr: string;
exitCode: number;
}
/** Compiles an inline program and runs the binary, tolerating nonzero exit.
* `ext` picks the frontend lane — "cjs" for JS-only shapes (deferred
* fences that tsc's arity families would reject in TS). `extraFiles`
* materialize beside the entry (multi-module shapes: the CJS link-error
* pins import sibling .cjs modules). */
async function compileAndRun(
name: string,
source: string,
ext = "ts",
extraFiles: Record<string, string> = {},
): Promise<RunResult> {
const key = createHash("sha256")
.update(source)
.update(JSON.stringify(extraFiles))
.update(sanitize ? "san" : "plain")
.digest("hex")
.slice(0, 16);
// Multi-module programs live OUTSIDE node_modules: the cache dir sits
// under it, where the resolver classifies sibling modules as npm files
// (correctly — for real packages) and the relative import stops
// resolving into the program.
const outDir =
Object.keys(extraFiles).length > 0
? join(tmpdir(), `scriptc-errors-${key}`)
: join(cacheDir, `errors-${key}`);
mkdirSync(outDir, { recursive: true });
const file = join(outDir, `${name}.${ext}`);
writeFileSync(file, source);
for (const [extraName, text] of Object.entries(extraFiles)) {
writeFileSync(join(outDir, extraName), text);
}
const result = await compile(file, {
outPath: join(outDir, name),
outDir,
sanitize,
backend: "llvm",
});
if (!result.ok) {
throw new Error(
"errors program failed to compile:\n" +
result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
);
}
try {
const { stdout, stderr } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" });
return { stdout, stderr: stripAsanFiberWarning(stderr), exitCode: 0 };
} catch (err) {
const e = err as { code?: unknown; stdout?: string; stderr?: string };
if (typeof e.code !== "number") throw err;
return {
stdout: e.stdout ?? "",
stderr: stripAsanFiberWarning(e.stderr ?? ""),
exitCode: e.code,
};
}
}
/** Linux ASan prints a once-per-process warning to stderr the first time a
* fiber swapcontext()s (the interceptor has no off switch; Apple's ASan
* never intercepts ucontext, so the macOS lanes never see it). Sanitizer
* diagnostic noise, never program output — dropped before any stderr
* expectation, the RC-audit-skip-notice pattern from differential.test.ts. */
function stripAsanFiberWarning(s: string): string {
return s.replace(
/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext.*\n/gm,
"",
);
}
describe(`uncaught error objects (scriptc-only${sanitize ? ", sanitized" : ""})`, () => {
test("an uncaught builtin error prints Uncaught name: message", async () => {
const r = await compileAndRun(
"uncaught-builtin",
`console.log("before");
throw new TypeError("boom");
`,
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("before\n");
expect(r.stderr).toBe("Uncaught TypeError: boom\n");
});
test("a subclass prints its name PROPERTY — 'Error' when never assigned", async () => {
// Node's uncaught display would show the constructor name (PlainSub)
// even though .name is "Error"; we print the property — divergence 11.
const r = await compileAndRun(
"uncaught-subclass",
`class PlainSub extends Error {}
throw new PlainSub("unnamed sub");
`,
);
expect(r.exitCode).toBe(1);
expect(r.stderr).toBe("Uncaught Error: unnamed sub\n");
});
test("toString's empty-half rules shape the line", async () => {
const empty = await compileAndRun("uncaught-empty", `throw new Error();\n`);
expect(empty.exitCode).toBe(1);
expect(empty.stderr).toBe("Uncaught Error\n");
const nameless = await compileAndRun(
"uncaught-nameless",
`const e = new Error("just the message");
e.name = "";
throw e;
`,
);
expect(nameless.exitCode).toBe(1);
expect(nameless.stderr).toBe("Uncaught just the message\n");
});
test("an uncaught checked-dynamic primitive prints its value, like Node", async () => {
// Node prints the thrown primitive itself (after its source excerpt);
// the value must never be lost behind a placeholder.
const text = await compileAndRun(
"uncaught-unknown-string",
`console.log("before");
const v: unknown = process.argv.length > 99 ? 0 : "plain";
throw v;
`,
);
expect(text.exitCode).toBe(1);
expect(text.stdout).toBe("before\n");
expect(text.stderr).toBe("Uncaught plain\n");
const num = await compileAndRun(
"uncaught-unknown-number",
`const v: unknown = process.argv.length > 99 ? "x" : 42;
throw v;
`,
);
expect(num.exitCode).toBe(1);
expect(num.stderr).toBe("Uncaught 42\n");
});
test("an unhandled ERROR rejection renders name: message too", async () => {
const r = await compileAndRun(
"unhandled-error-rejection",
`async function fail(): Promise<void> {
throw new RangeError("nobody awaits this");
}
fail();
console.log("sync done");
`,
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("sync done\n");
expect(r.stderr).toBe("Unhandled promise rejection: RangeError: nobody awaits this\n");
});
});
// `x as Sub` on a class value is a CHECKED downcast, and `x!` / `u as Arm`
// on a union value is a checked extraction: a stored value outside the
// target throws a catchable TypeError where Node's erasure would let the
// value flow on. The type and message are scriptc's contract (corpus 4432
// pins the Node-observable part: the failure is a TypeError raised before
// any later read). Plain operands are tested inline at the use and call
// operands through the interned helper; both must fail identically.
describe("checked class downcasts and extractions", () => {
test("failed downcasts throw the TypeError naming the target class", async () => {
const r = await compileAndRun(
"checked-downcast-messages",
`class Base {
kind = 0;
}
class Sub extends Base {
text = "sub";
}
class Other {
tag = "other";
}
function direct(b: Base): string {
return (b as Sub).text;
}
function optional(b: Base | undefined): string {
return (b as Sub).text;
}
function mixed(v: Base | Other): string {
return (v as Sub).text;
}
function make(b: Base | undefined): Base | undefined {
return b;
}
function called(b: Base | undefined): string {
return (make(b) as Sub).text;
}
function asserted(b: Base | undefined): number {
return b!.kind;
}
function assertedCall(b: Base | undefined): number {
return make(b)!.kind;
}
const runs: Array<() => unknown> = [
() => direct(new Sub()),
() => direct(new Base()),
() => optional(undefined),
() => optional(new Base()),
() => mixed(new Other()),
() => called(new Base()),
() => called(undefined),
() => asserted(new Base()),
() => asserted(undefined),
() => assertedCall(undefined),
];
for (const run of runs) {
try {
console.log("ok", run());
} catch (e) {
console.log(e instanceof TypeError, String(e));
}
}
direct(new Base());
`,
);
expect(r.stdout).toBe(
[
"ok sub",
"true TypeError: Value is not an instance of 'Sub'",
"true TypeError: Value is not an instance of 'Sub'",
"true TypeError: Value is not an instance of 'Sub'",
"true TypeError: Value is not an instance of 'Sub'",
"true TypeError: Value is not an instance of 'Sub'",
"true TypeError: Value is not an instance of 'Sub'",
"ok 0",
"true TypeError: undefined is not representable in the target union (a value narrowed or asserted past it still held it)",
"true TypeError: undefined is not representable in the target union (a value narrowed or asserted past it still held it)",
"",
].join("\n"),
);
expect(r.stderr).toBe("Uncaught TypeError: Value is not an instance of 'Sub'\n");
expect(r.exitCode).toBe(1);
});
});
// `e as C` on a catch binding is a CHECKED cast: a payload outside C's
// hierarchy throws a catchable TypeError where Node's erasure would read
// undefined off the value — dynCheck's documented trust-but-verify stance
// extended to exception payloads. This is exactly the case that cannot be
// a differential test; pinned so the divergence stays deliberate.
describe("checked catch-binding casts", () => {
test("a mismatched `as Error` throws the catchable TypeError", async () => {
const r = await compileAndRun(
"caught-check-mismatch",
`try {
throw "stringy";
} catch (err) {
try {
console.log((err as Error).message);
} catch (inner) {
if (inner instanceof TypeError) console.log("checked:", inner.message);
}
}
`,
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe("checked: caught value is not an instance of Error (checked cast)\n");
expect(r.stderr).toBe("");
});
test("an uncaught mismatch propagates as the TypeError", async () => {
const r = await compileAndRun(
"caught-check-uncaught",
`try {
throw 42;
} catch (err) {
console.log((err as Error).message);
}
`,
);
expect(r.exitCode).toBe(1);
expect(r.stderr).toBe(
"Uncaught TypeError: caught value is not an instance of Error (checked cast)\n",
);
});
test("an EFFECTFUL surplus argument is a named deferred fence (JS lane)", async () => {
// Effect-free surplus args drop at compile time, JS-exact (corpus
// 1760 — the test-string-decoder writeSequences shape). An effectful
// one has no evaluation slot in the completed call, so instead of
// skipping it without a diagnostic the build defers a named SC1090 to the
// call site. JS-only: tsc's arity errors gate the TS lane.
const r = await compileAndRun(
"surplus-args-effectful",
`'use strict';
function two(a, b) {
return a + b;
}
function effectful() {
console.log('side effect JS would run');
return 0;
}
console.log(two(1, 2, effectful()));
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toMatch(
/^Uncaught Error: surplus arguments with side effects .* are not supported yet \[SC1090 at .*surplus-args-effectful\.cjs:9\]\n$/,
);
});
test.each([
{
label: "Array.from",
name: "array-from-length-elements",
source: `const arr = Array.from({ length: 2 }, () => new Date(0));`,
},
{
label: "flatMap",
name: "flatmap-elements",
source: `const arr = [1].flatMap(() => new Date(0));`,
},
{
label: "tuple map",
name: "tuple-map-elements",
source: `/** @type {[number]} */
const tuple = [1];
const arr = tuple.map(() => new Date(0));`,
},
])("$label preserves checked callback results (JS lane)", async ({ name, source }) => {
const program = source + "\nconsole.log(arr.length, arr[0].getTime());\n";
const r = await compileAndRun(name, program, "js");
const reference = await execFileAsync(process.execPath, ["--eval", program], {
encoding: "utf8",
});
expect(r).toEqual({ stdout: reference.stdout, stderr: reference.stderr, exitCode: 0 });
});
test.each([
["an argument", "make(Upper, { objectMode: true });"],
["a spread argument", "spread(Upper, [{ objectMode: true }]);"],
])(
"%s through an untyped factory to an inherited stream constructor is a named deferred fence (JS lane)",
async (_, call) => {
// The constructor a ctor-less stream subclass inherits takes no
// arguments here; typed construction refuses them at compile time,
// and construction from untyped code refuses them at run time
// instead of constructing without them.
const r = await compileAndRun(
`inherited-stream-ctor-${call.startsWith("make") ? "args" : "spread"}`,
`'use strict';
const { Transform } = require('node:stream');
class Upper extends Transform {}
function make(F, options) { return new F(options); }
function bare(F) { return new F(); }
function spread(F, args) { return new F(...args); }
console.log('bare', bare(Upper) instanceof Upper, spread(Upper, []) instanceof Upper);
${call}
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("bare true true\n");
expect(r.stderr).toMatch(
/^Uncaught Error: new Upper with arguments through an inherited stream constructor .* is not supported yet \[SC1090 at .*inherited-stream-ctor-\w+\.cjs:3\]\n$/,
);
},
);
test("extending a property-assigned class ABOVE its assignment is a named deferred fence (JS lane)", async () => {
// The property-assigned class-expression family (corpus 2032/2033
// pins what lowers): `exports.O = class extends exports.I {}` with
// the base assigned BELOW would extend undefined in Node (TypeError
// at the derived statement) — the source-order guard defers a named
// fence instead of pinning a base that doesn't exist yet.
const r = await compileAndRun(
"extends-above-assignment",
`'use strict';
exports.O = class extends exports.I {};
exports.I = class {};
console.log('unreached');
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toMatch(
/^Uncaught Error: extending 'exports\.I' above the statement that assigns it .* \[SC1090 at .*extends-above-assignment\.cjs:2\]\n$/,
);
});
test("a REASSIGNED property-assigned class stays fenced instead of extending the wrong base (JS lane)", async () => {
// Two `exports.I = class …` assignments make the binding dynamic (the
// runtime base is whichever ran last). The second assignment's
// class-value flow fence fires first (nominal classval slots), and the
// extends resolver refuses to pin either class — the program fences
// instead of extending the first assignment's class.
const r = await compileAndRun(
"extends-reassigned-property",
`'use strict';
exports.I = class { a() { return 1; } };
exports.I = class { a() { return 2; } };
exports.O = class extends exports.I {};
console.log(new exports.O().a());
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toMatch(
/^Uncaught Error: .* \[SC1090 at .*extends-reassigned-property\.cjs:\d+\]\n$/,
);
});
test("a non-callable constructor field shadows its method and throws at the call (JS lane)", async () => {
const r = await compileAndRun(
"leaf-shadowing-class-defers",
`'use strict';
console.log("before");
class X {
constructor() {
this.m = "not callable";
}
m() {
console.log("method");
}
}
const x = new X();
x.m();
console.log("unreachable");
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("before\n");
expect(r.stderr).toBe("Uncaught TypeError: x.m is not a function\n");
});
test("a callable constructor field shadows an inherited method (JS lane)", async () => {
const r = await compileAndRun(
"derived-shadowing-class-defers",
`'use strict';
console.log("before");
class A {
foo() {
return 4;
}
}
class B extends A {
constructor() {
super();
this.foo = () => 3;
}
}
const i = new B();
console.log(i.foo());
`,
"cjs",
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe("before\n3\n");
expect(r.stderr).toBe("");
});
test("destructuring a SURFACED builtin global's member is a named fence at the pattern (JS lane)", async () => {
// `const { max } = Math` would detach a member whose lowering is
// keyed to its receiver (Math.max) — the pattern fences by member
// and global name instead of binding a token whose calls fail late.
// Corpus 2558 pins the OPAQUE globals, where members DO bind tokens.
const r = await compileAndRun(
"destructure-surfaced-global",
`'use strict';
const { max } = Math;
console.log(max(1, 2));
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toMatch(
/^Uncaught Error: destructuring the member 'max' of the builtin global 'Math' \(a detached member loses its receiver-keyed lowering — call it through the global\) is not supported yet \[SC1031 at .*destructure-surfaced-global\.cjs:2\]\n$/,
);
});
test("an OPAQUE global's destructured member binds a token; uses fence lazily per site (JS lane)", async () => {
// The node-suite webcrypto prologue: `const { subtle } =
// globalThis.crypto` binds the member identity token, so the program
// RUNS past the destructure (the eager pattern fence used to kill it
// at that line) and each use meets its own named fence.
const r = await compileAndRun(
"destructure-opaque-global",
`'use strict';
const { subtle } = globalThis.crypto;
console.log('bound', !!subtle);
subtle.digest('SHA-256');
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("bound true\n");
expect(r.stderr).toMatch(/^Uncaught TypeError: subtle\.digest is not a function\n$/);
});
test("a 7000-level expression is a named deferred fence, not a stack overflow (JS lane)", async () => {
// The binderBinaryExpressionStress depth: an AST deep enough that the
// PREFLIGHT walks (the checker facade's prefetch sweep, the require
// scans) used to overflow the JS stack as a RangeError ICE before the
// expression lowering's 200-level SC1090 fence could fire. Every
// whole-file walk is iterative now (walkPreorder), so the JS lane
// compiles and defers the named fence to the statement — the TS lane's
// compile-time twin is the deep-nesting-preflight diagnostics fixture.
// Parens carry the depth (a real 6500-term chain spends minutes in
// per-node type queries; the walk that used to crash is shape-blind).
const r = await compileAndRun(
"deep-nesting-chain",
`'use strict';
let x = ${"(".repeat(7000)}1${")".repeat(7000)};
console.log('after');
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toMatch(
/^Uncaught Error: expressions nested deeper than 200 levels are not supported yet \[SC1090 at .*deep-nesting-chain\.cjs:2\]\n$/,
);
});
test("process.on with a prototype-member event name fences by NAME", async () => {
// '__proto__' used to fall into the compiler's own signal table —
// `{ SIGINT: 2 }["__proto__"]` answered Object.prototype, which rode
// a numLit into the C as `[object Object]` (invalid C, cc.js death).
// The own() lookup makes it what every other unsupported process
// event is: a named deferred fence. Corpus 1761 pins the EMITTER
// side, where these names work and match Node byte-exactly.
const r = await compileAndRun(
"process-on-proto",
`'use strict';
process.on('__proto__', () => {});
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toMatch(
/^Uncaught Error: 'process\.on\("__proto__", \.\.\.\)' is part of the standard library types but has no scriptc lowering yet \[SC2020 at .*process-on-proto\.cjs:2\]\n$/,
);
});
});
// Node's CJS named-import LINK error: a named import of an export the CJS
// lexer cannot detect kills the graph before ANY module evaluates, and
// scriptc compiles the program to exactly that startup throw. The corpus
// (1613/1616/1618/1619) pins stdout+exit differentially; what lives here
// is the MESSAGE — Node's exact SyntaxError text, both flavors, hint
// construction included — which the exit-1 stdout-only contract cannot see.
describe("CJS named-import link SyntaxError messages", () => {
test("an entry-level import gets Node's CommonJS hint form, ` as ` rewritten", async () => {
const r = await compileAndRun(
"cjs-link-flavored",
`import { a as z, vis } from './table.cjs';
console.log('never runs', z, vis);
`,
"mjs",
{ "table.cjs": "'use strict';\nconst v = 5;\nmodule.exports = { vis: v, a: 7 };\n" },
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toBe(
"Uncaught SyntaxError: Named export 'a' not found. The requested module './table.cjs' is a CommonJS module, which may not support all module.exports as named exports.\n" +
"CommonJS modules can always be imported via the default export, for example using:\n" +
"\n" +
"import pkg from './table.cjs';\n" +
"const { a: z, vis } = pkg;\n" +
"\n",
);
});
test("a failure below the entry keeps V8's generic wording", async () => {
// module_job.js rewrites the message only when the failing specifier
// is one of the ROOT module's own requests — mid.mjs's is not.
const r = await compileAndRun(
"cjs-link-generic",
`import { a } from './mid.mjs';
console.log('never runs', a);
`,
"mjs",
{
"mid.mjs": "export { a } from './table.cjs';\n",
"table.cjs": "'use strict';\nmodule.exports = { a: 7 };\n",
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toBe(
"Uncaught SyntaxError: The requested module './table.cjs' does not provide an export named 'a'\n",
);
});
});
// Node's strip-only TypeScript loader keeps ordinary named imports in the
// ESM request graph. If the requested declaration is type-only, Node refuses
// during linking even though the TypeScript checker accepts the import. The
// corpus pins stdout and exit; these assertions pin scriptc's exact uncaught
// SyntaxError message and the local-name ordering used to choose the first
// failure.
describe("native ESM type-import link SyntaxError messages", () => {
test("an unqualified interface import fails before any module evaluates", async () => {
const r = await compileAndRun(
"esm-type-link",
`import { value, Shape as MissingShape } from './types.ts';
console.log('never runs', value);
`,
"ts",
{
"types.ts": `export interface Shape { value: number }
export const value = 7;
console.log('types evaluated');
`,
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toBe(
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Shape'\n",
);
});
test("a child re-export failure wins before the parent import is linked", async () => {
const r = await compileAndRun(
"esm-type-link-reexport",
`import { Zed as zed, Aa as aaa, value } from './middle.ts';
console.log('never runs', value);
`,
"ts",
{
"middle.ts": `export { Shape as Zed, Other as Aa, value } from './types.ts';
console.log('middle evaluated');
`,
"types.ts": `export interface Shape { value: number }
export type Other = string;
export const value = 9;
console.log('types evaluated');
`,
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toBe(
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Shape'\n",
);
});
test("direct imports choose the first missing binding by local name", async () => {
const r = await compileAndRun(
"esm-type-link-order",
`import { Shape as zed, Other as aaa } from './types.ts';
console.log('never runs');
`,
"ts",
{
"types.ts": `export interface Shape { value: number }
export type Other = string;
`,
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toBe(
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Other'\n",
);
});
test("native compilation retains the ESM link failure", async () => {
const r = await compileAndRun(
"esm-type-link-dynamic",
`// @dynamic
import { Shape as MissingShape } from './types.ts';
console.log('never runs');
`,
"ts",
{ "types.ts": "export interface Shape { value: number }\nconsole.log('types evaluated');\n" },
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toBe(
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Shape'\n",
);
});
test("default interface exports satisfy Node's strip-only default link", async () => {
const r = await compileAndRun(
"esm-default-interface-link",
`import DefaultShape from './types.ts';
type LocalShape = DefaultShape;
console.log('ok');
`,
"ts",
{ "types.ts": "export default interface DefaultShape { value: number }\n" },
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe("ok\n");
expect(r.stderr).toBe("");
});
test("explicit module packages do not expose default-interface placeholders", async () => {
const r = await compileAndRun(
"esm-default-interface-explicit-module",
`import DefaultShape from './types.ts';
type LocalShape = DefaultShape;
console.log('never runs');
`,
"ts",
{
"package.json": `{ "type": "module" }\n`,
"types.ts": "export default interface DefaultShape { value: number }\n",
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toBe(
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'default'\n",
);
});
test("typeless packages preserve default-interface placeholders through re-exports", async () => {
const r = await compileAndRun(
"esm-default-interface-reexport",
`import DefaultShape from './reexport.ts';
type LocalShape = DefaultShape;
console.log('ok');
`,
"ts",
{
"reexport.ts": `export { default } from './types.ts';\n`,
"types.ts": "export default interface DefaultShape { value: number }\n",
},
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe("ok\n");
expect(r.stderr).toBe("");
});
test("a child native-ESM failure wins over a later parent CJS failure", async () => {
const r = await compileAndRun(
"esm-type-link-before-cjs",
`import './middle.ts';
import { missing } from './table.cjs';
console.log('never runs', missing);
`,
"ts",
{
"middle.ts": `import { Shape } from './types.ts';
console.log('middle never runs');
`,
"types.ts": "export interface Shape { value: number }\nconsole.log('types evaluated');\n",
"table.cjs": "module.exports = { missing: 7 };\n",
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toBe(
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Shape'\n",
);
});
});
describe("checked-dynamic boundary behavior (scriptc-only)", () => {
test("a checked argument into a call through 'unknown' matches Node", async () => {
// `this` in a plain JS function is the checked-dynamic ambient
// receiver; a member CALL through it with an 'any'-typed argument
// 383(d)'s "one unbridgeable mix"). The crossing exists now
// (dynFromJsval, SEMANTICS 394): the argument wraps by reference, the
// statement compiles for real, and the unbound-`this` member read
// throws Node's OWN TypeError — the divergence this pin used to own
// is retired.
const r = await compileAndRun(
"checked-arg-dyn-call",
`// @dynamic
'use strict';
function register(item) {
try {
this.registry(item);
return "unreachable";
} catch (e) {
return "caught: " + e.message;
}
}
console.log(\`\${register(7)}\`);
`,
"mjs",
{ "tsconfig.json": '{"compilerOptions":{"strict":true,"noImplicitAny":false}}\n' },
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe("caught: Cannot read properties of undefined (reading 'registry')\n");
expect(r.stderr).toBe("");
});
});
// A cipher name Node lists in crypto.getCiphers() but the runtime does not
// implement throws a coded SC2020 error naming the cipher when the name is
// computed; literal names are refused at compile time (node-types-fenced).
describe("unimplemented cipher names", () => {
test("a computed cipher name throws SC2020 naming the cipher", async () => {
const r = await compileAndRun(
"cipher-computed-name",
`import { createCipheriv } from "node:crypto";
const mode = ["c", "c", "m"].join("");
try {
createCipheriv("aes-128-" + mode, Buffer.alloc(16), Buffer.alloc(12));
} catch (error) {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
try {
createCipheriv("aes-128-" + mode.slice(1), Buffer.alloc(16), Buffer.alloc(12));
} catch (error) {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
`,
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe(
"Error SC2020 The cipher 'aes-128-ccm' has no native implementation; the supported ciphers are " +
"aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in " +
"cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " +
"chacha20-poly1305\n" +
"Error ERR_CRYPTO_UNKNOWN_CIPHER Unknown cipher\n",
);
expect(r.stderr).toBe("");
});
});
// new tls.TLSSocket reads a runtime options record: the client mode, whose
// handshake Node never starts, and members without a lowering throw the
// fence, as do sockets it cannot take over (a second wrap, and
// tls.connect({ socket }) over a socket that is still dialing).
describe("tls.TLSSocket runtime fences", () => {
test("client mode, unlowered members and sockets that cannot be taken over throw", async () => {
const r = await compileAndRun(
"tls-socket-fences",
`import { readFileSync } from "node:fs";
import * as net from "node:net";
import * as tls from "node:tls";
const certs = "tests/fixtures/server/certs";
const cert = readFileSync(\`\${certs}/localhost.pem\`);
const key = readFileSync(\`\${certs}/localhost-key.pem\`);
function report(label: string, run: () => void): void {
try {
run();
console.log(label, "no error");
} catch (error) {
console.log(label, (error as Error).message);
}
}
const client: Record<string, unknown> = { cert, key };
const session: Record<string, unknown> = { isServer: true, cert, key, session: Buffer.alloc(1) };
const server: Record<string, unknown> = { isServer: true, cert, key };
const listener = net.createServer((raw) => {
report("client mode", () => new tls.TLSSocket(raw, client));
report("session", () => new tls.TLSSocket(raw, session));
const wrapped = new tls.TLSSocket(raw, server);
report("twice", () => new tls.TLSSocket(raw, server));
wrapped.destroy();
listener.close();
});
listener.listen(0, () => {
const c = net.connect((listener.address() as net.AddressInfo).port, "localhost");
report("dialing", () => tls.connect({ socket: c }));
c.on("error", () => {});
});
`,
);
expect(r.stderr).toBe("");
expect(r.stdout).toBe(
"dialing tls.connect over a socket that is not connected, or is already wrapped, is not supported yet\n" +
"client mode new tls.TLSSocket option 'isServer' has no static lowering — Node starts no handshake for a client-mode TLSSocket; tls.connect({ socket }) runs the client handshake over an open socket\n" +
"session new tls.TLSSocket option 'session' has no static lowering — isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members\n" +
"twice new tls.TLSSocket over a socket that is not connected, or is already wrapped, is not supported yet\n",
);
});
});
// With a secureContext, the TLSSocket's own options still apply: a truthy
// requestCert, which asks for client certificates this runtime does not
// verify, and SNICallback throw the fence instead of being dropped.
describe("tls.TLSSocket socket options beside a secureContext", () => {
test("requestCert and SNICallback throw the fence", async () => {
const r = await compileAndRun(
"tls-socket-context-fences",
`import { readFileSync } from "node:fs";
import * as net from "node:net";
import * as tls from "node:tls";
const certs = "tests/fixtures/server/certs";
const cert = readFileSync(\`\${certs}/localhost.pem\`);
const key = readFileSync(\`\${certs}/localhost-key.pem\`);
const context = tls.createSecureContext({ cert, key });
function report(label: string, run: () => void): void {
try {
run();
console.log(label, "no error");
} catch (error) {
console.log(label, (error as Error).message);
}
}
const listener = net.createServer((raw) => {
report("requestCert", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, requestCert: true, rejectUnauthorized: true }));
report("SNICallback", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, SNICallback: (name: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, context) }));
report("requestCert false", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, requestCert: false, rejectUnauthorized: true }).destroy());
listener.close();
});
listener.listen(0, () => {
const c = net.connect((listener.address() as net.AddressInfo).port, "localhost");
c.on("error", () => {});
c.on("close", () => console.log("client closed"));
});
`,
);
expect(r.stderr).toBe("");
expect(r.stdout).toBe(
"requestCert new tls.TLSSocket option 'requestCert' has no static lowering — client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default\n" +
"SNICallback new tls.TLSSocket option 'SNICallback' has no static lowering — isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members\n" +
"requestCert false no error\n" +
"client closed\n",
);
});
});
// A curve Node lists in crypto.getCurves() but the runtime does not
// implement throws a coded SC2020 error naming the curve when the name is
// computed; literal names are refused at compile time (node-types-fenced).
describe("unimplemented ECDH curves", () => {
test("a computed curve name throws SC2020 naming the curve", async () => {
const r = await compileAndRun(
"ecdh-computed-curve",
`import { createECDH, ECDH } from "node:crypto";
const size = ["1", "6", "3"].join("");
function report(error: unknown): void {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
try {
createECDH("sect" + size + "k1");
} catch (error) {
report(error);
}
try {
ECDH.convertKey(Buffer.from([0]), "sect" + size + "k1");
} catch (error) {
report(error);
}
try {
createECDH("sect" + size + "x1");
} catch (error) {
report(error);
}
`,
);
expect(r.exitCode).toBe(0);
const supported =
"has no native implementation; the supported curves are prime192v1, secp224r1, prime256v1, " +
"secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and " +
"brainpoolP512r1";
expect(r.stdout).toBe(
`Error SC2020 The curve 'sect163k1' ${supported}\n` +
`Error SC2020 The curve 'sect163k1' ${supported}\n` +
"TypeError ERR_CRYPTO_INVALID_CURVE Invalid EC curve name\n",
);
expect(r.stderr).toBe("");
});
test("ECDH property reads and writes throw SC2020", async () => {
const r = await compileAndRun(
"ecdh-properties",
`const crypto = require("node:crypto");
const ecdh = crypto.createECDH("prime256v1");
for (const run of [() => ecdh.getPublicKey.name, () => { ecdh.custom = 1; }]) {
try {
run();
} catch (e) {
console.log(e.name, e.code);
}
}
`,
"cjs",
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe("Error SC2020\nError SC2020\n");
});
});
// DiffieHellman primes past mbedTLS's 8192-bit limit throw a coded SC2020
// error, as do property reads other than verifyError, property writes, and
// Object.prototype method calls on an untyped DiffieHellman.
describe("DiffieHellman runtime refusals", () => {
test("primes longer than 8192 bits throw SC2020", async () => {
const r = await compileAndRun(
"dh-long-prime",
`import { createDiffieHellman } from "node:crypto";
const prime = Buffer.alloc(1025, 0xff);
for (const run of [() => createDiffieHellman(prime), () => createDiffieHellman(8193)]) {
try {
run();
} catch (error) {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
}
console.log(createDiffieHellman(Buffer.alloc(1024, 0xff)).verifyError);
`,
);
expect(r.exitCode).toBe(0);
const message = "DiffieHellman primes longer than 8192 bits have no native implementation";
expect(r.stdout).toBe(`Error SC2020 ${message}\nError SC2020 ${message}\n1\n`);
expect(r.stderr).toBe("");
});
test("DiffieHellman property reads, writes and Object.prototype calls throw SC2020", async () => {
const r = await compileAndRun(
"dh-properties",
`const crypto = require("node:crypto");
const dh = crypto.getDiffieHellman("modp1");
console.log(dh.verifyError);
for (const run of [() => dh.getPrime.name, () => { dh.verifyError = 1; }, () => dh.hasOwnProperty("verifyError")]) {
try {
run();
} catch (e) {
console.log(e.name, e.code);
}
}
`,
"cjs",
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe("0\nError SC2020\nError SC2020\nError SC2020\n");
});
});
// Node formats an unknown output encoding with %s, which prints a
// function's source text and calls a toString the encoding's class
// defines. The cipher runtime has neither, so both throw a coded SC2020
// error.
describe("cipher output encodings that print through source text or a class toString", () => {
test("update() and final() throw SC2020", async () => {
const r = await compileAndRun(
"cipher-encoding-class-to-string",
`import { createCipheriv } from "node:crypto";
class Encoding {
name: string;
constructor(name: string) {
this.name = name;
}
toString(): string {
return this.name;
}
}
const instance: any = new Encoding("nope");
const fn: any = () => "nope";
try {
createCipheriv("aes-128-ctr", Buffer.alloc(16), Buffer.alloc(16)).final(instance);
} catch (error) {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
try {
createCipheriv("aes-128-ctr", Buffer.alloc(16), Buffer.alloc(16)).update("abc", "utf8", fn);
} catch (error) {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
`,
);
expect(r.exitCode).toBe(0);
const message =
"Error SC2020 An output encoding that is a function or an instance of a class defining toString has no lowering; " +
"pass the encoding name as a string\n";
expect(r.stdout).toBe(message + message);
expect(r.stderr).toBe("");
});
});
// After a Hash's stream flushes its digest, Node copies the finished
// OpenSSL context, whose later updates and digests fail with OpenSSL
// error-queue codes. That copy throws a coded SC2020 error.
describe("copy() of a Hash whose stream has flushed", () => {
test("throws SC2020", async () => {
const r = await compileAndRun(
"hash-stream-copy-after-flush",
`import { createHash } from "node:crypto";
const hash = createHash("sha256");
hash.resume();
hash.on("finish", () => {
try {
hash.copy();
} catch (error) {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
});
hash.end("abc");
`,
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe(
"Error SC2020 copy() of a Hash whose stream has flushed its digest has no lowering\n",
);
expect(r.stderr).toBe("");
});
});
// A Hash or Hmac lets go of its stream once the stream is destroyed, so
// listeners that capture the value do not keep it alive. Using the value
// as a stream again throws a coded SC2020 error; Node keeps the destroyed
// stream.
describe("stream use of a Hash after its stream closed", () => {
test("throws SC2020", async () => {
const r = await compileAndRun(
"hash-stream-after-close",
`import { createHash } from "node:crypto";
const hash = createHash("sha256");
hash.resume();
hash.on("close", () => {
try {
hash.on("data", () => {});
} catch (error) {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
console.log("digest", hash.digest("hex").slice(0, 8));
});
hash.end("abc");
`,
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe(
"Error SC2020 Stream use of a Hash or Hmac after its stream closed has no lowering\n" +
"digest ba7816bf\n",
);
expect(r.stderr).toBe("");
});
});
// A Hash or Hmac held in an 'unknown' slot answers update(), digest() and
// Hash's copy(). Its other members throw a coded SC2020 error, and so do
// those methods called with another receiver.
describe("Hash and Hmac members through 'unknown'", () => {
test("throw SC2020 other than update, digest and copy", async () => {
const r = await compileAndRun(
"hash-unknown-members",
`const crypto = require("node:crypto");
function attempt(value, run) {
try {
run(value);
} catch (error) {
console.log(error.name, error.code, error.message);
}
}
const hash = [crypto.createHash("sha256")][0];
attempt(hash, (h) => h.on("data", () => {}));
attempt(hash, (h) => h.readable);
attempt([crypto.createHmac("sha256", "k")][0], (m) => m.write("x"));
attempt(hash, (h) => h.digest.call({}));
console.log(hash.update("abc").digest("hex").slice(0, 8));
`,
"cjs",
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe(
"Error SC2020 Hash members other than update(), digest() and copy() have no lowering on an 'unknown' value; cast the value to Hash first\n".repeat(
2,
) +
"Error SC2020 Hmac members other than update() and digest() have no lowering on an 'unknown' value; cast the value to Hmac first\n" +
"Error SC2020 Hash and Hmac methods called with a receiver other than their own Hash or Hmac have no lowering\n" +
"ba7816bf\n",
);
expect(r.stderr).toBe("");
});
});
// Node 24.22 skips the chacha20-poly1305 tag check when neither
// setAuthTag() nor authTagLength was used, and returns unverified
// plaintext. scriptc keeps Node 24.15's behavior: final() fails.
describe("chacha20-poly1305 decryption without a tag", () => {
test("final() fails instead of returning unverified plaintext", async () => {
const r = await compileAndRun(
"cipher-chacha-no-tag",
`import { createCipheriv, createDecipheriv } from "node:crypto";
const key = Buffer.alloc(32, 1);
const iv = Buffer.alloc(12, 2);
const cipher = createCipheriv("chacha20-poly1305", key, iv);
const ciphertext = Buffer.concat([cipher.update("secret"), cipher.final()]);
for (const input of [ciphertext, Buffer.from("forged data"), Buffer.alloc(0)]) {
const decipher = createDecipheriv("chacha20-poly1305", key, iv);
const text = decipher.update(input);
try {
decipher.final();
console.log("accepted", text.toString("hex"));
} catch (error) {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
}
`,
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe(
"Error undefined Unsupported state or unable to authenticate data\n".repeat(3),
);
expect(r.stderr).toBe("");
});
});
// Function expressions the frontend lowers speculatively (the receiver of
// `.call`/`.apply`, a computed class base) keep their refused statements as
// runtime fences. They used to vanish: the capture buffer held the refusal,
// the candidate was kept, and the statement was simply missing from it.
describe("refused statements in speculatively lowered JavaScript functions", () => {
test.each([
[
"speculative-call-this",
`(function () {
var root = this;
console.log("before");
process.on("__proto__", () => root);
console.log("after");
}).call(this);
`,
"before\n",
4,
],
[
"speculative-apply",
`(function () {
console.log("before");
process.on("__proto__", () => {});
}).apply(null, []);
`,
"before\n",
3,
],
[
"speculative-class-base",
`class Base {}
class Derived extends (function () {
console.log("before");
process.on("__proto__", () => {});
return Base;
})() {}
console.log(typeof Derived);
`,
"before\n",
4,
],
] as const)("%s", async (name, source, stdout, line) => {
const r = await compileAndRun(name, source, "cjs");
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe(stdout);
expect(r.stderr).toMatch(
new RegExp(
`^Uncaught Error: 'process\\.on\\("__proto__", \\.\\.\\.\\)' is part of the standard library types but has no scriptc lowering yet \\[SC2020 at .*${name}\\.cjs:${line}\\]\\n$`,
),
);
});
});
describe("constructors returning objects (JS lane)", () => {
test("a returned plain object is a named deferred fence", async () => {
// Node's `new Box(2)` answers `{ size: 99 }`; a Box instance cannot
// hold that object, so the return refuses instead of answering `this`.
const r = await compileAndRun(
"constructor-returns-object",
`'use strict';
class Box {
constructor(size) {
this.size = size;
if (size > 1) return { size: 99 };
}
}
console.log(new Box(1).size);
console.log(new Box(2));
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("1\n");
expect(r.stderr).toMatch(
/^Uncaught Error: returning an object other than an instance of the class from a constructor \(Node's 'new Box\(\)' answers the returned object, .*\) is not supported yet \[SC1090 at .*constructor-returns-object\.cjs:5\]\n$/,
);
});
test("an untyped returned value fences only when it holds a foreign object", async () => {
// Primitive results answer the constructed instance, as in Node; an
// object that is not a Wrap instance refuses.
const r = await compileAndRun(
"constructor-returns-untyped",
`'use strict';
class Wrap {
constructor(value) {
this.value = value;
return value;
}
}
console.log(new Wrap(1).value, new Wrap('s').value, new Wrap(null).value);
const w = new Wrap(0);
console.log(new Wrap(w) === w);
console.log(new Wrap({ a: 1 }));
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("1 s null\ntrue\n");
expect(r.stderr).toMatch(
/^Uncaught Error: returning an object other than an instance of the class from a constructor .* is not supported yet \[SC1090 at .*constructor-returns-untyped\.cjs:5\]\n$/,
);
});
});
describe("module exports that read this, called through the module binding (JS lane)", () => {
test("a require binding's member call is a named deferred fence", async () => {
// Node passes the exports object as `this` to `m.hello()`; the direct
// call has none to pass, so it refuses instead of reading another one.
const r = await compileAndRun(
"require-member-this",
`'use strict';
const m = require('./lib.cjs');
console.log(m.plain(21));
console.log(m.hello());
`,
"cjs",
{
"lib.cjs": `'use strict';
exports.name = 'lib';
exports.hello = function () { return 'hi from ' + this.name; };
exports.plain = function (x) { return x * 2; };
`,
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("42\n");
expect(r.stderr).toMatch(
/^Uncaught Error: calling a module export that reads 'this' through its module binding \('hello': .*\) is not supported yet \[SC1090 at .*require-member-this\.cjs:4\]\n$/,
);
});
test("a namespace import's member call is a named deferred fence", async () => {
const r = await compileAndRun(
"namespace-member-this",
`import * as ns from './lib.mjs';
console.log(ns.plain(1));
console.log(ns.who());
`,
"mjs",
{
"lib.mjs": `export function who() { return typeof this; }
export function plain(x) { return x + 1; }
`,
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("2\n");
expect(r.stderr).toMatch(
/^Uncaught Error: calling a module export that reads 'this' through its module binding \('who': .*\) is not supported yet \[SC1090 at .*namespace-member-this\.mjs:3\]\n$/,
);
});
test("a namespace import's export that passes this on with apply or call compiles", async () => {
// Effect 4's provideService: `dual(...).apply(this, arguments)` hands the
// receiver to a function that `dual` returns, which reads it only to
// pass it on to an arrow, as here.
const r = await compileAndRun(
"namespace-member-forwards-this",
`import * as ns from './lib.mjs';
export const added = ns.provide(1, 2, 3);
console.log(added, ns.sum(3, 4));
`,
"mjs",
{
"lib.mjs": `const dual = function (arity, body) {
switch (arity) {
case 2:
return function (a, b) { return arguments.length >= 2 ? body(a, b) : (self) => body(self, a); };
default:
return function () { return body.apply(this, arguments); };
}
};
export const provide = function () { return dual(3, (a, b, c) => a + b + c).apply(this, arguments); };
const twice = function (a, b) { return (a + b) * 2; };
export const sum = function (a, b) { return twice.call(this, a, b); };
`,
},
);
expect(r).toEqual({ stdout: "6 14\n", stderr: "", exitCode: 0 });
});
test("a require binding's export that passes this on with apply compiles", async () => {
const r = await compileAndRun(
"require-member-forwards-this",
`'use strict';
const m = require('./lib.cjs');
console.log(m.scale(5));
`,
"cjs",
{
"lib.cjs": `'use strict';
const triple = function (x) { return x * 3; };
exports.scale = function () { return triple.apply(this, arguments); };
`,
},
);
expect(r).toEqual({ stdout: "15\n", stderr: "", exitCode: 0 });
});
test.each([
[
"a namespace import's export that passes this on to a function that reads it",
"namespace-member-forwards-to-reader",
"mjs",
`import * as ns from './lib.mjs';
console.log(ns.plain(1));
console.log(ns.viaCall('?'));
`,
{
"lib.mjs": `export const tag = 'ns';
function read(suffix) { return this.tag + suffix; }
export function viaCall(suffix) { return read.call(this, suffix); }
export function plain(x) { return x + 1; }
`,
},
"2\n",
"viaCall",
3,
],
[
"a namespace import's export that passes this on through a factory to a function that reads it",
"namespace-member-forwards-through-factory",
"mjs",
`import * as ns from './lib.mjs';
console.log(ns.plain(1));
console.log(ns.viaDual('?'));
`,
{
"lib.mjs": `export const tag = 'ns';
const dual = function (arity, body) {
switch (arity) {
case 2:
return function (a, b) { return arguments.length >= 2 ? body(a, b) : (self) => body(self, a); };
default:
return function () { return body.apply(this, arguments); };
}
};
export const viaDual = function () { return dual(9, function (s) { return this.tag + s; }).apply(this, arguments); };
export function plain(x) { return x + 1; }
`,
},
"2\n",
"viaDual",
3,
],
[
"a namespace import's export that passes this on through a factory that reassigns its parameter",
"namespace-member-forwards-reassigned",
"mjs",
`import * as ns from './lib.mjs';
console.log(ns.plain(1));
console.log(ns.viaWrap('?'));
`,
{
"lib.mjs": `export const tag = 'ns';
const wrap = function (body) {
body = function (s) { return this.tag + s; };
return function () { return body.apply(this, arguments); };
};
export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); };
export function plain(x) { return x + 1; }
`,
},
"2\n",
"viaWrap",
3,
],
[
"a namespace import's export that passes this on through a factory that replaces its parameter as a destructuring",
"namespace-member-forwards-destructured",
"mjs",
`import * as ns from './lib.mjs';
console.log(ns.plain(1));
console.log(ns.viaWrap('?'));
`,
{
"lib.mjs": `export const tag = 'ns';
const wrap = function (body) {
[body] = [function (s) { return this.tag + s; }];
return function () { return body.apply(this, arguments); };
};
export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); };
export function plain(x) { return x + 1; }
`,
},
"2\n",
"viaWrap",
3,
],
[
"a namespace import's export that passes this on through a factory that replaces its parameter as a object destructuring",
"namespace-member-forwards-object-destructured",
"mjs",
`import * as ns from './lib.mjs';
console.log(ns.plain(1));
console.log(ns.viaWrap('?'));
`,
{
"lib.mjs": `export const tag = 'ns';
const wrap = function (body) {
({ body } = { body: function (s) { return this.tag + s; } });
return function () { return body.apply(this, arguments); };
};
export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); };
export function plain(x) { return x + 1; }
`,
},
"2\n",
"viaWrap",
3,
],
[
"a namespace import's export that passes this on through a factory that replaces its parameter as a for-of target",
"namespace-member-forwards-for-of",
"mjs",
`import * as ns from './lib.mjs';
console.log(ns.plain(1));
console.log(ns.viaWrap('?'));
`,
{
"lib.mjs": `export const tag = 'ns';
const wrap = function (body) {
for (body of [function (s) { return this.tag + s; }]);
return function () { return body.apply(this, arguments); };
};
export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); };
export function plain(x) { return x + 1; }
`,
},
"2\n",
"viaWrap",
3,
],
[
"a require binding's export that passes this on to a function that reads it",
"require-member-forwards-to-reader",
"cjs",
`'use strict';
const m = require('./lib.cjs');
console.log(m.plain(21));
console.log(m.viaApply('!'));
`,
{
"lib.cjs": `'use strict';
exports.name = 'lib';
const read = function (suffix) { return this.name + suffix; };
exports.viaApply = function () { return read.apply(this, arguments); };
exports.plain = function (x) { return x * 2; };
`,
},
"42\n",
"viaApply",
4,
],
] as const)(
"%s is a named deferred fence",
async (_, name, ext, source, files, stdout, member, line) => {
// Node passes the module object on to `read`, which reads it; the
// direct call has none to pass.
const r = await compileAndRun(name, source, ext, files);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe(stdout);
expect(r.stderr).toMatch(
new RegExp(
`^Uncaught Error: calling a module export that reads 'this' through its module binding \\('${member}': .*\\) is not supported yet \\[SC1090 at .*${name}\\.${ext}:${line}\\]\\n$`,
),
);
},
);
test("an export that also reads this beside passing it on is a named deferred fence", async () => {
const r = await compileAndRun(
"namespace-member-forwards-and-reads-this",
`import * as ns from './lib.mjs';
console.log(ns.tag('x'));
`,
"mjs",
{
"lib.mjs": `const join = function (a, b) { return a + ':' + b; };
export function tag(x) { return join.call(this, x, typeof this); }
`,
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toMatch(
/^Uncaught Error: calling a module export that reads 'this' through its module binding \('tag': .*\) is not supported yet \[SC1090 at .*namespace-member-forwards-and-reads-this\.mjs:2\]\n$/,
);
});
});
// CommonJS forms around the module's exports that keep a refusal name the
// construct itself (they used to report an unlowerable `require` value, a
// "binding form with no lowering", or the ES-module `module` message).
describe("CommonJS export-object and inline require refusals", () => {
test.each([
[
"exports-as-argument",
`exports.x = 1;
function keys(o) {
return Object.keys(o);
}
console.log("before");
console.log(keys(exports));
`,
{},
"before\n",
6,
"'exports' as a value other than a member access, comparison, typeof or truthiness test \\(the compiled exports are a static table, not an object: read exports\\.<name>\\)",
],
[
"exports-rebinding",
`console.log("before");
exports = function hash(x) {
return x;
};
exports.extra = 1;
`,
{},
"before\n",
2,
"rebinding 'exports' without module\\.exports \\(exports = value\\)",
],
[
"require-computed",
`const name = "./lib.cjs";
console.log("before");
console.log(require(name).x);
`,
{ "lib.cjs": "exports.x = 1;\n" },
"before\n",
3,
"require\\(\\) with a computed specifier",
],
[
"exports-call-reads-this",
`exports.timeouts = function (n) {
return this.scale * n;
};
exports.scale = 2;
console.log("before");
console.log(exports.timeouts(3));
`,
{},
"before\n",
6,
"calling 'exports\\.timeouts' through the module's exports object when the function may read 'this'",
],
[
"require-table-as-value",
`function count(o) {
return Object.keys(o).length;
}
console.log("before");
console.log(count(require("./lib.cjs")));
`,
{ "lib.cjs": "exports.x = 1;\n" },
"before\n",
5,
"the exports object of '\\./lib\\.cjs' as a value",
],
] as const)("%s", async (name, source, extra, stdout, line, message) => {
const r = await compileAndRun(name, source, "cjs", extra);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe(stdout);
expect(r.stderr).toMatch(
new RegExp(`^Uncaught Error: ${message}.*\\[SC1090 at .*${name}\\.cjs:${line}\\]\\n$`),
);
});
});
describe("builtin constructor values (JS lane)", () => {
test("members without a lowering stay refused instead of reading undefined", async () => {
// Builtin constructors read as interned function values that model only
// name, length and constructor. Every other static member or prototype
// read (direct, bracketed, `in`, through an alias or globalThis), and
// reflection on or redefinition of their members, must stay a located
// SC2020/SC1090 refusal, never an undefined or otherwise wrong value.
// Test262 lost 769 refusals to undefined reads before this was pinned.
const reads = [
["Map.prototype", "Map.prototype"],
["Date.prototype", "Date.prototype"],
["RegExp.prototype", "RegExp.prototype"],
["Set.prototype", "Set.prototype"],
["WeakMap.prototype", "WeakMap.prototype"],
["WeakSet.prototype", "WeakSet.prototype"],
["Date.prototype.getYear", "Date.prototype.getYear"],
["Map.prototype.constructor", "Map.prototype.constructor"],
["Object.isFrozen", "Object.isFrozen"],
[
"getOwnPropertyDescriptor(Map.prototype)",
'Object.getOwnPropertyDescriptor(Map.prototype, "size")',
],
["isExtensible(Date.prototype)", "Object.isExtensible(Date.prototype)"],
["Map['prototype']", 'Map["prototype"]'],
["'prototype' in Map", '"prototype" in Map'],
["globalThis.Map.prototype", "globalThis.Map.prototype"],
["Map alias prototype", "MapAlias.prototype"],
["Date alias prototype", "DateAlias.prototype"],
["Object alias isFrozen", "ObjectAlias.isFrozen"],
["Object.hasOwn(Object)", 'Object.hasOwn(Object, "hasOwn")'],
["getOwnPropertyNames(Date)", "Object.getOwnPropertyNames(Date)"],
["defineProperty(Promise)", 'Object.defineProperty(Promise, "resolve", { value: 1 })'],
["hasOwnProperty.call(Map)", 'Object.prototype.hasOwnProperty.call(Map, "groupBy")'],
] as const;
const source = `"use strict";
const MapAlias = Map;
const DateAlias = Date;
const { Object: ObjectAlias } = globalThis;
function report(label, e) {
const m = /\\[(SC\\d{4}) at [^\\]]*:(\\d+)\\]$/.exec(e.message);
console.log(label, m ? "refused " + m[1] + " line " + m[2] : "threw " + e.message);
}
${reads.map(([label, read]) => `try { const value = ${read}; console.log(${JSON.stringify(label)}, "read", typeof value); } catch (e) { report(${JSON.stringify(label)}, e); }`).join("\n")}
console.log(Date.length, RegExp.length, Promise.length, Object.length, Map.length, Set.length);
console.log(Date.name, Map.name, Map.constructor === Function, Object.constructor === Function);
const promisePrototype = Promise.prototype;
console.log(typeof promisePrototype, typeof promisePrototype.then, promisePrototype.constructor === Promise);
`;
const r = await compileAndRun("builtin-constructor-members", source, "cjs");
expect(r.stderr).toBe("");
expect(r.exitCode).toBe(0);
const lines = r.stdout.trimEnd().split("\n");
expect(lines.slice(0, reads.length)).toEqual(
reads.map(([label], i) =>
expect.stringMatching(
new RegExp(
`^${label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} refused SC(2020|1090) line ${i + 9}$`,
),
),
),
);
// Promise.prototype reads as an object holding the runtime's methods.
expect(lines.slice(reads.length)).toEqual([
"7 2 1 1 0 0",
"Date Map true true",
"object function true",
]);
});
});
// WebCrypto's native slice is crypto.subtle's secret-key methods and its
// ECDSA, Ed25519 and RSA algorithms. Algorithms Node recognizes outside
// that slice reject with a coded SC2020
// Error instead of a Node-shaped success, and a JavaScript call of a
// method outside it meets its named fence when it runs.
// Node's own rejections (unknown names, bad usages) are corpus-pinned
// (6292, 6294); these refusals have no Node twin, so they live here.
describe("WebCrypto refusals (JS lane)", () => {
test("recognized algorithms outside the lowered ones reject with SC2020", async () => {
const r = await compileAndRun(
"webcrypto-unsupported-algorithms",
`const bytes = new TextEncoder().encode("secret");
async function attempt(label, run) {
try {
await run();
console.log(label, "resolved");
} catch (e) {
console.log(label, e.name, e.code, e.message);
}
}
const key = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
await attempt("ed448", () => crypto.subtle.sign({ name: "Ed448" }, key, bytes));
await attempt("ecdsa with an hmac key", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes));
await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes));
await attempt("sha3 hmac", () =>
crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]),
);
const aes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, false, ["encrypt"]);
await attempt("aes-ocb", () => crypto.subtle.encrypt({ name: "AES-OCB", iv: new Uint8Array(12) }, aes, bytes));
await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key, 256));
await attempt("rsa-oaep wrap", () => crypto.subtle.wrapKey("raw", key, key, { name: "RSA-OAEP" }));
const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
const kw = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, false, ["wrapKey", "unwrapKey"]);
await attempt("wrap an ecdsa key", () => crypto.subtle.wrapKey("spki", pair.publicKey, kw, "AES-KW"));
await attempt("wrap with an ecdsa key", () => crypto.subtle.wrapKey("raw", aes, pair.privateKey, "AES-KW"));
await attempt("unwrap an ecdsa key", () =>
crypto.subtle.unwrapKey("spki", new Uint8Array(24), kw, "AES-KW", { name: "ECDSA", namedCurve: "P-256" }, true, [
"verify",
]),
);
`,
"mjs",
);
expect(r.exitCode).toBe(0);
expect(r.stderr).toBe("");
expect(r.stdout).toBe(
"ed448 Error SC2020 crypto.subtle.sign with the Ed448 algorithm is not supported yet\n" +
"ecdsa with an hmac key InvalidAccessError 15 Key algorithm mismatch\n" +
"sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" +
"sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" +
"aes-ocb Error SC2020 crypto.subtle.encrypt with the AES-OCB algorithm is not supported yet\n" +
"ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n" +
"rsa-oaep wrap Error SC2020 crypto.subtle.wrapKey with the RSA-OAEP algorithm is not supported yet\n" +
"wrap an ecdsa key Error SC2020 crypto.subtle.wrapKey with an ECDSA, Ed25519 or RSA key is not supported yet\n" +
"wrap with an ecdsa key InvalidAccessError 15 Key algorithm mismatch\n" +
"unwrap an ecdsa key Error SC2020 crypto.subtle.unwrapKey with the ECDSA algorithm is not supported yet\n",
);
});
test("a method outside the slice is a named fence where it runs", async () => {
const r = await compileAndRun(
"webcrypto-get-public-key",
`console.log("before");
const key = await crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
await crypto.subtle.getPublicKey(key, ["verify"]);
`,
"mjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("before\n");
expect(r.stderr).toMatch(
/^Uncaught Error: 'crypto\.subtle\.getPublicKey' is part of the standard library types but has no scriptc lowering yet.*\n$/,
);
});
});
// The Ed25519 arithmetic (scr_ed25519.c) links only into programs whose
// string literals name Ed25519. Without it, an Ed25519 public key still
// imports and exports, while private-key parsing, signing and verification
// reject with a coded SC2020 Error where Node answers; there is no Node
// twin, so this lives here. The corpus (7080, 7081) pins the linked path.
describe("Ed25519 without the Ed25519 unit (JS lane)", () => {
test("a program that never names Ed25519 refuses its arithmetic with SC2020", async () => {
const r = await compileAndRun(
"ed25519-unit-not-linked",
`import * as c from "node:crypto";
const spki = Buffer.from("302a300506032b6570032100d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "hex");
const pkcs8 = Buffer.from("302e020100300506032b6570042204209d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "hex");
const pub = c.createPublicKey({ key: spki, format: "der", type: "spki" });
console.log(pub.asymmetricKeyType, pub.export({ format: "jwk" }).x);
async function attempt(label, run) {
try {
await run();
console.log(label, "resolved");
} catch (e) {
console.log(label, e.name, e.code, e.message);
}
}
await attempt("pkcs8", () => c.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" }));
await attempt("verify", () => c.verify(null, Buffer.from("x"), pub, Buffer.alloc(64)));
// ECDSA links the WebCrypto key unit; the Ed25519 name is built at run time.
const ecdsa = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
console.log(ecdsa.privateKey.type);
const name = ["Ed", "25519"].join("");
await attempt("generateKey", () => crypto.subtle.generateKey(name, false, ["sign", "verify"]));
await attempt("importKey", () => crypto.subtle.importKey("pkcs8", pkcs8, name, false, ["sign"]));
const raw = await crypto.subtle.importKey("raw", spki.subarray(12), name, false, ["verify"]);
await attempt("subtle verify", () => crypto.subtle.verify(name, raw, new Uint8Array(64), new Uint8Array(1)));
`,
"mjs",
);
const refusal =
"Error SC2020 Ed25519 keys are supported when a string literal in the program names 'ed25519'";
expect(r.exitCode).toBe(0);
expect(r.stderr).toBe("");
expect(r.stdout).toBe(
"ed25519 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\n" +
`pkcs8 ${refusal}\n` +
`verify ${refusal}\n` +
"private\n" +
`generateKey ${refusal}\n` +
`importKey ${refusal}\n` +
`subtle verify ${refusal}\n`,
);
});
});
describe("inspecting error subclasses with properties created at run time (JS lane)", () => {
test("an instance whose constructor created a property is a named fence", async () => {
// Node prints `[Conditional [Error]: m] { extra: 'x' }`. The property
// lives in the instance's property bag, which the error rendering does
// not read, so the instance refuses instead of printing a partial
// object. One without such a property prints as Node prints it.
const r = await compileAndRun(
"error-subclass-runtime-properties",
`import * as util from "node:util";
class Conditional extends Error {
constructor(message, extra) {
super(message);
if (extra) this.extra = extra;
}
}
const plain = new Conditional("plain");
plain.stack = "";
console.log(util.inspect(plain));
const extra = new Conditional("extra", "x");
extra.stack = "";
console.log(util.inspect(extra));
`,
"mjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("[Conditional [Error]: plain]\n");
expect(r.stderr).toMatch(
/^Uncaught Error: inspecting a 'Conditional' error with properties created at run time is not supported yet \[SC2020[^\]]*\]\n$/,
);
});
});
describe("elements on Array.prototype", () => {
// Holes of native arrays read undefined without consulting
// Array.prototype, so storing an element there refuses instead of
// letting later hole reads diverge from Node. Named members stay
// writable.
test("an index write is a named fence (TS lane)", async () => {
const r = await compileAndRun(
"array-prototype-element-ts",
`const proto = Array.prototype as unknown as Record<string, unknown>;
proto.label = "named";
console.log(proto.label);
(Array.prototype as unknown as Record<number, unknown>)[1] = "proto";
console.log("unreachable");
`,
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("named\n");
expect(r.stderr).toMatch(
/^Uncaught Error: Elements on Array\.prototype are not supported yet \[SC2020[^\]]*\]\n$/,
);
});
test("an index write and a mutating method are named fences (JS lane)", async () => {
const write = await compileAndRun(
"array-prototype-element-js",
`Array.prototype[0] = "proto";
console.log("unreachable");
`,
"cjs",
);
expect(write.exitCode).toBe(1);
expect(write.stdout).toBe("");
expect(write.stderr).toMatch(
/^Uncaught Error: Elements on Array\.prototype are not supported yet \[SC2020[^\]]*\]\n$/,
);
const push = await compileAndRun(
"array-prototype-push-js",
`Array.prototype.push("proto");
console.log("unreachable");
`,
"cjs",
);
expect(push.exitCode).toBe(1);
expect(push.stdout).toBe("");
expect(push.stderr).toMatch(
/^Uncaught Error: Elements on Array\.prototype are not supported yet \[SC2020[^\]]*\]\n$/,
);
});
});