5.5

compare_functions and rea compare

compare_functions diffs two dossiers on eight dimensions and reports each as unchanged, changed, or unknown, and its assembly dimension is a text diff without relocation normalization.

You rebuilt ledger.c at -O2 and want to know which functions the compiler changed before you trust a claim about the -O1 build. The capture asked rea that question for _percent_change and for _total, with the two builds open in turn. When you finish this section, you can read a comparison record and say what each of its eight dimensions compares. You can also name the limit that keeps a byte-level proof outside rea.

Two dossiers in, one derived record out

compare_functions takes two complete Evidence records as left and right. Each is parsed with parseEvidence, so a record with a tampered id is refused. Each must be an analyze_function record with predicate type rea.analysis, or the tool answers "Expected analyze_function Evidence". The capture passed the dossiers it had received a few calls earlier.

the request for _percent_change across the two buildsjsonFull log
"name": "compare_functions",
"arguments": {
  "left": {
    "evidence_id": "ev_b492c85e…",
    "record": "<cut by run.py: the complete analyze_function record, see 10-mcp-09-analyze-function-percent-change-O1.json>"
  },
  "right": {
    "evidence_id": "ev_142cc943…",
    "record": "<cut by run.py: the complete analyze_function record, see 10-mcp-17-analyze-function-percent-change-O2.json>"
  }
}
The kit kept each evidence_id and replaced the complete dossier with a marker that names the file holding it.

The answer is a new Evidence record from provider rea-function-comparison version 1. Its predicate type is rea.function-comparison, its confidence derived, and its authority analyst-inference. Both input ids appear as parameters and as evidence_links. The record has no subject, so it carries the automatic limitation about artifact identity (the Evidence record). Before it is recorded, both inputs are recorded in the session ledger, which is why import later found two O1 records already present.

