mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
- The wrapper check before JSON, util.inspect, structuredClone and deep equality searched the value's graph recursively with only a depth limit, so an object with two references to itself took exponentially many calls, and a wrapper more than 64 levels deep was not found. The search now keeps a set of the objects it reached and an explicit stack: each object and array is searched once, at any depth. - String.prototype, Number.prototype and Boolean.prototype are wrappers of "", 0 and false, as in Node, so their valueOf and toString answer the primitive instead of throwing the brand TypeError. String.prototype has its own `length` of 0. - A string-named method call on a dynamic receiver in a program with wrappers read the member twice: once to check for the builtin String.prototype method and once for the call. The call now reads it once, and wrapper.isStringMethod checks that value. - createsWrappers is listed with the lowerer's first-use caches that the unit journal leaves out. The wrapper corpus program covers the prototypes' values, a getter read once per call and console.log of a 40-level shared graph. The refusal test covers a wrapper 100 levels deep, a wrapper under a shared graph and util.inspect of a cyclic value.
70 lines
2.8 KiB
TypeScript
70 lines
2.8 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { expect, test } from "vitest";
|
|
import { compile } from "@scriptc/compiler";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
// JSON, util.inspect, structuredClone and deep equality cannot read a
|
|
// wrapper's [[PrimitiveValue]] slot yet, so a wrapper anywhere in their
|
|
// argument refuses with a catchable error instead of a silently wrong
|
|
// answer, including one 100 levels deep or under a graph that shares its
|
|
// objects. Values without a wrapper keep working in the same program, and
|
|
// the search reaches each object once, so a cyclic value is checked in
|
|
// linear time.
|
|
test("graph-walking consumers refuse primitive wrapper objects", async () => {
|
|
const outDir = mkdtempSync(join(tmpdir(), "scriptc-wrapper-refusals-"));
|
|
const entry = join(outDir, "main.mjs");
|
|
writeFileSync(
|
|
entry,
|
|
`
|
|
import util from "node:util";
|
|
function attempt(label, run) {
|
|
try { run(); process.stdout.write(label + " ok\\n"); } catch (error) { process.stdout.write(label + " " + error.message + "\\n"); }
|
|
}
|
|
attempt("json", () => JSON.stringify({ v: new Number(3) }));
|
|
attempt("log", () => console.log([new String("a")]));
|
|
attempt("clone", () => structuredClone([new Boolean(true)]));
|
|
attempt("plain json", () => JSON.stringify({ v: 3 }));
|
|
attempt("plain clone", () => structuredClone([true]));
|
|
let deep = new String("deep");
|
|
for (let i = 0; i < 100; i++) deep = { next: deep };
|
|
attempt("deep clone", () => structuredClone(deep));
|
|
let shared = [new Number(1)];
|
|
for (let i = 0; i < 40; i++) shared = { left: shared, right: shared };
|
|
attempt("shared log", () => console.log(shared));
|
|
const cyclic = JSON.parse('{ "name": "cyclic" }');
|
|
cyclic.self = cyclic;
|
|
cyclic.again = cyclic;
|
|
attempt("cyclic inspect", () => util.inspect(cyclic));
|
|
`,
|
|
);
|
|
const result = await compile(entry, {
|
|
backend: "llvm",
|
|
sanitize: process.env["SCRIPTC_SAN"] === "1",
|
|
outDir,
|
|
outPath: join(outDir, "program"),
|
|
});
|
|
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
|
if (!result.ok) return;
|
|
const { stdout } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" });
|
|
const refused = (consumer: string): string =>
|
|
`${consumer} of a primitive wrapper object (new String/Number/Boolean or Object(primitive)) is not supported yet`;
|
|
expect(stdout).toBe(
|
|
[
|
|
`json ${refused("JSON.stringify")}`,
|
|
`log ${refused("console.log/util.inspect")}`,
|
|
`clone ${refused("structuredClone")}`,
|
|
"plain json ok",
|
|
"plain clone ok",
|
|
`deep clone ${refused("structuredClone")}`,
|
|
`shared log ${refused("console.log/util.inspect")}`,
|
|
"cyclic inspect ok",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
});
|