perf(javascript): cache canonical export comparison sort keys (#1028)

This commit is contained in:
morluto
2026-10-08 04:35:32 +08:00
committed by GitHub
parent 28d5c397c8
commit 63ba7424c0
4 changed files with 58 additions and 31 deletions
@@ -1,5 +1,8 @@
import { uniqueSorted } from "../canonicalOrdering.js";
import { digestExportShapeValue } from "./javascriptExportShapeComparisonIdentity.js";
import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js";
import {
canonicalExportShapeValue,
digestExportShapeValue,
} from "./javascriptExportShapeComparisonIdentity.js";
import {
javaScriptExportShapeComparisonResultSchema,
type JavaScriptExportShapeComparisonChange,
@@ -14,7 +17,6 @@ import {
} from "./javascriptExportShapeSelection.js";
import {
buildJavaScriptExportShapeChanges,
compareJavaScriptExportShapeChanges,
hasPartialJavaScriptExportPropertyCoverage,
pairJavaScriptExportShapeVariants,
type JavaScriptExportShapePairing,
@@ -42,14 +44,16 @@ export const compareJavaScriptExportShapes = (
input.left.evidenceId,
input.right.evidenceId,
);
const allChanges = buildJavaScriptExportShapeChanges({
leftSelection: left,
rightSelection: right,
pairing,
leftShapes: leftRetained.shapes,
rightShapes: rightRetained.shapes,
evidenceLinks,
}).sort(compareJavaScriptExportShapeChanges);
const allChanges = sortChanges(
buildJavaScriptExportShapeChanges({
leftSelection: left,
rightSelection: right,
pairing,
leftShapes: leftRetained.shapes,
rightShapes: rightRetained.shapes,
evidenceLinks,
}),
);
const changes = allChanges;
const omittedChanges = 0;
const coverage = comparisonCoverage({
@@ -78,6 +82,29 @@ export const compareJavaScriptExportShapes = (
});
};
const sortChanges = (
changes: readonly JavaScriptExportShapeComparisonChange[],
): JavaScriptExportShapeComparisonChange[] =>
changes
.map((change, index) => ({
change,
index,
sortKey: canonicalExportShapeValue({
discriminant: change.discriminant,
path: change.path,
status: change.status,
left_source_range: change.left_source_range,
right_source_range: change.right_source_range,
change_id: change.change_id,
}),
}))
.sort(
(left, right) =>
compareCodePoints(left.sortKey, right.sortKey) ||
left.index - right.index,
)
.map(({ change }) => change);
interface CoverageInput {
readonly input: JavaScriptExportShapeComparisonProjectionInput;
readonly left: SelectedJavaScriptExport;
@@ -112,16 +112,6 @@ export const buildJavaScriptExportShapeChanges = (
return [...paired, ...unpairedLeft, ...unpairedRight];
};
/** Sort changes by semantic content rather than traversal order. */
export const compareJavaScriptExportShapeChanges = (
left: JavaScriptExportShapeComparisonChange,
right: JavaScriptExportShapeComparisonChange,
): number =>
compareCodePoints(
canonicalExportShapeValue(changeSortKey(left)),
canonicalExportShapeValue(changeSortKey(right)),
);
/** Report whether any retained shape lacks complete property coverage. */
export const hasPartialJavaScriptExportPropertyCoverage = (
shapes: readonly Shape[],
@@ -429,15 +419,6 @@ const changeWithId = (
change_id: `jesc_change_${digestExportShapeValue(semantic)}`,
});
const changeSortKey = (change: JavaScriptExportShapeComparisonChange) => ({
discriminant: change.discriminant,
path: change.path,
status: change.status,
left_source_range: change.left_source_range,
right_source_range: change.right_source_range,
change_id: change.change_id,
});
const literalValue = (
field: Field,
):
@@ -135,9 +135,13 @@ describe("source-produced export comparison inventories", () => {
`export const candidate${String(index)} = ${String(index)};`,
).join("\n");
const inventorySource = `export default function parse(value) {\n${returnSites}\n throw new Error("no match");\n}\n${extraExports}`;
const pairedVariantsSource = `export default function parse(value) {
${returnSites}
throw new Error("no match");
}`;
const [inventoryLeft, inventoryRight] = await Promise.all([
analyzeSourceEvidence(inventorySource),
analyzeSourceEvidence(inventorySource),
analyzeSourceEvidence(pairedVariantsSource),
]);
const variants = compareEvidence(inventoryLeft, inventoryRight);
expect(variants.left).toMatchObject({ status: "selected" });
@@ -88,6 +88,21 @@ describe("JavaScript export return-shape comparison", () => {
expect(result.coverage.status).toBe("partial");
});
it("orders multiple changed paths deterministically across Unicode names", async () => {
const graphs = await analyzeSources({
left: `export default () => ({ type: "item", zeta: 1, alpha: 1, "éclair": 1 });`,
right: `export default () => ({ type: "item", zeta: 2, alpha: 2, "éclair": 2 });`,
});
const first = compare(...graphs);
const second = compare(...graphs);
expect(new Set(first.changes.map(({ path }) => path))).toEqual(
new Set(["/alpha", "/zeta", "/éclair"]),
);
expect(first.changes).toEqual(second.changes);
expect(first.comparison_id).toBe(second.comparison_id);
});
it("does not pair discriminants that an unknown trailing spread can overwrite", async () => {
const [left, right] = await analyzeSources({
left: `