fix(hopper): normalize native block endpoints and retain terminal evidence

This commit is contained in:
morluto committed 2026-10-09 05:31:49 +08:00
1 parent 4458128a84
commit 2847311a57
6 files changed
+179 -23

No files matched your search

+28
View File
@@ -394,6 +394,33 @@ export async function verifyHopperLifecycleAndCli(client, options, targets) {
JSON.parse(analyzed.stdout).normalized_result,
expectedFunction,
);
let cliTerminalFunctionParity = null;
if (targets.unicode !== undefined) {
const objc = join(directory, `objc-${suffix}`);
await copyFile(targets.unicode, objc);
await call("open_binary", { path: objc });
const procedures = await call("list_procedures");
const delegate = procedures.find(
(item) => item.value === "-[REAWidget delegate]",
);
assert.ok(delegate, "Objective-C fixture omitted its tail-call method");
const expected = await call("analyze_function", {
procedure: delegate.address,
});
await call("close_binary");
const analyzed = await runCli([
dispatcher,
"function",
objc,
delegate.address,
"--provider",
"hopper",
"--format",
"json",
]);
assert.deepEqual(JSON.parse(analyzed.stdout).normalized_result, expected);
cliTerminalFunctionParity = true;
}
let failure;
try {
await runCli([
@@ -424,6 +451,7 @@ export async function verifyHopperLifecycleAndCli(client, options, targets) {
cliInvalidAddress: true,
cliLiteralTraceParity: true,
cliFunctionDossierParity: true,
cliTerminalFunctionParity,
callerCancellationRecovered: true,
closedDocumentAbsent: true,
};
+115
View File
@@ -1,3 +1,4 @@
import assert from "node:assert/strict";
import { requireMcpResult } from "./mcp-verifier-results.mjs";
export const verifyHopperFunctionBasics = async (
@@ -64,3 +65,117 @@ export const verifyHopperFunctionBasics = async (
instructionWindowCount: instructions.instructions.length,
};
};
/** Check native terminal call sites across instruction, reference and dossier projections. */
export async function verifyHopperTerminalInstructions(call) {
const procedures = await call("list_procedures");
const selected = procedures.filter(
({ value }) =>
value === "-[REAWidget delegate]" ||
value.startsWith("imp___stubs__") ||
value.startsWith("_objc_msgSend$"),
);
if (selected.length === 0)
throw new Error("Objective-C fixture omitted terminal-call procedures");
let terminalCalls = 0;
for (const { address } of selected) {
const references = await call("procedure_references", {
procedure: address,
direction: "outgoing",
});
const nativeCalls = references.references.filter(
(edge) => edge.call !== undefined,
);
if (nativeCalls.length === 0)
throw new Error(
`Native fixture procedure ${address} omitted its terminal call`,
);
const dossier = await call("analyze_function", { procedure: address });
const instructions = await call("read_function_instructions", {
procedure: address,
});
const assembly = await call("procedure_assembly", { procedure: address });
const info = await call("procedure_info", { procedure: address });
for (const edge of nativeCalls) {
const observed = dossier.outgoing_references.find(
(item) =>
item.source_address === edge.source_address &&
item.target_address === edge.target_address,
);
assert.ok(
observed,
`Dossier discarded native call ${edge.source_address} -> ${edge.target_address}`,
);
assert.deepEqual(observed.call, edge.call);
assert.ok(
instructions.instructions.some((line) =>
line.startsWith(`${edge.source_address}:`),
),
`Instruction inventory discarded native call site ${edge.source_address}`,
);
assert.ok(
dossier.basic_blocks.some(
({ start, end }) =>
BigInt(start) <= BigInt(edge.source_address) &&
BigInt(edge.source_address) < BigInt(end),
),
`Normalized block excluded native call site ${edge.source_address}`,
);
terminalCalls++;
}
assert.equal(assembly, instructions.instructions.join("\n"));
assert.deepEqual(dossier.assembly, instructions.instructions);
if (address === selected[0].address)
await verifyHopperTerminalComment(
call,
address,
nativeCalls.at(-1).source_address,
);
const expectedLength = dossier.basic_blocks.reduce(
(total, { start, end }) => total + Number(BigInt(end) - BigInt(start)),
0,
);
assert.equal(
info.length,
expectedLength,
"Procedure length disagrees with normalized native blocks",
);
}
if (terminalCalls === 0)
throw new Error("Objective-C fixture omitted native terminal calls");
return {
procedures: selected.length,
terminalCalls,
normalizedBlockEndpoints: true,
terminalCommentPreserved: true,
};
}
async function verifyHopperTerminalComment(call, address, terminal) {
const original = await call("comment", { address: terminal });
try {
await call("set_comment", {
address: terminal,
comment: "REA terminal instruction comment",
});
const annotated = await call("analyze_function", {
procedure: address,
});
if (
!annotated.comments.some(
(item) =>
item.address === terminal &&
item.kind === "comment" &&
item.text === "REA terminal instruction comment",
)
)
throw new Error(
"Dossier discarded a comment on the native terminal instruction",
);
} finally {
await call("set_comment", {
address: terminal,
comment: original ?? "",
});
}
}
+10 -1
View File
@@ -30,7 +30,10 @@ import {
requireMcpResult,
requireWorkflowEvidenceProvider,
} from "./lib/mcp-verifier-results.mjs";
import { verifyHopperFunctionBasics } from "./lib/real-hopper-function-basics.mjs";
import {
verifyHopperTerminalInstructions,
verifyHopperFunctionBasics,
} from "./lib/real-hopper-function-basics.mjs";
import { openAndVerifyLargeFixture } from "./lib/real-hopper-exhaustive-search.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import { requireHopperSelection } from "./lib/real-hopper-selection.mjs";
@@ -465,6 +468,7 @@ try {
);
if (closed.isError === true) throw new Error(textValue(closed));
let unicodeStrings = null;
let terminalInstructions = null;
if (fixtureTargets.unicode !== undefined) {
const call = async (name, args = {}) =>
requireSuccessfulTool(
@@ -472,6 +476,7 @@ try {
name,
);
await call("open_binary", { path: fixtureTargets.unicode.path });
terminalInstructions = await verifyHopperTerminalInstructions(call);
unicodeStrings = await verifyHopperStringObjects(call, [
{ value: "REA_UTF16_é_😀", encoding: "utf-16-le" },
{ value: 'REA_UTF16_ESCAPED_é_"\\line\nend\t\r', encoding: "utf-16-le" },
@@ -482,6 +487,9 @@ try {
const lifecycleAndCli = await verifyHopperLifecycleAndCli(client, options, {
primary: targetA,
secondary: targetB,
...(fixtureTargets.unicode === undefined
? {}
: { unicode: fixtureTargets.unicode.path }),
ownedProcessIds,
});
const closedSession = requireMcpResult(
@@ -509,6 +517,7 @@ try {
analyses: [firstAnalysis, secondAnalysis],
fixtureAnalysis,
unicodeStrings,
terminalInstructions,
boundaryContracts,
lifecycleAndCli,
inventoryCounts,