feat: add Python and Java regex engines

This commit is contained in:
2026-07-27 02:02:21 +02:00
parent 4951c966d4
commit 7079cde15f
95 changed files with 8866 additions and 251 deletions

View File

@@ -0,0 +1,123 @@
import { expect, test, type Page } from "@playwright/test";
async function setEditor(page: Page, testId: string, value: string) {
await page.getByTestId(testId).locator(".cm-content").fill(value);
}
async function waitForReady(page: Page, timeout = 15_000) {
await expect(page.locator(".run-status")).toHaveClass(/status-ready/u, {
timeout,
});
}
test("CPython re executes and replaces with code-point-native offsets under the production CSP", async ({
page,
}) => {
const pageErrors: string[] = [];
const consoleErrors: string[] = [];
page.on("pageerror", (error) => pageErrors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") consoleErrors.push(message.text());
});
const response = await page.goto("/deep/nested/regex/");
expect(response?.headers()["content-security-policy"]).toContain(
"script-src 'self' 'wasm-unsafe-eval'",
);
expect(response?.headers()["content-security-policy"]).not.toContain(
"'unsafe-eval'",
);
await waitForReady(page);
await page.getByLabel("Live").uncheck();
await page.getByRole("combobox", { name: "Flavour" }).selectOption("python");
await setEditor(page, "editor-regular-expression-pattern", "(?P<word>\\w+)");
await setEditor(page, "editor-test-text", "😀 café");
await page.getByRole("button", { name: "Run", exact: true }).click();
await waitForReady(page, 45_000);
await expect(page.locator(".run-status")).toContainText(
"native offsets code-point",
);
const match = page.locator(".extraction-row").first();
await expect(match).toContainText("UTF-16 range: 3…7");
await expect(match).toContainText("native range: 2…6 code-point");
await page.getByRole("button", { name: "Replace", exact: true }).click();
await setEditor(page, "editor-replacement-template", "[\\g<word>]");
await page.getByRole("button", { name: "Run", exact: true }).click();
await waitForReady(page, 45_000);
await expect(
page.getByTestId("editor-replacement-output").locator(".cm-content"),
).toHaveText("😀 [café]");
await page.getByRole("button", { name: "Quick reference" }).click();
await expect(
page.getByRole("dialog", { name: "Quick reference" }),
).toContainText("Python 3.14 re documentation");
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Capabilities" }).click();
const capabilities = page.getByRole("dialog", { name: "Capabilities" });
await expect(capabilities).toContainText("CPython re via Pyodide");
await expect(capabilities).toContainText("CPython 3.14.2 re");
expect(pageErrors).toEqual([]);
expect(consoleErrors).toEqual([]);
});
test("TeaVM java.util.regex executes Java-only syntax and numeric replacement with UTF-16 offsets", async ({
page,
}) => {
const pageErrors: string[] = [];
page.on("pageerror", (error) => pageErrors.push(error.message));
await page.goto("/deep/nested/regex/");
await waitForReady(page);
await page.getByLabel("Live").uncheck();
await page.getByRole("combobox", { name: "Flavour" }).selectOption("java");
await setEditor(
page,
"editor-regular-expression-pattern",
"(?<word>[a-z]++)",
);
await setEditor(page, "editor-test-text", "😀 abc");
await page.getByRole("button", { name: "Run", exact: true }).click();
await waitForReady(page);
await expect(page.locator(".run-status")).toContainText(
"native offsets utf16",
);
const match = page.locator(".extraction-row").first();
await expect(match).toContainText("UTF-16 range: 3…6");
await expect(match).toContainText("native range: 3…6 utf16");
await page.getByRole("button", { name: "Replace", exact: true }).click();
await setEditor(page, "editor-replacement-template", "<$1>");
await page.getByRole("button", { name: "Run", exact: true }).click();
await waitForReady(page);
await expect(
page.getByTestId("editor-replacement-output").locator(".cm-content"),
).toHaveText("😀 <abc>");
await page.getByRole("button", { name: "Capabilities" }).click();
const capabilities = page.getByRole("dialog", { name: "Capabilities" });
await expect(capabilities).toContainText("TeaVM java.util.regex");
await expect(capabilities).toContainText("not OpenJDK");
expect(pageErrors).toEqual([]);
});
test("serves the self-hosted Python and Java runtime assets with executable MIME types", async ({
request,
}) => {
const pythonModule = await request.get("./engines/python/pyodide.asm.mjs");
expect(pythonModule.ok()).toBe(true);
expect(pythonModule.headers()["content-type"]).toContain("text/javascript");
const pythonWasm = await request.get("./engines/python/pyodide.asm.wasm");
expect(pythonWasm.ok()).toBe(true);
expect(pythonWasm.headers()["content-type"]).toContain("application/wasm");
expect((await pythonWasm.body()).subarray(0, 4)).toEqual(
Buffer.from([0x00, 0x61, 0x73, 0x6d]),
);
const javaModule = await request.get("./engines/java/java-regex.mjs");
expect(javaModule.ok()).toBe(true);
expect(javaModule.headers()["content-type"]).toContain("text/javascript");
});