the head of the _percent_change comparisonjsonFull log
"normalized_result": {
  "status": "unknown",
  "function_match": {
    "status": "matched",
    "method": "symbol",
    "left_name": "_percent_change",
    "right_name": "_percent_change"
  },
  "left_subject_sha256": "01b5d08b…",
  "right_subject_sha256": "8bbbaa58…",
  "summary": {
    "unchanged": 7,
    "changed": 0,
    "unknown": 1
  },
The dimensions, the changes, and the limitations are cut.

Function match: how rea decided that the two dossiers describe the same function. matched by symbol means the two procedure names are equal. A mismatched or ambiguous match is reported with method explicit, and an ambiguous match makes the whole comparison unknown.

The eight dimensions

compareDimensions runs eight comparisons in a fixed order: identity, pseudocode, assembly, comments, calls, references, strings_names, and cfg. Each dimension has a status of unchanged, changed, or unknown, and a left and right digest. It also has a left and right count, a text_delta for the two text dimensions, and a conclusion_kind. The overall status is unknown if any dimension is unknown, else changed if any dimension changed, else unchanged.

A dimension is also unknown, with null counts, when either dossier declares a needed facet unavailable in facet_availability. An empty facet with no declaration has the same effect. The call dimension also reads relationship_coverage.

DimensionWhat is compared_percent_change_total
identitythe normalized identity of the procedureunchangedunchanged
pseudocodethe decompiler text, as linesunchanged, 272 and 272 code pointschanged, 300 to 2904 code points
assemblythe listing text, as linesunchanged, 161 and 161changed, 336 to 1866
commentscomment text at normalized offsetsunchanged, 0 and 0unchanged, 0 and 0
callsthe resolved calleesunchanged, 1 and 1unchanged, 1 and 1
referencesreference endpoints and kindsunknown, 3 and 3changed, 4 to 13
strings_namesreferenced strings and namesunchanged, 1 and 1changed, 2 to 11
cfgthe basic blocks and edges, address-normalizedunchanged, 3 and 3changed, 5 to 16

One unknown dimension

The _percent_change comparison has seven unchanged dimensions and one unknown, so the whole record is unknown. The unknown dimension is references. Both sides have three references and the same named endpoints. One incoming reference of each dossier is a DATA reference with no source procedure, so that endpoint has no name.

the references dimension of the _percent_change comparisonjsonFull log
{
  "dimension": "references",
  "left_count": 3,
  "right_count": 3,
  "evidence_links": [
    "ev_b492c85e…",
    "ev_142cc943…"
  ],
  "limitations": [
    "Unnamed reference endpoints have no stable identity across different artifacts, so equal named endpoints do not establish equal references."
  ],
  "status": "unknown",
  "left_digest": null,
  "right_digest": null,
  "text_delta": null,
  "conclusion_kind": "unresolved_branch"
},
Nothing inside the dimension is cut.

The rule applies only when the two dossiers come from different files. Inside one file an address is a stable identity. The -O1 and -O2 builds are different files. So rea cannot say that the unnamed endpoint on the left is the one on the right. Edge kinds that differ still count as a change, and that is why the _total references dimension reads changed.

Read the summary before the overall status. One unknown dimension makes the whole record unknown, and it opens a residual unknown of severity high (residual unknowns). The seven other dimensions keep their own status. A claim about the pseudocode or the assembly of _percent_change still passes (verification). A claim about references or overall does not.

The calls dimension of both comparisons carries two limitations, one for each side. Each says that only resolved Ghidra call references are returned. They qualify the unchanged status and do not change it.

The text dimensions

The two text dimensions are line diffs with a budget: 100 ms and 10,000 edits. A diff that runs out of budget makes the dimension unknown with the limitation "Bounded line diff exceeded its time or edit-distance limit.". The counts are code points of the text. The text_delta reports added lines, removed lines, and hunks. Both text dimensions carry the limitation "Exact provider text is compared without address or generated-name normalization; layout-only differences do not establish a behavior change."

For _total, the pseudocode grew by 109 lines and lost 11 in 11 hunks (10-mcp-21-compare-functions-total.json). The assembly grew by 61 lines and lost 12 in 2 hunks. The -O2 build of _total has 16 basic blocks where the -O1 build has 5, and 13 references where it had 4. Those are the facts rea states, and the record stops there. Why the compiler made that change is your question to answer with the two assembly listings.

Fig. 5.6compare_functions on two functions across the -O1 and -O2 buildscomparison
compare_functions on two functions across the -O1 and -O2 builds Two tables of the eight comparison dimensions. Left, _percent_change: identity, pseudocode 272 to 272 code points, assembly 161 to 161, comments 0 to 0, calls 1 to 1, strings_names 1 to 1 and cfg 3 to 3 are unchanged, and references, 3 to 3, is unknown because an unnamed reference endpoint has no stable identity across two different artifacts, so the status is unknown. Right, _total: identity, comments and calls are unchanged, while pseudocode grows from 300 to 2904 code points, assembly from 336 to 1866, references from 4 to 13, strings_names from 2 to 11 and cfg from 5 to 16 basic blocks, so five dimensions are changed and the status is changed. A note says why the references row is unknown. A · _PERCENT_CHANGE, -O1 AGAINST -O2 status unknown: 7 unchanged, 1 unknown identity digests equal unchanged pseudocode 272 to 272 unchanged assembly 161 to 161 unchanged comments 0 to 0 unchanged calls 1 to 1 unchanged references 3 to 3 unknown strings_names 1 to 1 unchanged cfg 3 to 3 unchanged B · _TOTAL, -O1 AGAINST -O2 status changed, 5 of 8 dimensions identity digests equal unchanged pseudocode 300 to 2904 changed assembly 336 to 1866 changed comments 0 to 0 unchanged calls 1 to 1 unchanged references 4 to 13 changed strings_names 2 to 11 changed cfg 5 to 16 changed Counts are code points for the two text dimensions and items for the others. The dashed row is unknown: unnamed reference endpoints have no identity across two artifacts.
The compiler left _percent_change the same on seven dimensions with references unknown, changed _total on five, and rea reports counts without saying why. Read each table top to bottom. Amber rows are changed dimensions, and the dashed row is unknown.

The assembly limit

Every assembly dimension carries two limitations, whether its status is unchanged or changed.

the assembly dimension of the _total comparisonjsonFull log
{
  "dimension": "assembly",
  "left_count": 336,
  "right_count": 1866,
  "evidence_links": [
    "ev_a1fe53ff…",
    "ev_bfe3275b…"
  ],
  "limitations": [
    "Exact provider text is compared without address or generated-name normalization; layout-only differences do not establish a behavior change.",
    "Assembly is opaque provider text; relocation normalization is unavailable."
  ],
  "status": "changed",
  "left_digest": "40e2d441…",
  "right_digest": "e1140153…",
  "text_delta": {
    "added_lines": 61,
    "removed_lines": 12,
    "hunks": 2
  },
  "conclusion_kind": "derived_relationship"
},
Nothing inside the dimension is cut.

rea attaches these strings before any diff runs. The dossier's assembly is Ghidra's listing text, with the addresses and the resolved operands inside the text. The tool compares that text line by line. It does not decode the instructions, it does not know which operand bytes a linker will patch, and it cannot mask them. Two copies of the same code at different addresses diff as changed, and a relocatable object compared with its linked image diffs as changed. The same holds across engines: when the two dossiers come from different providers, both text dimensions become unknown with "Exact text comparison requires one provider identity.".

An unchanged assembly dimension therefore means equal text from one engine, and a changed one means different text. Neither is a statement about bytes. The byte-level proof zeroes the relocation fields and compares the referenced table as well. Lesson 36 builds that proof, and Part 7 runs it against the same fixture. The references dimension has a limit of its own, because it compares the endpoints and not the kinds: "Reference endpoints differ independently of provider reference-kind metadata.".

rea compare on two bundles

rea compare LEFT RIGHT, also spelled rea compare-bundles, compares two bundle files without a session. It works by evidence_id: a record in both bundles is compared, and a record in one bundle only is an unresolved branch. The overall status is unknown as soon as one change is unresolved.

rea compare on the O1 and O2 bundlesjsonFull log
{
  "status": "unknown",
  "left_bundle_sha256": "47160c2d…",
  "right_bundle_sha256": "e0a9fe04…",
  "summary": {
    "records_unchanged": 0,
    "records_added": 0,
    "records_removed": 0,
    "records_changed": 0,
    "unknowns_unchanged": 0,
    "unknowns_added": 0,
    "unknowns_removed": 0,
    "unknowns_advanced": 0,
    "unknowns_diverged": 0,
    "unresolved": 10
  },
  "changes": [
    {
      "entity": "evidence",
      "key": "ev_142cc943…",
      "classification": "unknown",
      "conclusion_kind": "unresolved_branch",
      "left_evidence_ids": [],
      "right_evidence_ids": [
        "ev_142cc943…"
      ],
      "left_digest": null,
      "right_digest": "142cc943…",
      "limitations": [
        "EvidenceBundle has no inventory completeness marker; one-sided membership may reflect incomplete capture."
      ]
    },
Nine of the ten changes are cut, and so are the closing limitations.

Ten unresolved, none unchanged, none removed. The two bundles come from two different files, ledger-arm64-O1 and ledger-arm64-O2. No evidence_id is in both, and every one of the ten records is one-sided. The tool does not call a one-sided record removed, because a bundle has no marker of completeness.

The digests it reports are digests of the canonical bytes. 47160c2d is the digest of bundle O1 without the kit's trailing newline (export and import). rea compare answers the question "what changed in this evidence set" for two bundles of one build. For two builds, compare_functions is the tool, one function at a time.

rea (rea-agents) with the Ghidra provider rea-agents 6.7.0, Ghidra 12.1.4 · 27dc42d · 2026-10-18 · Edition 2026.10