Files
scriptc/tests/harness/strip-vt-linear.test.ts
T
Chris Tate 731462be5c Strip VT control characters in one pass
util.stripVTControlCharacters ran Node's ANSI pattern through the
regex replace loop, which tried the OSC branch's lazy scan at every
introducer. Text with many unterminated OSC introducers took quadratic
time (about 200 s for 100,000 of them).

The function now scans once and gives the same result as Node 24.22's
pattern: the OSC branch ends at the first terminator after ESC ], and
that position is cached across introducers; the CSI branch resolves its
first match without backtracking. A removal that brings lone surrogates
together keeps canonical storage.

Corpus programs 5470-5474 cover OSC and CSI forms, seeded random input,
long unterminated runs and surrogates, and strip-vt-linear.test.ts checks
that 400,000 unterminated introducers finish quickly.
2026-10-14 23:43:52 -05:00

49 lines
1.9 KiB
TypeScript

/* util.stripVTControlCharacters runs in linear time on unterminated OSC
* introducers: Node's pattern, applied by a backtracking matcher at every
* position, rescans the rest of the text from each introducer. */
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { compile } from "@scriptc/compiler";
import { expect, test } from "vitest";
const root = join(import.meta.dirname, "../..");
const entry = join(root, "tests/fixtures/strip-vt/unterminated-osc.mts");
test("stripVTControlCharacters is linear in unterminated OSC introducers", async () => {
const dir = mkdtempSync(
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-strip-vt-"),
);
try {
const built = await compile(entry, {
backend: "llvm",
sanitize: process.env["SCRIPTC_SAN"] === "1",
optimization: "dev",
outDir: dir,
outPath: join(dir, process.platform === "win32" ? "strip.exe" : "strip"),
});
if (!built.ok)
throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
for (const n of [100_000, 400_000]) {
const run = spawnSync(built.binaryPath, [String(n)], { encoding: "utf8", timeout: 60_000 });
expect(run.error).toBeUndefined();
expect(run.status, run.stderr).toBe(0);
const lines = run.stdout
.trim()
.split("\n")
.map((line) => JSON.parse(line) as { length: number; ms: number });
expect(lines).toHaveLength(2);
for (const line of lines) {
// Nothing is removed: each 3-unit introducer stays.
expect(line.length).toBe(3 * n);
// A quadratic scan takes minutes at these sizes; a linear one, a
// few milliseconds (the bound leaves room for sanitized builds).
expect(line.ms).toBeLessThan(5_000);
}
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
}, 180_000);