// @vitest-environment node import path from "node:path"; import { pathToFileURL } from "node:url"; import { beforeAll, describe, expect, it } from "vitest"; import { Pcre2EngineAdapter, PCRE2_ENGINE_VERSION, } from "../../src/regex/execution/adapters/pcre2/Pcre2EngineAdapter"; import { Pcre2TraceAdapter } from "../../src/regex/execution/adapters/pcre2/Pcre2TraceAdapter"; import type { Pcre2ModuleFactory } from "../../src/regex/execution/adapters/pcre2/pcre2-module"; import type { RegexExecutionRequest, RegexReplacementRequest, } from "../../src/regex/model/match"; const pack = path.resolve("public/engines/pcre2"); let adapter: Pcre2EngineAdapter; let traceAdapter: Pcre2TraceAdapter; function request( pattern: string, subject: string, overrides: Partial = {}, ): RegexExecutionRequest { return { flavour: "pcre2", flavourVersion: "PCRE2 10.47 8-bit WebAssembly", pattern, flags: ["g"], options: { matchLimit: 1_000_000, depthLimit: 1_000, heapLimitKib: 32_768, }, subject, captureMetadata: [], scanAll: false, maximumMatches: 100, maximumCaptureRows: 1_000, ...overrides, }; } beforeAll(async () => { const imported = (await import( `${pathToFileURL(path.join(pack, "pcre2.mjs")).href}?conformance` )) as { default: Pcre2ModuleFactory }; const module = await imported.default({ locateFile: (name) => path.join(pack, name), print: () => undefined, printErr: () => undefined, }); adapter = new Pcre2EngineAdapter(module, "Node.js WebAssembly conformance"); traceAdapter = new Pcre2TraceAdapter( module, "Node.js WebAssembly trace conformance", ); await adapter.load(); await traceAdapter.load(); }); describe("pinned PCRE2 10.47 WebAssembly conformance", () => { it("loads the exact production engine identity", async () => { await expect(adapter.load()).resolves.toEqual( expect.objectContaining({ flavour: "pcre2", engineName: "PCRE2 WebAssembly", engineVersion: PCRE2_ENGINE_VERSION, offsetUnit: "utf8-byte", }), ); }); it.each([ { name: "branch-reset groups", pattern: "(?|(a)|(b))-(\\d+)", subject: "b-42", match: "b-42", captures: ["b", "42"], }, { name: "named recursion", pattern: "(?\\((?:[^()]|(?&parentheses))*\\))", subject: "x (a(b)c) y", match: "(a(b)c)", captures: ["(a(b)c)"], }, { name: "match-start reset", pattern: "foo\\Kbar", subject: "foobar", match: "bar", captures: [], }, ])("executes PCRE2-only $name", async (testCase) => { const result = await adapter.execute( request(testCase.pattern, testCase.subject), ); expect(result.accepted).toBe(true); expect(result.matches[0]?.value).toBe(testCase.match); expect( result.matches[0]?.captures .filter((capture) => capture.status !== "did-not-participate") .map((capture) => capture.value), ).toEqual(testCase.captures); }); it("preserves duplicate names from the native name table", async () => { const result = await adapter.execute( request("(?a)|(?b)", "b", { flags: ["J"], }), ); expect(result.accepted).toBe(true); expect(result.matches[0]?.captures).toEqual([ expect.objectContaining({ groupNumber: 1, groupName: "value", status: "did-not-participate", }), expect.objectContaining({ groupNumber: 2, groupName: "value", value: "b", }), ]); }); it.each([ ["i", "^abc$", "AbC", "AbC"], ["m", "^b$", "a\nb\nc", "b"], ["s", "a.b", "a\nb", "a\nb"], ["x", "a # ignored\n b", "ab", "ab"], ["U", "a.+b", "a1b2b", "a1b"], ] as const)( "applies the native %s flag", async (flag, pattern, subject, expected) => { const result = await adapter.execute( request(pattern, subject, { flags: [flag] }), ); expect(result.accepted).toBe(true); expect(result.matches[0]?.value).toBe(expected); }, ); it("always uses PCRE2 UTF and Unicode-property semantics", async () => { const result = await adapter.execute( request("^\\w+$", "Grüße", { flags: [] }), ); expect(result.accepted).toBe(true); expect(result.matches[0]?.value).toBe("Grüße"); }); it("normalizes astral UTF-8 offsets and terminates global empty matches", async () => { const result = await adapter.execute(request("(?=.)", "😀a")); expect(result.accepted).toBe(true); expect(result.matches.map((match) => match.range)).toEqual([ { startUtf16: 0, endUtf16: 0 }, { startUtf16: 2, endUtf16: 2 }, ]); expect(result.matches.map((match) => match.nativeRange)).toEqual([ { start: 0, end: 0, unit: "utf8-byte" }, { start: 4, end: 4, unit: "utf8-byte" }, ]); }); it("uses PCRE2's own CRLF-aware next-match progression", async () => { const result = await adapter.execute(request("(*CRLF)(?m)^", "\r\nx")); expect(result.accepted).toBe(true); expect(result.matches.map((match) => match.nativeRange.start)).toEqual([ 0, 2, ]); }); it("executes native named substitution and reports exact output bytes", async () => { const replacement: RegexReplacementRequest = { ...request("(?\\p{L}+)", "Grüße 42"), replacement: "<${word}>", maximumOutputBytes: 1_024, }; const result = await adapter.replace(replacement); expect(result.execution.accepted).toBe(true); expect(result.output).toBe(" 42"); expect(result.outputBytes).toBe( new TextEncoder().encode(result.output).length, ); expect(result.truncated).toBe(false); }); it("executes PCRE2 subject, highest-capture and mark substitutions", async () => { const result = await adapter.replace({ ...request("(*MARK:route)(a)(b)", "ab", { flags: [] }), replacement: "$_:$+:$*MARK", maximumOutputBytes: 1_024, }); expect(result.execution.accepted).toBe(true); expect(result.output).toBe("ab:b:route"); }); it("marks match and replacement bounds without returning invented completeness", async () => { const limitedMatches = await adapter.execute( request("\\d", "1 2 3", { maximumMatches: 2 }), ); expect(limitedMatches.matches.map((match) => match.value)).toEqual([ "1", "2", ]); expect(limitedMatches.truncated).toBe(true); expect(limitedMatches.diagnostics).toEqual([ expect.objectContaining({ code: "result-limit" }), ]); const limitedOutput = await adapter.replace({ ...request("a", "aaaa"), replacement: "long", maximumOutputBytes: 5, }); expect(limitedOutput.outputTruncated).toBe(true); expect(limitedOutput.truncated).toBe(true); expect( new TextEncoder().encode(limitedOutput.output).length, ).toBeLessThanOrEqual(5); }); it("reports native compile and match-limit failures as failures", async () => { const compile = await adapter.execute(request("é(", "")); expect(compile.accepted).toBe(false); expect(compile.diagnostics[0]).toEqual( expect.objectContaining({ code: "compile-error", range: { startUtf16: 2, endUtf16: 2 }, }), ); const limited = await adapter.execute( request("(a+)+$", `${"a".repeat(4_096)}!`, { flags: [], options: { matchLimit: 100, depthLimit: 100, heapLimitKib: 1_024, }, }), ); expect(limited.accepted).toBe(false); expect(limited.diagnostics[0]).toEqual( expect.objectContaining({ code: "match-limit" }), ); }); it("returns reported automatic callouts and only derived movement labels", async () => { const result = await traceAdapter.trace({ flavour: "pcre2", pattern: "(*MARK:route)a+b", flags: [], options: { matchLimit: 1_000_000, depthLimit: 1_000, heapLimitKib: 32_768, }, subject: "aaab", maximumTraceEvents: 1_000, maximumTraceBytes: 64 * 1024, }); expect(result.accepted).toBe(true); expect(result.matched).toBe(true); expect(result.truncated).toBe(false); expect(result.events.length).toBeGreaterThan(2); expect(result.events.some((event) => event.reported.mark === "route")).toBe( true, ); expect(result.events[0]).toEqual( expect.objectContaining({ eventNumber: 1, reported: expect.objectContaining({ provenance: "reported", calloutNumber: 255, patternPosition: expect.objectContaining({ utf16: expect.any(Number), nativeByte: expect.any(Number), }), }), movement: expect.objectContaining({ provenance: "derived", classification: "first-event", }), }), ); expect( result.events.every( (event) => event.reported.nextPatternItem.startUtf16 <= event.reported.nextPatternItem.endUtf16, ), ).toBe(true); }); it("stops natively at the complete-event trace cap", async () => { const result = await traceAdapter.trace({ flavour: "pcre2", pattern: "(a+)+$", flags: ["g"], options: { matchLimit: 1_000_000, depthLimit: 1_000, heapLimitKib: 32_768, }, subject: "aaaa!", maximumTraceEvents: 1, maximumTraceBytes: 4_096, }); expect(result.accepted).toBe(true); expect(result.events).toHaveLength(1); expect(result.totalEventCount).toBe(2); expect(result.lastCompleteEvent).toBe(1); expect(result.nativeMatchStatus).toBe(-37); expect(result.matched).toBe(false); expect(result.truncated).toBe(true); expect(result.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ code: "trace-single-invocation" }), expect.objectContaining({ code: "trace-limit" }), ]), ); }); it("normalizes trace compile errors to UTF-16", async () => { const result = await traceAdapter.trace({ flavour: "pcre2", pattern: "é(", flags: [], subject: "", maximumTraceEvents: 100, maximumTraceBytes: 4_096, }); expect(result.accepted).toBe(false); expect(result.diagnostics[0]).toEqual( expect.objectContaining({ code: "compile-error", range: { startUtf16: 2, endUtf16: 2 }, }), ); }); });