View File

@@ -0,0 +1,294 @@
// @vitest-environment node
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { beforeAll, describe, expect, it } from "vitest";
import {
JavaEngineAdapter,
JAVA_ENGINE_IDENTITY,
JAVA_ENGINE_VERSION,
} from "../../src/regex/execution/adapters/java/JavaEngineAdapter";
import type { TeaVmJavaRegexModule } from "../../src/regex/execution/adapters/java/java-module";
import type {
RegexExecutionRequest,
RegexReplacementRequest,
} from "../../src/regex/model/match";
const pack = path.resolve("public/engines/java");
let adapter: JavaEngineAdapter;
function request(
pattern: string,
subject: string,
overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
return {
flavour: "java",
flavourVersion: JAVA_ENGINE_IDENTITY,
pattern,
flags: ["g"],
subject,
captureMetadata: [],
scanAll: false,
maximumMatches: 100,
maximumCaptureRows: 1_000,
...overrides,
};
}
function replacement(
pattern: string,
subject: string,
replacementText: string,
overrides: Partial<RegexReplacementRequest> = {},
): RegexReplacementRequest {
return {
...request(pattern, subject),
replacement: replacementText,
maximumOutputBytes: 1_000,
...overrides,
};
}
beforeAll(async () => {
const imported = (await import(
`${pathToFileURL(path.join(pack, "java-regex.mjs")).href}?conformance`
)) as unknown as TeaVmJavaRegexModule;
adapter = new JavaEngineAdapter(
imported,
"TeaVM JavaScript · Node.js conformance · not OpenJDK",
);
await adapter.load();
});
describe("pinned TeaVM 0.15.0 java.util.regex conformance", () => {
it("loads the exact compatibility engine identity", async () => {
await expect(adapter.load()).resolves.toEqual(
expect.objectContaining({
engineName: "TeaVM java.util.regex",
engineVersion: JAVA_ENGINE_VERSION,
runtimeVersion: expect.stringContaining("not OpenJDK"),
offsetUnit: "utf16",
}),
);
});
it("returns native UTF-16 offsets and named/numbered metadata", async () => {
const result = await adapter.execute(
request("(?<emoji>😀)([A-Z]+)?", "😀AB 😀", {
captureMetadata: [
{
number: 1,
name: "emoji",
range: { startUtf16: 0, endUtf16: 12 },
repeated: false,
},
{
number: 2,
name: "tail",
range: { startUtf16: 12, endUtf16: 21 },
repeated: false,
},
],
}),
);
expect(result.accepted).toBe(true);
expect(result.matches.map(({ range }) => range)).toEqual([
{ startUtf16: 0, endUtf16: 4 },
{ startUtf16: 5, endUtf16: 7 },
]);
expect(result.matches[0]?.captures).toEqual([
expect.objectContaining({
groupNumber: 1,
groupName: "emoji",
value: "😀",
range: { startUtf16: 0, endUtf16: 2 },
}),
expect.objectContaining({
groupNumber: 2,
groupName: "tail",
value: "AB",
range: { startUtf16: 2, endUtf16: 4 },
}),
]);
expect(result.matches[1]?.captures[1]).toEqual({
groupNumber: 2,
groupName: "tail",
status: "did-not-participate",
});
});
it("supports TeaVM lookbehind, numeric backreferences and atomic groups", async () => {
const lookbehind = await adapter.execute(request("(?<=a)(b)\\1", "abb ab"));
expect(lookbehind.matches.map(({ value }) => value)).toEqual(["bb"]);
const atomic = await adapter.execute(request("(?>a|ab)c", "abc"));
expect(atomic.matches).toHaveLength(0);
});
it("advances global zero-length matches in UTF-16 without looping", async () => {
const result = await adapter.execute(request("(?=.)", "😀x"));
expect(result.matches.map(({ range }) => range)).toEqual([
{ startUtf16: 0, endUtf16: 0 },
{ startUtf16: 1, endUtf16: 1 },
{ startUtf16: 2, endUtf16: 2 },
]);
});
it("maps i, m, s, u and x to TeaVM Pattern flags", async () => {
const cases = [
["a", "A", ["i"]],
["ä", "Ä", ["i", "u"]],
["^b$", "a\nb\nc", ["m"]],
["a.b", "a\nb", ["s"]],
["a b # comment", "ab", ["x"]],
] as const;
for (const [pattern, subject, flags] of cases) {
const result = await adapter.execute(
request(pattern, subject, { flags: [...flags] }),
);
expect(result.matches, `${flags.join("")}: ${pattern}`).toHaveLength(1);
}
const normalized = await adapter.execute(
request("a", "A", { flags: ["i", "m", "s", "u", "x"] }),
);
expect(normalized.flags.effectiveFlags).toBe("imsux");
});
it("maps d to UNIX_LINES and reports the honest U compatibility boundary", async () => {
const ordinaryLines = await adapter.execute(
request(".", "\r", { flags: [] }),
);
const unixLines = await adapter.execute(
request(".", "\r", { flags: ["d"] }),
);
expect(ordinaryLines.matches).toHaveLength(0);
expect(unixLines.matches).toHaveLength(1);
const result = await adapter.execute(
request("\\w+", "é", { flags: ["d", "U"] }),
);
expect(result.accepted).toBe(true);
expect(result.matches).toHaveLength(0);
expect(result.flags.userFlags).toBe("dU");
expect(result.flags.effectiveFlags).toBe("duU");
expect(result.flags.internallyAddedIndicesFlag).toBe(false);
expect(result.diagnostics[0]).toEqual(
expect.objectContaining({
code: "engine-compatibility",
message: expect.stringContaining("does not implement OpenJDK"),
}),
);
});
it("uses TeaVM Matcher native single-digit $n replacement", async () => {
const result = await adapter.replace(replacement("(a)", "a a", "<$1>"));
expect(result.output).toBe("<a> <a>");
expect(result.outputTruncated).toBe(false);
expect(result.execution.matches).toHaveLength(2);
const escaped = await adapter.replace(replacement("(a)", "a a", "\\$1"));
expect(escaped.output).toBe("$1 $1");
});
it("rejects ${name}, which TeaVM 0.15.0 does not support", async () => {
const unsupportedNamed = await adapter.replace(
replacement("(?<name>a)", "a", "${name}"),
);
expect(unsupportedNamed.execution.accepted).toBe(false);
expect(unsupportedNamed.execution.diagnostics[0]).toEqual(
expect.objectContaining({
code: "replacement-error",
message: expect.stringMatching(/IllegalArgumentException/u),
}),
);
});
it("returns compile diagnostics and enforces match/capture/output caps", async () => {
const invalid = await adapter.execute(request("(", "x"));
expect(invalid.accepted).toBe(false);
expect(invalid.diagnostics[0]).toEqual(
expect.objectContaining({
code: "compile-error",
provenance: "reported",
}),
);
const matchLimited = await adapter.execute(
request("a", "aaa", { maximumMatches: 1 }),
);
expect(matchLimited.matches).toHaveLength(1);
expect(matchLimited.truncated).toBe(true);
const captureLimited = await adapter.execute(
request("(a)(b)", "ab", {
maximumCaptureRows: 1,
captureMetadata: [
{
number: 1,
range: { startUtf16: 0, endUtf16: 3 },
repeated: false,
},
{
number: 2,
range: { startUtf16: 3, endUtf16: 6 },
repeated: false,
},
],
}),
);
expect(captureLimited.matches).toHaveLength(0);
expect(captureLimited.truncated).toBe(true);
const outputLimited = await adapter.replace(
replacement("a", "aaaa", "long", { maximumOutputBytes: 5 }),
);
expect(outputLimited.output).toBe("longl");
expect(outputLimited.outputBytes).toBe(5);
expect(outputLimited.outputTruncated).toBe(true);
});
it("ships internally consistent source identity, licence and checksums", async () => {
const metadata = JSON.parse(
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
) as {
engineName: string;
engineVersion: string;
semanticIdentity: string;
source: {
tag: string;
commitSha1: string;
license: string;
};
};
expect(metadata).toEqual(
expect.objectContaining({
engineName: "TeaVM java.util.regex",
engineVersion: "0.15.0",
semanticIdentity: expect.stringContaining("not OpenJDK"),
source: expect.objectContaining({
tag: "0.15.0",
commitSha1: "ee91b03e616c4b45401cd11fb0cd7eb0daf6649b",
license: "Apache-2.0",
}),
}),
);
const checksumText = await readFile(path.join(pack, "SHA256SUMS"), "utf8");
for (const line of checksumText.trim().split("\n")) {
const [expected, name] = line.split(/ {2}/u);
const contents = await readFile(path.join(pack, name ?? ""));
expect(createHash("sha256").update(contents).digest("hex"), name).toBe(
expected,
);
}
expect(await readFile(path.join(pack, "LICENSE.txt"), "utf8")).toContain(
"Apache License",
);
});
});

