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,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);
});
});