mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
fix(hopper): normalize native block endpoints and retain terminal evidence
This commit is contained in:
+20
-21
@@ -387,13 +387,26 @@ def _containing_procedure(document, address):
|
||||
return (procedure, None) if procedure is not None else (None, "not_in_procedure")
|
||||
|
||||
|
||||
def _basic_block_end(procedure, block):
|
||||
"""Normalize native block endpoints to an exclusive byte address."""
|
||||
end = block.getEndingAddress()
|
||||
# Hopper 6.1 returns the final instruction address, despite the Python
|
||||
# documentation describing an exclusive end. Membership distinguishes
|
||||
# that representation from an actual exclusive endpoint in other builds.
|
||||
if procedure.getBasicBlockAtAddress(end) == block:
|
||||
instruction = procedure.getSegment().getInstructionAtAddress(end)
|
||||
if instruction is not None and instruction.getInstructionLength() > 0:
|
||||
return end + instruction.getInstructionLength()
|
||||
return end
|
||||
|
||||
|
||||
def _instruction_addresses(procedure):
|
||||
result = []
|
||||
seen = set()
|
||||
segment = procedure.getSegment()
|
||||
for block in procedure.basicBlockIterator():
|
||||
address = block.getStartingAddress()
|
||||
end = block.getEndingAddress()
|
||||
end = _basic_block_end(procedure, block)
|
||||
while address < end and address not in seen:
|
||||
seen.add(address)
|
||||
instruction = segment.getInstructionAtAddress(address)
|
||||
@@ -633,25 +646,11 @@ def _render_instruction(segment, address):
|
||||
|
||||
def _assembly(procedure):
|
||||
"""Render assembly while guarding against malformed instruction cycles."""
|
||||
lines = []
|
||||
segment = procedure.getSegment()
|
||||
seen = set()
|
||||
for block in procedure.basicBlockIterator():
|
||||
address = block.getStartingAddress()
|
||||
end = block.getEndingAddress()
|
||||
while address < end and address not in seen:
|
||||
seen.add(address)
|
||||
instruction = segment.getInstructionAtAddress(address)
|
||||
if instruction is None:
|
||||
break
|
||||
line = _render_instruction(segment, address)
|
||||
if line is not None:
|
||||
lines.append(line)
|
||||
length = instruction.getInstructionLength()
|
||||
if length <= 0:
|
||||
break
|
||||
address += length
|
||||
return "\n".join(lines)
|
||||
return "\n".join(
|
||||
line for address in _instruction_addresses(procedure)
|
||||
if (line := _render_instruction(segment, address)) is not None
|
||||
)
|
||||
|
||||
|
||||
def _read_function_instructions(document, params):
|
||||
@@ -688,7 +687,7 @@ def _analyze_function(document, params):
|
||||
successors.append(_hex(successor))
|
||||
blocks.append({
|
||||
"start": _hex(block.getStartingAddress()),
|
||||
"end": _hex(block.getEndingAddress()),
|
||||
"end": _hex(_basic_block_end(procedure, block)),
|
||||
"successors": sorted(set(successors), key=lambda value: int(value, 16)),
|
||||
})
|
||||
pseudo = _pseudocode(document, procedure) or ""
|
||||
@@ -986,7 +985,7 @@ def _dispatch(method, params):
|
||||
return sorted((_hex(item.getEntryPoint()) for item in procedure.getAllCalleeProcedures()), key=lambda value: int(value, 16))
|
||||
if method == "procedure_info":
|
||||
blocks = list(procedure.basicBlockIterator())
|
||||
length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
|
||||
length = sum(max(0, _basic_block_end(procedure, block) - block.getStartingAddress()) for block in blocks)
|
||||
return {
|
||||
"name": _procedure_name(procedure),
|
||||
"entrypoint": _hex(procedure.getEntryPoint()),
|
||||
|
||||
+3
-1
@@ -211,7 +211,9 @@ CallReference classifications across reference and dossier results, retains long
|
||||
string fragment metadata in dossiers, and exercises pathological regex deadline
|
||||
and cancellation followed by successful requests in the same native session. When the macOS
|
||||
Objective-C fixture is present, the lane also verifies native UTF-16 string objects
|
||||
and their inferred encodings against their actual bytes.
|
||||
and their inferred encodings against their actual bytes. Native terminal calls
|
||||
are checked across reference, instruction, assembly, block-range and procedure-length
|
||||
projections; block endpoints are normalized using actual native membership.
|
||||
Unmapped annotation destinations and
|
||||
oversized later batch names fail before any earlier edit is applied.
|
||||
The Linux demo lane remains a separate `verify:hopper:linux` command.
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+3
@@ -71,6 +71,9 @@ class FakeProcedure:
|
||||
def basicBlockIterator(self):
|
||||
return [FakeBlock()]
|
||||
|
||||
def getBasicBlockAtAddress(self, address):
|
||||
return None
|
||||
|
||||
def getBasicBlockCount(self):
|
||||
return 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user