Files
scriptc/tests/harness/regex.test.ts
Chris Tate c08ab955ae Enforce linting and consistent source formatting
- Enforce oxlint and oxfmt for owned JavaScript and TypeScript with independent CI checks.
- Preserve compiler import boundaries and protected fixtures while cleaning up lint findings.
- Standardize source formatting and validate import, fixture, and embedded-source protections.
2026-10-05 17:51:24 -05:00

139 lines
6.2 KiB
TypeScript

/* Regex behavior that is scriptc-only by nature — deliberately NOT in the
* differential corpus (tests/corpus/1200.. holds everything Node-comparable):
*
* - The two size/stability pins: a regex-free program must not reference
* the regex runtime at all (byte-identical link line — its C names no
* ScrRegex symbol), and a regex-USING static binary pays libregexp
* (~110KB), never the ~620KB engine class: well under 300KB.
*
* SCRIPTC_SAN=1 builds the programs with ASan + the RC audit.
*/
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
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";
const platformTest = process.env["SCRIPTC_PORTABLE_ONLY"] === "1" ? test.skip : test;
interface BuildResult {
binaryPath: string;
llvmPath: string;
}
/** Compiles an inline program (static by default — regex needs no flag). */
async function build(
name: string,
source: string,
opts: { sanitize?: boolean } = {},
): Promise<BuildResult> {
const san = opts.sanitize ?? sanitize;
const key = createHash("sha256")
.update(source)
.update(san ? "san" : "plain")
.digest("hex")
.slice(0, 16);
const outDir = join(cacheDir, `regex-${key}`);
mkdirSync(outDir, { recursive: true });
const file = join(outDir, `${name}.ts`);
writeFileSync(file, source);
const result = await compile(file, {
outPath: join(outDir, name),
outDir,
sanitize: san,
backend: "llvm",
});
if (!result.ok) {
throw new Error(
"regex program failed to compile:\n" +
result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
);
}
return { binaryPath: result.binaryPath, llvmPath: result.llvmPath };
}
describe("regex (scriptc-only behavior)", () => {
test("a pattern the engine rejects aborts at first use with a SyntaxError message", async () => {
// tsc's parser (and V8) accept 300 capture groups; libregexp caps
// captures at 255, so the engine rejects the pattern at its LAZY
// compile on first use — Node would run it fine (the abort is the
// documented divergence surface: engine-level rejections happen at
// first use, not at program parse time).
const r = await build(
"lazy-compile-error",
`console.log("before");
console.log(/${"(a)".repeat(300)}/.test("a"));
`,
);
const err = await execFileAsync(r.binaryPath, []).then(
() => {
throw new Error("expected the invalid pattern to abort");
},
(e: Error & { signal?: string; stdout?: string; stderr?: string }) => e,
);
expect(err.signal).toBe("SIGABRT");
expect(err.stdout).toContain("before");
expect(err.stderr).toContain("SyntaxError: Invalid regular expression:");
});
platformTest(
"regex-free programs never reference the regex runtime; regex use stays in its size class",
async () => {
// Measured on plain (non-ASan) builds. A regex-free program's C names
// no ScrRegex symbol, so its link line is the historical one (the
// compact static size class is pinned by island.test.ts). A regex-USING
// binary links libregexp + libunicode, but must stay far below the
// embedded-engine size class.
const [plainBuild, regexBuild] = await Promise.all([
build("size-plain", `console.log("hello", "world");\n`, { sanitize: false }),
build(
"size-regex",
`console.log("a-b c".replace(/[-\\s]/g, "_"), /\\p{L}+/u.test("héllo"));\n`,
{ sanitize: false },
),
]);
const plainC = readFileSync(plainBuild.llvmPath, "utf8");
expect(plainC).not.toContain("@scr_regex");
// The class bounds are page-granular (macOS rounds segments to 16KB,
// so a few hundred bytes of new runtime can tip a whole page): the net
// loop hooks, the console/process/child surface (the piped-stream
// slice included — scr_child.c is always linked), the
// fs-scandir/Math-static slice, the path.win32 port, and the Buffer
// numeric/encoding/search families (~11KB of always-linked bytes
// runtime) each cost pages — regex linkage would be a ~110KB jump
// above the static class, the engine a ~620KB one. The regex class
// carries scr_assert.c (the regex link switch has always pulled it),
// so the assert.throws shape machinery — the Comparison diff renderer
// and the throws/rejects helpers — re-based it by a page, and the
// dyn-assert machinery (the compact:false dyn renderer plus the real
// myers line-diff printer) re-based it by another; the primitive-
// prototype formatters (toExponential/toFixed0, Date.UTC, String.raw)
// re-based the static class by one more; the ambient-receiver stack,
// the %j dyn stringify walk, and the runtime-encoding readFileSync
// form (all in always-linked TUs) tipped the regex class one more
// page; positioned readSync's cross-platform pread seam tipped the
// Mach-O regex class by one further page. Native Response construction
// and mutable constructed Headers added another page to both Darwin
// classes; the Linux bounds were rebased by the same 16KB while
// preserving their existing cushion.
// The sparse UTF-16 string index adds its cache/allocator path to the
// always-linked string unit: the Ubuntu 24.04/clang Sandbox measures
// 423,488 bytes for the plain binary and 580,264 with regex linked.
// The bounds retain roughly one native page of growth; neither cushion
// can hide an engine-sized jump.
expect(statSync(plainBuild.binaryPath).size).toBeLessThan(
process.platform === "linux" ? 440_000 : 450_000,
);
expect(statSync(regexBuild.binaryPath).size).toBeLessThan(
process.platform === "linux" ? 601_000 : 590_000,
);
},
);
});