Tree Matching
The diff core runs one algorithm, in the GumTree lineage, across three phases: top-down isomorphic matching, bottom-up container matching, and a Chawathe edit script over the matched pairs. All six tiers feed the same algorithm; only the trees differ.Phase 1: top-down isomorphic matching
The algorithm walks both trees top-down and pairs subtrees that are structurally identical. Two subtrees are isomorphic when their structure hashes match exactly: same kind, same children, same shape, with labels and values ignored. Take these two small ASTs.a.ts:
b.ts:
return node in a.ts and the return node in b.ts differ, but the parameter list, name: string, is identical in both. Top-down matching anchors on those identical subtrees first:
Phase 2: bottom-up container matching
Most changes don’t preserve isomorphism. In the example above the bodies differ, so no body node matched top-down. Bottom-up matching propagates matches upward: a container matches if most of its children already matched, even when the container’s own shape changed. The function declarations have identical parameter lists (matched in phase 1) and the return statements differ. Thefunction_declaration nodes share a matched child, so they pair up:
Chawathe edit scripts
Once the match sets exist, the algorithm computes the minimal edit script that transforms the old tree into the new one, in the style of Chawathe’s tree-diff work:
The script is minimal: one action per affected node, and a rename is one
Update, never a delete plus an insert.
Content hash vs structure hash
Every node carries two hashes, 53-bit-safe folds of two 32-bit FNV-1a streams, both computed bottom-up (Merkle-style):- Content hash: kind + label + value + the children’s content hashes. Two nodes with the same content hash are the same code. Renaming
greetchanges the label and therefore the content hash. - Structure hash: kind + the children’s structure hashes. Labels and values are ignored, so
function greet(...)andfunction farewell(...)with identical bodies share a structure hash. This is what phase 1 matches on.
FNV-1a hashing
Hashes use FNV-1a over two independent 32-bit streams, folded into one 53-bit-safe JavaScript number. Each node’s hash combines its own fields with its children’s hashes, so a change anywhere in a subtree changes every ancestor’s hash. That Merkle property makes subtree equality a single comparison. The collision bound follows from the birthday problem: at the 250,000-node cap, the chance of any two nodes colliding is about 0.0003%.Hashes are JavaScript numbers, not full 64-bit integers, so no
BigInt handling is involved. The JSON formatter serializes them as regular integers. See Output.Safety valves
Tree matching is inherently super-linear, so the core enforces two bounds:maxNodes= 250,000: trees larger than this makediffTreesreturn alinesfallback, and the caller falls back to a line diff, which keeps the birthday bound quoted above honest.- O(n²) bottom-up matching: the bottom-up phase is bounded to quadratic worst-case work; candidate pairs are only considered inside already-matched parents, which keeps typical runs far below the bound.