Large literals now draw on one heap budget per dossier, measured once before decoding, instead of a separate check per literal. A literal that does not fit is listed in a new swift_string_literal_omissions field with its address, byte length and reason, so consumers read omissions from structured data rather than from limitation text. strings_names comparison checks referenced strings and names first and reports a change there even when Swift literal coverage differs; it returns unknown only when those facts match and literal coverage is absent on one side or partial. A computed jump counts as a tail call only when it has no resolved target inside the function, so jump tables no longer qualify, and a return requires a RETURN p-code operation, so traps no longer qualify. Decoded literals keep control characters, which Swift strings may contain and JSON escapes. The Swift verifier builds the literal fixture for arm64 on any Mac host and now also covers the empty string and a literal containing a tab.
335 lines
9.3 KiB
TypeScript
335 lines
9.3 KiB
TypeScript
import { canonicalJson } from "./comparisonSemantics.js";
|
|
import { diffLines } from "diff";
|
|
|
|
import type { FunctionSnapshot } from "./functionDossierEvidence.js";
|
|
import {
|
|
commentProjection,
|
|
identityProjection,
|
|
isAutoName,
|
|
normalizeCfg,
|
|
referenceKindProjection,
|
|
referenceProjection,
|
|
sorted,
|
|
stringAndNameProjection,
|
|
} from "./functionComparisonNormalization.js";
|
|
import type {
|
|
DimensionName,
|
|
FunctionDimension,
|
|
} from "./functionComparisonSchemas.js";
|
|
import {
|
|
dimensionResult,
|
|
unresolvedDimension,
|
|
type DimensionResultInput,
|
|
} from "./functionComparisonResults.js";
|
|
|
|
type ComparisonContext = Pick<
|
|
DimensionResultInput,
|
|
"links" | "providersDiffer"
|
|
>;
|
|
|
|
export const compareDimensions = (
|
|
left: FunctionSnapshot,
|
|
right: FunctionSnapshot,
|
|
context: ComparisonContext,
|
|
): FunctionDimension[] => {
|
|
return [
|
|
compareIdentity(left, right, context),
|
|
compareText(
|
|
"pseudocode",
|
|
left.dossier.pseudocode,
|
|
right.dossier.pseudocode,
|
|
context,
|
|
),
|
|
compareText(
|
|
"assembly",
|
|
left.dossier.assembly.join("\n"),
|
|
right.dossier.assembly.join("\n"),
|
|
context,
|
|
),
|
|
compareComments(left, right, context),
|
|
compareCalls(left, right, context),
|
|
compareReferences(left, right, context),
|
|
compareStringsAndNames(left, right, context),
|
|
compareCfg(left, right, context),
|
|
];
|
|
};
|
|
|
|
const compareText = (
|
|
dimension: "pseudocode" | "assembly",
|
|
left: string,
|
|
right: string,
|
|
context: ComparisonContext,
|
|
): FunctionDimension => {
|
|
const limitations =
|
|
dimension === "assembly"
|
|
? [
|
|
"Assembly is opaque provider text; relocation normalization is unavailable.",
|
|
]
|
|
: [];
|
|
if (context.providersDiffer)
|
|
return unresolvedDimension({
|
|
dimension,
|
|
links: context.links,
|
|
leftCount: [...left].length,
|
|
rightCount: [...right].length,
|
|
limitations: [
|
|
...limitations,
|
|
"Exact text comparison requires one provider identity.",
|
|
],
|
|
});
|
|
const changes = diffLines(left, right, {
|
|
timeout: 100,
|
|
maxEditLength: 10_000,
|
|
});
|
|
if (changes === undefined)
|
|
return unresolvedDimension({
|
|
dimension,
|
|
links: context.links,
|
|
leftCount: [...left].length,
|
|
rightCount: [...right].length,
|
|
limitations: [
|
|
"Bounded line diff exceeded its time or edit-distance limit.",
|
|
],
|
|
});
|
|
const delta = changes.reduce(
|
|
(summary, change) => ({
|
|
added_lines:
|
|
summary.added_lines + (change.added ? (change.count ?? 0) : 0),
|
|
removed_lines:
|
|
summary.removed_lines + (change.removed ? (change.count ?? 0) : 0),
|
|
hunks: summary.hunks + (change.added || change.removed ? 1 : 0),
|
|
}),
|
|
{ added_lines: 0, removed_lines: 0, hunks: 0 },
|
|
);
|
|
return dimensionResult({
|
|
dimension,
|
|
status: delta.hunks === 0 ? "unchanged" : "changed",
|
|
left,
|
|
right,
|
|
...context,
|
|
leftCount: [...left].length,
|
|
rightCount: [...right].length,
|
|
textDelta: delta,
|
|
limitations,
|
|
});
|
|
};
|
|
|
|
const compareComments = (
|
|
left: FunctionSnapshot,
|
|
right: FunctionSnapshot,
|
|
context: ComparisonContext,
|
|
): FunctionDimension => {
|
|
const leftValues = commentProjection(left);
|
|
const rightValues = commentProjection(right);
|
|
if (leftValues === null || rightValues === null)
|
|
return unresolvedDimension({
|
|
dimension: "comments",
|
|
links: context.links,
|
|
leftCount: left.dossier.comments.length,
|
|
rightCount: right.dossier.comments.length,
|
|
limitations: [
|
|
"Comment locations could not be normalized relative to the function.",
|
|
],
|
|
});
|
|
return compareValues("comments", leftValues, rightValues, context);
|
|
};
|
|
|
|
const compareStringsAndNames = (
|
|
left: FunctionSnapshot,
|
|
right: FunctionSnapshot,
|
|
context: ComparisonContext,
|
|
): FunctionDimension => {
|
|
const complete =
|
|
swiftLiteralCoverage(left) === swiftLiteralCoverage(right) &&
|
|
swiftLiteralCoverage(left) !== "partial";
|
|
// Facts both dossiers record decide a change before Swift literal coverage can.
|
|
const leftValues = stringAndNameProjection(left, complete);
|
|
const rightValues = stringAndNameProjection(right, complete);
|
|
if (leftValues === null || rightValues === null)
|
|
return unresolvedDimension({
|
|
dimension: "strings_names",
|
|
links: context.links,
|
|
leftCount: null,
|
|
rightCount: null,
|
|
limitations: [
|
|
"Reference source locations could not be normalized to function offsets.",
|
|
],
|
|
});
|
|
const compared = compareValues(
|
|
"strings_names",
|
|
leftValues,
|
|
rightValues,
|
|
context,
|
|
);
|
|
if (complete || compared.status === "changed") return compared;
|
|
return unresolvedDimension({
|
|
dimension: "strings_names",
|
|
links: context.links,
|
|
leftCount: null,
|
|
rightCount: null,
|
|
limitations: [
|
|
"Swift String literal coverage differs or is partial; referenced strings and names match, but decoded literals were not compared.",
|
|
],
|
|
});
|
|
};
|
|
|
|
const swiftLiteralCoverage = (
|
|
snapshot: FunctionSnapshot,
|
|
): "absent" | "complete" | "partial" =>
|
|
snapshot.dossier.swift_string_literals === undefined
|
|
? "absent"
|
|
: (snapshot.dossier.swift_string_literal_omissions ?? []).length > 0
|
|
? "partial"
|
|
: "complete";
|
|
|
|
const compareReferences = (
|
|
left: FunctionSnapshot,
|
|
right: FunctionSnapshot,
|
|
context: ComparisonContext,
|
|
): FunctionDimension => {
|
|
const leftProjection = referenceProjection(left);
|
|
const rightProjection = referenceProjection(right);
|
|
if (leftProjection.length === 0 && rightProjection.length === 0)
|
|
return compareValues(
|
|
"references",
|
|
leftProjection,
|
|
rightProjection,
|
|
context,
|
|
);
|
|
if (
|
|
canonicalJson(leftProjection, "Function comparison") !==
|
|
canonicalJson(rightProjection, "Function comparison")
|
|
)
|
|
return dimensionResult({
|
|
dimension: "references",
|
|
status: "changed",
|
|
left: leftProjection,
|
|
right: rightProjection,
|
|
...context,
|
|
leftCount: leftProjection.length,
|
|
rightCount: rightProjection.length,
|
|
textDelta: null,
|
|
limitations: [
|
|
"Reference endpoints differ independently of provider reference-kind metadata.",
|
|
],
|
|
});
|
|
const leftKinds = referenceKindProjection(left);
|
|
const rightKinds = referenceKindProjection(right);
|
|
if (leftKinds !== null && rightKinds !== null)
|
|
return compareValues("references", leftKinds, rightKinds, context);
|
|
return unresolvedDimension({
|
|
dimension: "references",
|
|
links: context.links,
|
|
leftCount: leftProjection.length,
|
|
rightCount: rightProjection.length,
|
|
limitations: [
|
|
"At least one provider did not expose reference kinds, so equal endpoints do not prove equal edge semantics.",
|
|
],
|
|
});
|
|
};
|
|
|
|
const compareIdentity = (
|
|
left: FunctionSnapshot,
|
|
right: FunctionSnapshot,
|
|
context: ComparisonContext,
|
|
): FunctionDimension => {
|
|
if (
|
|
isAutoName(left.dossier.procedure.name) ||
|
|
isAutoName(right.dossier.procedure.name)
|
|
)
|
|
return unresolvedDimension({
|
|
dimension: "identity",
|
|
links: context.links,
|
|
leftCount: null,
|
|
rightCount: null,
|
|
limitations: [
|
|
"Address-derived function names are not stable cross-version identity.",
|
|
],
|
|
});
|
|
return compareValues(
|
|
"identity",
|
|
identityProjection(left),
|
|
identityProjection(right),
|
|
context,
|
|
);
|
|
};
|
|
|
|
const compareCalls = (
|
|
left: FunctionSnapshot,
|
|
right: FunctionSnapshot,
|
|
context: ComparisonContext,
|
|
): FunctionDimension => {
|
|
const leftValues = callProjection(left);
|
|
const rightValues = callProjection(right);
|
|
if ([...leftValues, ...rightValues].some(({ name }) => isAutoName(name)))
|
|
return unresolvedDimension({
|
|
dimension: "calls",
|
|
links: context.links,
|
|
leftCount: leftValues.length,
|
|
rightCount: rightValues.length,
|
|
limitations: [
|
|
"Address-derived callee or caller names cannot be matched safely.",
|
|
],
|
|
});
|
|
return compareValues(
|
|
"calls",
|
|
sorted(leftValues),
|
|
sorted(rightValues),
|
|
context,
|
|
);
|
|
};
|
|
|
|
const callProjection = (snapshot: FunctionSnapshot) => [
|
|
...snapshot.dossier.callers.map(({ name }) => ({
|
|
direction: "in" as const,
|
|
name,
|
|
})),
|
|
...snapshot.dossier.callees.map(({ name }) => ({
|
|
direction: "out" as const,
|
|
name,
|
|
})),
|
|
];
|
|
|
|
const compareCfg = (
|
|
left: FunctionSnapshot,
|
|
right: FunctionSnapshot,
|
|
context: ComparisonContext,
|
|
): FunctionDimension => {
|
|
const leftBlocks = left.dossier.basic_blocks;
|
|
const rightBlocks = right.dossier.basic_blocks;
|
|
const leftGraph = normalizeCfg(leftBlocks);
|
|
const rightGraph = normalizeCfg(rightBlocks);
|
|
if (leftGraph === null || rightGraph === null)
|
|
return unresolvedDimension({
|
|
dimension: "cfg",
|
|
links: context.links,
|
|
leftCount: leftBlocks.length,
|
|
rightCount: rightBlocks.length,
|
|
limitations: [
|
|
"CFG addresses could not be normalized to local block indices.",
|
|
],
|
|
});
|
|
return compareValues("cfg", leftGraph, rightGraph, context);
|
|
};
|
|
|
|
const compareValues = (
|
|
dimension: DimensionName,
|
|
left: unknown,
|
|
right: unknown,
|
|
context: ComparisonContext,
|
|
): FunctionDimension => {
|
|
const leftJson = canonicalJson(left, "Function comparison");
|
|
const rightJson = canonicalJson(right, "Function comparison");
|
|
return dimensionResult({
|
|
dimension,
|
|
status: leftJson === rightJson ? "unchanged" : "changed",
|
|
left,
|
|
right,
|
|
...context,
|
|
leftCount: Array.isArray(left) ? left.length : null,
|
|
rightCount: Array.isArray(right) ? right.length : null,
|
|
textDelta: null,
|
|
limitations: [],
|
|
});
|
|
};
|