View File

@@ -0,0 +1,240 @@
// @vitest-environment node
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
PythonEngineAdapter,
PYODIDE_ENGINE_VERSION,
PYTHON_ENGINE_VERSION,
} from "../../src/regex/execution/adapters/python/PythonEngineAdapter";
import type {
PyodideModule,
PyodideRuntime,
} from "../../src/regex/execution/adapters/python/python-module";
import type {
RegexExecutionRequest,
RegexReplacementRequest,
} from "../../src/regex/model/match";
const engineDirectory = path.resolve(
process.cwd(),
"public",
"engines",
"python",
);
function request(
overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
return {
flavour: "python",
flavourVersion: PYTHON_ENGINE_VERSION,
pattern: "a",
flags: ["g"],
subject: "a",
captureMetadata: [],
scanAll: false,
maximumMatches: 100,
maximumCaptureRows: 1_000,
...overrides,
};
}
describe("CPython re 3.14.2 conformance through self-hosted Pyodide", () => {
let runtime: PyodideRuntime;
let adapter: PythonEngineAdapter;
beforeAll(async () => {
const moduleUrl = pathToFileURL(path.join(engineDirectory, "pyodide.mjs"));
const imported = (await import(
/* @vite-ignore */ moduleUrl.href
)) as PyodideModule;
expect(imported.version).toBe(PYODIDE_ENGINE_VERSION);
runtime = await imported.loadPyodide({
indexURL: `${engineDirectory}${path.sep}`,
stdout: () => undefined,
stderr: () => undefined,
});
adapter = new PythonEngineAdapter(runtime, "Node conformance runtime");
await adapter.load();
}, 30_000);
afterAll(() => {
adapter?.terminate();
});
it("reports the exact engine, runtime and native offset identity", async () => {
await expect(adapter.load()).resolves.toEqual(
expect.objectContaining({
flavour: "python",
engineName: "CPython re via Pyodide",
engineVersion: "CPython 3.14.2 re",
runtimeVersion: expect.stringContaining("Pyodide 314.0.3"),
offsetUnit: "code-point",
}),
);
});
it("uses native Python named groups and preserves optional captures", async () => {
const result = await adapter.execute(
request({
pattern: "(?P<word>[A-Za-z]+)(?:-(\\d+))?",
subject: "abc-12 xyz",
}),
);
expect(result.accepted).toBe(true);
expect(result.matches).toHaveLength(2);
expect(result.matches[0]?.captures).toEqual([
expect.objectContaining({
groupNumber: 1,
groupName: "word",
value: "abc",
}),
expect.objectContaining({ groupNumber: 2, value: "12" }),
]);
expect(result.matches[1]?.captures).toEqual([
expect.objectContaining({
groupNumber: 1,
groupName: "word",
value: "xyz",
}),
{ groupNumber: 2, status: "did-not-participate" },
]);
});
it("normalizes astral and zero-length code-point offsets to UTF-16", async () => {
const result = await adapter.execute(
request({
pattern: "(?=.)",
subject: "😀a",
}),
);
expect(
result.matches.map((match) => ({
native: match.nativeRange,
editor: match.range,
status: match.valueStatus,
})),
).toEqual([
{
native: { start: 0, end: 0, unit: "code-point" },
editor: { startUtf16: 0, endUtf16: 0 },
status: "complete",
},
{
native: { start: 1, end: 1, unit: "code-point" },
editor: { startUtf16: 2, endUtf16: 2 },
status: "complete",
},
]);
});
it("implements g, a, i, m, s and x with scan-all normalization", async () => {
const combined = await adapter.execute(
request({
pattern: "^ a . b $",
flags: ["i", "m", "s", "x"],
subject: "z\nA\nB\nq",
scanAll: true,
}),
);
expect(combined.accepted).toBe(true);
expect(combined.flags.effectiveFlags).toBe("gimsx");
expect(combined.flags.internallyAddedGlobalFlag).toBe(true);
expect(combined.matches.map((match) => match.value)).toEqual(["A\nB"]);
const ascii = await adapter.execute(
request({
pattern: "\\w+",
flags: ["g", "a"],
subject: "é az",
}),
);
expect(ascii.matches.map((match) => match.value)).toEqual(["az"]);
});
it("uses native match.expand syntax for named, numbered and zero-length replacement", async () => {
const replacementRequest: RegexReplacementRequest = {
...request({
pattern: "(?P<word>[a-z]+)-(\\d+)",
subject: "a-1 b-22",
}),
replacement: "\\g<word>[\\2]",
maximumOutputBytes: 1_024,
};
const replaced = await adapter.replace(replacementRequest);
expect(replaced.execution.accepted).toBe(true);
expect(replaced.output).toBe("a[1] b[22]");
expect(replaced.outputTruncated).toBe(false);
const zeroLength = await adapter.replace({
...request({
pattern: "(?=.)",
subject: "😀a",
}),
replacement: "|",
maximumOutputBytes: 1_024,
});
expect(zeroLength.output).toBe("|😀|a");
});
it("bounds UTF-8 output without splitting an astral scalar", async () => {
const result = await adapter.replace({
...request({ pattern: ".", subject: "abc" }),
replacement: "😀",
maximumOutputBytes: 5,
});
expect(result.execution.accepted).toBe(true);
expect(result.output).toBe("😀");
expect(result.outputBytes).toBe(4);
expect(result.outputTruncated).toBe(true);
expect(result.truncated).toBe(true);
});
it("reports native compile and replacement errors", async () => {
const compileError = await adapter.execute(request({ pattern: "😀(" }));
expect(compileError.accepted).toBe(false);
expect(compileError.diagnostics[0]).toEqual(
expect.objectContaining({
code: "compile-error",
range: { startUtf16: 2, endUtf16: 2 },
}),
);
const replacementError = await adapter.replace({
...request({ pattern: "(?P<word>a)" }),
replacement: "\\g<missing>",
maximumOutputBytes: 1_024,
});
expect(replacementError.execution.accepted).toBe(false);
expect(replacementError.execution.diagnostics[0]?.code).toBe(
"replacement-error",
);
});
it("enforces authoritative match and capture-row collection caps", async () => {
const matchCap = await adapter.execute(
request({
pattern: "a",
subject: "aaa",
maximumMatches: 1,
}),
);
expect(matchCap.matches).toHaveLength(1);
expect(matchCap.truncated).toBe(true);
const rowCap = await adapter.execute(
request({
pattern: "(a)(b)",
subject: "ab",
maximumCaptureRows: 1,
}),
);
expect(rowCap.matches).toHaveLength(0);
expect(rowCap.truncated).toBe(true);
});
});