871 lines
29 KiB
TypeScript
871 lines
29 KiB
TypeScript
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 activeEngineSupported =
|
||
activeFlavour === "ecmascript" || activeFlavour === "pcre2";
|
||
|
||
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: activeEngineSupported
|
||
? activeFlavourVersion
|
||
: ECMASCRIPT.defaultVersion,
|
||
pattern: initialPattern,
|
||
flags: activeEngineSupported ? activeFlags : ECMASCRIPT.defaultFlags,
|
||
options: activeEngineSupported
|
||
? activeOptions
|
||
: defaultRegexOptions(ECMASCRIPT),
|
||
...(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 () => {
|
||
if (mode !== "comparison-mismatch" && !activeEngineSupported) {
|
||
setStatus(
|
||
`Subject minimization is not yet implemented for ${activeFlavour}; select ECMAScript or PCRE2, or use the explicit ECMAScript ↔ PCRE2 comparison target.`,
|
||
);
|
||
return;
|
||
}
|
||
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>
|
||
{!activeEngineSupported ? (
|
||
<p className="minimizer-advisory" role="status">
|
||
Python and Java execution remain available in the main workbench,
|
||
corpus and unit-test modes. This reducer currently has verified
|
||
failure oracles only for ECMAScript and PCRE2.
|
||
</p>
|
||
) : null}
|
||
|
||
<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 ||
|
||
(mode !== "comparison-mismatch" && !activeEngineSupported)
|
||
}
|
||
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>
|
||
);
|
||
}
|