feat: publish Regex Tools 0.2.0
This commit is contained in:
848
src/components/MinimizePanel.tsx
Normal file
848
src/components/MinimizePanel.tsx
Normal file
@@ -0,0 +1,848 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { writeClipboardText } from "../browser/clipboard";
|
||||
import {
|
||||
AVAILABLE_REGEX_FLAVOURS,
|
||||
defaultRegexOptions,
|
||||
} from "../regex/flavours/flavour-registry";
|
||||
import {
|
||||
DEFAULT_REGEX_LIMITS,
|
||||
utf8ByteLength,
|
||||
} from "../regex/execution/request-limits";
|
||||
import type {
|
||||
RegexEngineOptions,
|
||||
RegexFlavourId,
|
||||
} from "../regex/model/flavour";
|
||||
import type { RegexTestExpectation } from "../regex/tests/test-case.types";
|
||||
import { SubjectMinimizer } from "../regex/minimization/SubjectMinimizer";
|
||||
import type {
|
||||
EngineFailureOracle,
|
||||
SubjectMinimizationProgress,
|
||||
SubjectMinimizationRequest,
|
||||
SubjectMinimizationResult,
|
||||
} from "../regex/minimization/minimization.types";
|
||||
import "./MinimizePanel.css";
|
||||
|
||||
type MinimizationMode =
|
||||
| "unit-test-failure"
|
||||
| "capture-range-mismatch"
|
||||
| "engine-timeout"
|
||||
| "comparison-mismatch";
|
||||
|
||||
type UnitExpectationKind = RegexTestExpectation["kind"];
|
||||
|
||||
export interface SubjectMinimizerClient {
|
||||
minimize(
|
||||
request: SubjectMinimizationRequest,
|
||||
onProgress?: (progress: SubjectMinimizationProgress) => void,
|
||||
): Promise<SubjectMinimizationResult>;
|
||||
cancel(): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface MinimizePanelProps {
|
||||
readonly activeFlavour: RegexFlavourId;
|
||||
readonly activeFlavourVersion: string;
|
||||
readonly activeFlags: readonly string[];
|
||||
readonly activeOptions: RegexEngineOptions;
|
||||
readonly pattern: string;
|
||||
readonly subject: string;
|
||||
readonly replacement: string;
|
||||
readonly scanAll: boolean;
|
||||
readonly timeoutMs: number;
|
||||
readonly onUseSubject: (subject: string) => void;
|
||||
readonly onClose: () => void;
|
||||
readonly createMinimizer?: () => SubjectMinimizerClient;
|
||||
}
|
||||
|
||||
const ECMASCRIPT = AVAILABLE_REGEX_FLAVOURS.require("ecmascript");
|
||||
const PCRE2 = AVAILABLE_REGEX_FLAVOURS.require("pcre2");
|
||||
|
||||
function flagsFor(
|
||||
activeFlavour: RegexFlavourId,
|
||||
activeFlags: readonly string[],
|
||||
flavour: "ecmascript" | "pcre2",
|
||||
): readonly string[] {
|
||||
const definition = flavour === "ecmascript" ? ECMASCRIPT : PCRE2;
|
||||
return activeFlavour === flavour
|
||||
? definition.flags
|
||||
.map((flag) => flag.value)
|
||||
.filter((flag) => activeFlags.includes(flag))
|
||||
: definition.defaultFlags;
|
||||
}
|
||||
|
||||
function optionsFor(
|
||||
activeFlavour: RegexFlavourId,
|
||||
activeOptions: RegexEngineOptions,
|
||||
flavour: "ecmascript" | "pcre2",
|
||||
): RegexEngineOptions {
|
||||
if (activeFlavour === flavour) return activeOptions;
|
||||
return defaultRegexOptions(flavour === "ecmascript" ? ECMASCRIPT : PCRE2);
|
||||
}
|
||||
|
||||
function parseCaptureSelector(value: string): {
|
||||
readonly groupNumber?: number;
|
||||
readonly groupName?: string;
|
||||
} {
|
||||
const trimmed = value.trim();
|
||||
const number = Number(trimmed);
|
||||
return trimmed !== "" && Number.isSafeInteger(number) && number > 0
|
||||
? { groupNumber: number }
|
||||
: { groupName: trimmed };
|
||||
}
|
||||
|
||||
function resultLabel(result: SubjectMinimizationResult): string {
|
||||
if (result.locallyMinimal) {
|
||||
return "Locally minimal under the documented transforms";
|
||||
}
|
||||
return result.stopReason.replaceAll("-", " ");
|
||||
}
|
||||
|
||||
function Observation({
|
||||
label,
|
||||
observation,
|
||||
}: {
|
||||
readonly label: string;
|
||||
readonly observation: SubjectMinimizationResult["baseline"];
|
||||
}) {
|
||||
return (
|
||||
<article className={`minimizer-observation is-${observation.status}`}>
|
||||
<strong>{label}</strong>
|
||||
<span>
|
||||
{observation.status} ·{" "}
|
||||
{observation.reproduced ? "predicate reproduced" : "not reproduced"}
|
||||
</span>
|
||||
<p>{observation.summary}</p>
|
||||
{observation.fingerprint.length > 0 ? (
|
||||
<code>{observation.fingerprint.join(" · ")}</code>
|
||||
) : null}
|
||||
{observation.sides?.length ? (
|
||||
<small>
|
||||
{observation.sides
|
||||
.map(
|
||||
(side) =>
|
||||
`${side.flavour}: ${side.status}${
|
||||
side.engineIdentity ? ` · ${side.engineIdentity}` : ""
|
||||
}`,
|
||||
)
|
||||
.join(" | ")}
|
||||
</small>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function MinimizePanel({
|
||||
activeFlavour,
|
||||
activeFlavourVersion,
|
||||
activeFlags,
|
||||
activeOptions,
|
||||
pattern: initialPattern,
|
||||
subject: initialSubject,
|
||||
replacement: initialReplacement,
|
||||
scanAll: initialScanAll,
|
||||
timeoutMs: initialTimeoutMs,
|
||||
onUseSubject,
|
||||
onClose,
|
||||
createMinimizer = () => new SubjectMinimizer(),
|
||||
}: MinimizePanelProps) {
|
||||
const [mode, setMode] = useState<MinimizationMode>("unit-test-failure");
|
||||
const [subject, setSubject] = useState(initialSubject);
|
||||
const [scanAll, setScanAll] = useState(initialScanAll);
|
||||
const [unitKind, setUnitKind] =
|
||||
useState<UnitExpectationKind>("should-not-match");
|
||||
const [expectedCount, setExpectedCount] = useState(1);
|
||||
const [matchIndex, setMatchIndex] = useState(0);
|
||||
const [expectedValue, setExpectedValue] = useState("");
|
||||
const [captureSelector, setCaptureSelector] = useState("1");
|
||||
const [captureStatus, setCaptureStatus] = useState<
|
||||
"participated" | "did-not-participate" | "matched-empty"
|
||||
>("participated");
|
||||
const [checkCaptureValue, setCheckCaptureValue] = useState(false);
|
||||
const [expectedStart, setExpectedStart] = useState(0);
|
||||
const [expectedEnd, setExpectedEnd] = useState(0);
|
||||
const [durationLimitMs, setDurationLimitMs] = useState(10);
|
||||
const [comparisonOperation, setComparisonOperation] = useState<
|
||||
"match" | "replace"
|
||||
>("match");
|
||||
const [ecmaPattern, setEcmaPattern] = useState(initialPattern);
|
||||
const [pcrePattern, setPcrePattern] = useState(initialPattern);
|
||||
const [ecmaReplacement, setEcmaReplacement] = useState(initialReplacement);
|
||||
const [pcreReplacement, setPcreReplacement] = useState(initialReplacement);
|
||||
const [maximumEvaluations, setMaximumEvaluations] = useState(500);
|
||||
const [maximumWallTimeMs, setMaximumWallTimeMs] = useState(15_000);
|
||||
const [candidateTimeoutMs, setCandidateTimeoutMs] = useState(
|
||||
Math.min(
|
||||
DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs,
|
||||
Math.max(1, initialTimeoutMs),
|
||||
),
|
||||
);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [status, setStatus] = useState(
|
||||
"Select an exact failure predicate, then verify the baseline.",
|
||||
);
|
||||
const [progress, setProgress] = useState<SubjectMinimizationProgress>();
|
||||
const [result, setResult] = useState<SubjectMinimizationResult>();
|
||||
const [copyStatus, setCopyStatus] = useState("");
|
||||
const minimizer = useRef<SubjectMinimizerClient | null>(null);
|
||||
const revision = useRef(0);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
revision.current += 1;
|
||||
minimizer.current?.dispose();
|
||||
minimizer.current = null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const ecmaFlags = useMemo(
|
||||
() => flagsFor(activeFlavour, activeFlags, "ecmascript"),
|
||||
[activeFlags, activeFlavour],
|
||||
);
|
||||
const pcreFlags = useMemo(
|
||||
() => flagsFor(activeFlavour, activeFlags, "pcre2"),
|
||||
[activeFlags, activeFlavour],
|
||||
);
|
||||
const ecmaOptions = useMemo(
|
||||
() => optionsFor(activeFlavour, activeOptions, "ecmascript"),
|
||||
[activeFlavour, activeOptions],
|
||||
);
|
||||
const pcreOptions = useMemo(
|
||||
() => optionsFor(activeFlavour, activeOptions, "pcre2"),
|
||||
[activeFlavour, activeOptions],
|
||||
);
|
||||
|
||||
const unitExpectation = (): RegexTestExpectation => {
|
||||
switch (unitKind) {
|
||||
case "should-match":
|
||||
case "should-not-match":
|
||||
case "must-time-out":
|
||||
return { kind: unitKind };
|
||||
case "match-count":
|
||||
return { kind: unitKind, count: expectedCount };
|
||||
case "full-match":
|
||||
return {
|
||||
kind: unitKind,
|
||||
matchIndex,
|
||||
value: expectedValue,
|
||||
};
|
||||
case "capture":
|
||||
return {
|
||||
kind: unitKind,
|
||||
matchIndex,
|
||||
...parseCaptureSelector(captureSelector),
|
||||
status: captureStatus,
|
||||
...(checkCaptureValue ? { value: expectedValue } : {}),
|
||||
};
|
||||
case "replacement":
|
||||
return { kind: unitKind, expected: expectedValue };
|
||||
case "must-complete-within":
|
||||
return { kind: unitKind, milliseconds: durationLimitMs };
|
||||
}
|
||||
};
|
||||
|
||||
const activeSide = {
|
||||
flavour:
|
||||
activeFlavour === "pcre2" ? ("pcre2" as const) : ("ecmascript" as const),
|
||||
flavourVersion: activeFlavourVersion,
|
||||
pattern: initialPattern,
|
||||
flags: activeFlags,
|
||||
options: activeOptions,
|
||||
...(unitKind === "replacement" ? { replacement: initialReplacement } : {}),
|
||||
};
|
||||
|
||||
const request = (): SubjectMinimizationRequest => {
|
||||
let target: SubjectMinimizationRequest["target"];
|
||||
if (mode === "comparison-mismatch") {
|
||||
target = {
|
||||
kind: "comparison-mismatch",
|
||||
operation: comparisonOperation,
|
||||
scanAll,
|
||||
sides: [
|
||||
{
|
||||
flavour: "ecmascript",
|
||||
flavourVersion: ECMASCRIPT.defaultVersion,
|
||||
pattern: ecmaPattern,
|
||||
flags: ecmaFlags,
|
||||
options: ecmaOptions,
|
||||
...(comparisonOperation === "replace"
|
||||
? { replacement: ecmaReplacement }
|
||||
: {}),
|
||||
},
|
||||
{
|
||||
flavour: "pcre2",
|
||||
flavourVersion: PCRE2.defaultVersion,
|
||||
pattern: pcrePattern,
|
||||
flags: pcreFlags,
|
||||
options: pcreOptions,
|
||||
...(comparisonOperation === "replace"
|
||||
? { replacement: pcreReplacement }
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
let oracle: EngineFailureOracle;
|
||||
if (mode === "engine-timeout") {
|
||||
oracle = { kind: "engine-timeout" };
|
||||
} else if (mode === "capture-range-mismatch") {
|
||||
oracle = {
|
||||
kind: "capture-range-mismatch",
|
||||
matchIndex,
|
||||
...parseCaptureSelector(captureSelector),
|
||||
expectedRange: {
|
||||
startUtf16: expectedStart,
|
||||
endUtf16: expectedEnd,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
oracle = {
|
||||
kind: "unit-test-failure",
|
||||
expectation: unitExpectation(),
|
||||
};
|
||||
}
|
||||
target = {
|
||||
kind: "engine",
|
||||
side: activeSide,
|
||||
scanAll,
|
||||
oracle,
|
||||
};
|
||||
}
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
subject,
|
||||
target,
|
||||
budgets: {
|
||||
maximumEvaluations,
|
||||
maximumWallTimeMs,
|
||||
candidateTimeoutMs,
|
||||
},
|
||||
maximumMatches: 1_000,
|
||||
maximumCaptureRows: 10_000,
|
||||
maximumOutputBytes: 1024 * 1024,
|
||||
};
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const currentRevision = ++revision.current;
|
||||
setRunning(true);
|
||||
setResult(undefined);
|
||||
setCopyStatus("");
|
||||
setStatus("Verifying the baseline with the selected real engine worker…");
|
||||
try {
|
||||
minimizer.current ??= createMinimizer();
|
||||
const minimized = await minimizer.current.minimize(request(), (value) => {
|
||||
if (currentRevision !== revision.current) return;
|
||||
setProgress(value);
|
||||
setStatus(
|
||||
`${value.phase.replaceAll("-", " ")} · ${value.evaluations}/${value.maximumEvaluations} evaluations · ${value.acceptedReductions} accepted`,
|
||||
);
|
||||
});
|
||||
if (currentRevision !== revision.current) return;
|
||||
setResult(minimized);
|
||||
setStatus(resultLabel(minimized));
|
||||
} catch (error) {
|
||||
if (currentRevision !== revision.current) return;
|
||||
setStatus(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
if (currentRevision === revision.current) setRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
revision.current += 1;
|
||||
minimizer.current?.cancel();
|
||||
setRunning(false);
|
||||
setStatus("Cancelled; active syntax and engine workers were terminated.");
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className="panel minimize-panel"
|
||||
aria-labelledby="minimize-heading"
|
||||
>
|
||||
<header className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Bounded failing-subject reduction</p>
|
||||
<h2 id="minimize-heading">Minimize a reproducer</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Close subject minimizer"
|
||||
data-dialog-initial-focus
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="minimizer-body">
|
||||
<p className="minimizer-advisory">
|
||||
This reducer proves only local minimality under deterministic
|
||||
Unicode-scalar chunk deletion, single-scalar deletion and canonical
|
||||
replacement. It does not claim a globally or semantically minimal
|
||||
example.
|
||||
</p>
|
||||
|
||||
<section className="minimizer-config" aria-labelledby="minimize-target">
|
||||
<header>
|
||||
<h3 id="minimize-target">Exact predicate</h3>
|
||||
<span>
|
||||
{activeFlavour} · {utf8ByteLength(subject).toLocaleString()} bytes
|
||||
</span>
|
||||
</header>
|
||||
<div className="minimizer-control-grid">
|
||||
<label>
|
||||
<span>Failure kind</span>
|
||||
<select
|
||||
aria-label="Minimization failure kind"
|
||||
value={mode}
|
||||
disabled={running}
|
||||
onChange={(event) =>
|
||||
setMode(event.target.value as MinimizationMode)
|
||||
}
|
||||
>
|
||||
<option value="unit-test-failure">
|
||||
Failing unit-test assertion
|
||||
</option>
|
||||
<option value="capture-range-mismatch">
|
||||
Wrong capture range
|
||||
</option>
|
||||
<option value="engine-timeout">Exact engine timeout</option>
|
||||
<option value="comparison-mismatch">
|
||||
ECMAScript ↔ PCRE2 mismatch
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="check-control minimizer-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={scanAll}
|
||||
disabled={running}
|
||||
onChange={(event) => setScanAll(event.target.checked)}
|
||||
/>
|
||||
Scan all
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{mode === "unit-test-failure" ? (
|
||||
<div className="minimizer-control-grid">
|
||||
<label>
|
||||
<span>Saved assertion</span>
|
||||
<select
|
||||
aria-label="Unit-test expectation"
|
||||
value={unitKind}
|
||||
disabled={running}
|
||||
onChange={(event) =>
|
||||
setUnitKind(event.target.value as UnitExpectationKind)
|
||||
}
|
||||
>
|
||||
<option value="should-match">Should match</option>
|
||||
<option value="should-not-match">Should not match</option>
|
||||
<option value="match-count">Exact match count</option>
|
||||
<option value="full-match">Full-match value</option>
|
||||
<option value="capture">Capture status/value</option>
|
||||
<option value="replacement">Replacement output</option>
|
||||
<option value="must-complete-within">
|
||||
Must complete within
|
||||
</option>
|
||||
<option value="must-time-out">Must time out</option>
|
||||
</select>
|
||||
</label>
|
||||
{unitKind === "match-count" ? (
|
||||
<label>
|
||||
<span>Expected count</span>
|
||||
<input
|
||||
type="number"
|
||||
value={expectedCount}
|
||||
min={0}
|
||||
max={DEFAULT_REGEX_LIMITS.maximumMatches}
|
||||
onChange={(event) =>
|
||||
setExpectedCount(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{unitKind === "full-match" || unitKind === "capture" ? (
|
||||
<label>
|
||||
<span>Zero-based match index</span>
|
||||
<input
|
||||
type="number"
|
||||
value={matchIndex}
|
||||
min={0}
|
||||
onChange={(event) =>
|
||||
setMatchIndex(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{unitKind === "capture" ? (
|
||||
<>
|
||||
<label>
|
||||
<span>Capture number or name</span>
|
||||
<input
|
||||
type="text"
|
||||
value={captureSelector}
|
||||
onChange={(event) =>
|
||||
setCaptureSelector(event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Expected participation</span>
|
||||
<select
|
||||
value={captureStatus}
|
||||
onChange={(event) =>
|
||||
setCaptureStatus(
|
||||
event.target.value as typeof captureStatus,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="participated">Participated</option>
|
||||
<option value="did-not-participate">
|
||||
Did not participate
|
||||
</option>
|
||||
<option value="matched-empty">Matched empty</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="check-control minimizer-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checkCaptureValue}
|
||||
onChange={(event) =>
|
||||
setCheckCaptureValue(event.target.checked)
|
||||
}
|
||||
/>
|
||||
Assert value too
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
{unitKind === "full-match" ||
|
||||
unitKind === "replacement" ||
|
||||
(unitKind === "capture" && checkCaptureValue) ? (
|
||||
<label className="minimizer-wide-control">
|
||||
<span>Expected value</span>
|
||||
<input
|
||||
type="text"
|
||||
value={expectedValue}
|
||||
onChange={(event) => setExpectedValue(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{unitKind === "must-complete-within" ? (
|
||||
<label>
|
||||
<span>Assertion milliseconds</span>
|
||||
<input
|
||||
type="number"
|
||||
value={durationLimitMs}
|
||||
min={1}
|
||||
onChange={(event) =>
|
||||
setDurationLimitMs(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{mode === "capture-range-mismatch" ? (
|
||||
<div className="minimizer-control-grid">
|
||||
<label>
|
||||
<span>Zero-based match index</span>
|
||||
<input
|
||||
type="number"
|
||||
value={matchIndex}
|
||||
min={0}
|
||||
onChange={(event) =>
|
||||
setMatchIndex(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Capture number or name</span>
|
||||
<input
|
||||
type="text"
|
||||
value={captureSelector}
|
||||
onChange={(event) => setCaptureSelector(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Expected UTF-16 start</span>
|
||||
<input
|
||||
type="number"
|
||||
value={expectedStart}
|
||||
min={0}
|
||||
max={subject.length}
|
||||
onChange={(event) =>
|
||||
setExpectedStart(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Expected UTF-16 end</span>
|
||||
<input
|
||||
type="number"
|
||||
value={expectedEnd}
|
||||
min={0}
|
||||
max={subject.length}
|
||||
onChange={(event) =>
|
||||
setExpectedEnd(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{mode === "comparison-mismatch" ? (
|
||||
<>
|
||||
<div className="minimizer-control-grid">
|
||||
<label>
|
||||
<span>Operation</span>
|
||||
<select
|
||||
value={comparisonOperation}
|
||||
onChange={(event) =>
|
||||
setComparisonOperation(
|
||||
event.target.value as "match" | "replace",
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="match">Match</option>
|
||||
<option value="replace">Replace</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="minimizer-variant-grid">
|
||||
<label>
|
||||
<span>ECMAScript pattern</span>
|
||||
<textarea
|
||||
value={ecmaPattern}
|
||||
onChange={(event) => setEcmaPattern(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>PCRE2 pattern</span>
|
||||
<textarea
|
||||
value={pcrePattern}
|
||||
onChange={(event) => setPcrePattern(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
{comparisonOperation === "replace" ? (
|
||||
<>
|
||||
<label>
|
||||
<span>ECMAScript replacement</span>
|
||||
<textarea
|
||||
value={ecmaReplacement}
|
||||
onChange={(event) =>
|
||||
setEcmaReplacement(event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>PCRE2 replacement</span>
|
||||
<textarea
|
||||
value={pcreReplacement}
|
||||
onChange={(event) =>
|
||||
setPcreReplacement(event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<label className="minimizer-subject">
|
||||
<span>Failing subject · local text only</span>
|
||||
<textarea
|
||||
aria-label="Subject to minimize"
|
||||
value={subject}
|
||||
disabled={running}
|
||||
onChange={(event) => setSubject(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="minimizer-config" aria-labelledby="minimize-bounds">
|
||||
<header>
|
||||
<h3 id="minimize-bounds">Hard bounds</h3>
|
||||
<span>Fixed deadline per candidate</span>
|
||||
</header>
|
||||
<div className="minimizer-control-grid">
|
||||
<label>
|
||||
<span>Candidate evaluations</span>
|
||||
<input
|
||||
type="number"
|
||||
value={maximumEvaluations}
|
||||
min={1}
|
||||
max={DEFAULT_REGEX_LIMITS.maximumMinimizerRuns}
|
||||
disabled={running}
|
||||
onChange={(event) =>
|
||||
setMaximumEvaluations(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Aggregate wall time ms</span>
|
||||
<input
|
||||
type="number"
|
||||
value={maximumWallTimeMs}
|
||||
min={1}
|
||||
max={60_000}
|
||||
disabled={running}
|
||||
onChange={(event) =>
|
||||
setMaximumWallTimeMs(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Each candidate ms</span>
|
||||
<input
|
||||
type="number"
|
||||
value={candidateTimeoutMs}
|
||||
min={1}
|
||||
max={DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs}
|
||||
disabled={running}
|
||||
onChange={(event) =>
|
||||
setCandidateTimeoutMs(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="minimizer-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
disabled={running}
|
||||
onClick={() => void run()}
|
||||
>
|
||||
{running ? "Minimizing…" : "Verify & minimize"}
|
||||
</button>
|
||||
{running ? (
|
||||
<button type="button" className="secondary-button" onClick={cancel}>
|
||||
Cancel
|
||||
</button>
|
||||
) : null}
|
||||
<p className="minimizer-status" aria-live="polite">
|
||||
{status}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{progress ? (
|
||||
<div className="minimizer-progress">
|
||||
<progress
|
||||
aria-label="Minimizer evaluation progress"
|
||||
value={progress.evaluations}
|
||||
max={progress.maximumEvaluations}
|
||||
/>
|
||||
<span>
|
||||
{progress.currentScalars.toLocaleString()} scalars ·{" "}
|
||||
{progress.currentUtf8Bytes.toLocaleString()} bytes ·{" "}
|
||||
{progress.elapsedMs.toFixed(0)}/
|
||||
{progress.maximumWallTimeMs.toLocaleString()} ms
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{result ? (
|
||||
<section
|
||||
className="minimizer-result"
|
||||
aria-labelledby="minimize-result"
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<p className="eyebrow">Exact target {result.targetIdentity}</p>
|
||||
<h3 id="minimize-result">{resultLabel(result)}</h3>
|
||||
</div>
|
||||
<span className={`minimizer-result-badge is-${result.status}`}>
|
||||
{result.status}
|
||||
</span>
|
||||
</header>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Scalars</dt>
|
||||
<dd>
|
||||
{result.originalScalars.toLocaleString()} →{" "}
|
||||
{result.minimizedScalars.toLocaleString()}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>UTF-8 bytes</dt>
|
||||
<dd>
|
||||
{result.originalUtf8Bytes.toLocaleString()} →{" "}
|
||||
{result.minimizedUtf8Bytes.toLocaleString()}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Evaluations</dt>
|
||||
<dd>
|
||||
{result.evaluations.toLocaleString()} /{" "}
|
||||
{result.budgets.maximumEvaluations.toLocaleString()}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Accepted reductions</dt>
|
||||
<dd>{result.acceptedReductions.toLocaleString()}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Wall time</dt>
|
||||
<dd>{result.elapsedMs.toFixed(1)} ms</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Stop reason</dt>
|
||||
<dd>{result.stopReason.replaceAll("-", " ")}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="minimizer-observation-grid">
|
||||
<Observation
|
||||
label="Baseline verification"
|
||||
observation={result.baseline}
|
||||
/>
|
||||
<Observation
|
||||
label="Final accepted candidate"
|
||||
observation={result.final}
|
||||
/>
|
||||
</div>
|
||||
<label className="minimizer-output">
|
||||
<span>Minimized subject</span>
|
||||
<textarea
|
||||
aria-label="Minimized subject"
|
||||
readOnly
|
||||
value={result.minimizedSubject}
|
||||
/>
|
||||
</label>
|
||||
<div className="minimizer-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
disabled={!result.final.reproduced}
|
||||
onClick={() => onUseSubject(result.minimizedSubject)}
|
||||
>
|
||||
Use minimized subject
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => {
|
||||
void writeClipboardText(result.minimizedSubject)
|
||||
.then(() => setCopyStatus("Copied locally."))
|
||||
.catch((error: unknown) =>
|
||||
setCopyStatus(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<p aria-live="polite">{copyStatus}</p>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user