Path: blob/main/contrib/libdiff/lib/diff_patience.c
35065 views
/* Implementation of the Patience Diff algorithm invented by Bram Cohen:1* Divide a diff problem into smaller chunks by an LCS (Longest Common Sequence)2* of common-unique lines. */3/*4* Copyright (c) 2020 Neels Hofmeyr <[email protected]>5*6* Permission to use, copy, modify, and distribute this software for any7* purpose with or without fee is hereby granted, provided that the above8* copyright notice and this permission notice appear in all copies.9*10* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES11* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF12* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR13* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES14* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN15* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF16* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.17*/1819#include <assert.h>20#include <errno.h>21#include <stdbool.h>22#include <stdint.h>23#include <stdio.h>24#include <stdlib.h>2526#include <arraylist.h>27#include <diff_main.h>2829#include "diff_internal.h"30#include "diff_debug.h"3132/* Algorithm to find unique lines:33* 0: stupidly iterate atoms34* 1: qsort35* 2: mergesort36*/37#define UNIQUE_STRATEGY 13839/* Per-atom state for the Patience Diff algorithm */40struct atom_patience {41#if UNIQUE_STRATEGY == 042bool unique_here;43#endif44bool unique_in_both;45struct diff_atom *pos_in_other;46struct diff_atom *prev_stack;47struct diff_range identical_lines;48};4950/* A diff_atom has a backpointer to the root diff_data. That points to the51* current diff_data, a possibly smaller section of the root. That current52* diff_data->algo_data is a pointer to an array of struct atom_patience. The53* atom's index in current diff_data gives the index in the atom_patience array.54*/55#define PATIENCE(ATOM) \56(((struct atom_patience*)((ATOM)->root->current->algo_data))\57[diff_atom_idx((ATOM)->root->current, ATOM)])5859#if UNIQUE_STRATEGY == 06061/* Stupid iteration and comparison of all atoms */62static int63diff_atoms_mark_unique(struct diff_data *d, unsigned int *unique_count)64{65struct diff_atom *i;66unsigned int count = 0;67diff_data_foreach_atom(i, d) {68PATIENCE(i).unique_here = true;69PATIENCE(i).unique_in_both = true;70count++;71}72diff_data_foreach_atom(i, d) {73struct diff_atom *j;7475if (!PATIENCE(i).unique_here)76continue;7778diff_data_foreach_atom_from(i + 1, j, d) {79bool same;80int r = diff_atom_same(&same, i, j);81if (r)82return r;83if (!same)84continue;85if (PATIENCE(i).unique_here) {86PATIENCE(i).unique_here = false;87PATIENCE(i).unique_in_both = false;88count--;89}90PATIENCE(j).unique_here = false;91PATIENCE(j).unique_in_both = false;92count--;93}94}95if (unique_count)96*unique_count = count;97return 0;98}99100/* Mark those lines as PATIENCE(atom).unique_in_both = true that appear exactly101* once in each side. */102static int103diff_atoms_mark_unique_in_both(struct diff_data *left, struct diff_data *right,104unsigned int *unique_in_both_count)105{106/* Derive the final unique_in_both count without needing an explicit107* iteration. So this is just some optimiziation to save one iteration108* in the end. */109unsigned int unique_in_both;110int r;111112r = diff_atoms_mark_unique(left, &unique_in_both);113if (r)114return r;115r = diff_atoms_mark_unique(right, NULL);116if (r)117return r;118119debug("unique_in_both %u\n", unique_in_both);120121struct diff_atom *i;122diff_data_foreach_atom(i, left) {123if (!PATIENCE(i).unique_here)124continue;125struct diff_atom *j;126int found_in_b = 0;127diff_data_foreach_atom(j, right) {128bool same;129int r = diff_atom_same(&same, i, j);130if (r)131return r;132if (!same)133continue;134if (!PATIENCE(j).unique_here) {135found_in_b = 2; /* or more */136break;137} else {138found_in_b = 1;139PATIENCE(j).pos_in_other = i;140PATIENCE(i).pos_in_other = j;141}142}143144if (found_in_b == 0 || found_in_b > 1) {145PATIENCE(i).unique_in_both = false;146unique_in_both--;147debug("unique_in_both %u (%d) ", unique_in_both,148found_in_b);149debug_dump_atom(left, NULL, i);150}151}152153/* Still need to unmark right[*]->patience.unique_in_both for atoms that154* don't exist in left */155diff_data_foreach_atom(i, right) {156if (!PATIENCE(i).unique_here157|| !PATIENCE(i).unique_in_both)158continue;159struct diff_atom *j;160bool found_in_a = false;161diff_data_foreach_atom(j, left) {162bool same;163int r;164if (!PATIENCE(j).unique_in_both)165continue;166r = diff_atom_same(&same, i, j);167if (r)168return r;169if (!same)170continue;171found_in_a = true;172break;173}174175if (!found_in_a)176PATIENCE(i).unique_in_both = false;177}178179if (unique_in_both_count)180*unique_in_both_count = unique_in_both;181return 0;182}183184#else /* UNIQUE_STRATEGY != 0 */185186/* Use an optimized sorting algorithm (qsort, mergesort) to find unique lines */187188static int diff_atoms_compar(const void *_a, const void *_b)189{190const struct diff_atom *a = *(struct diff_atom**)_a;191const struct diff_atom *b = *(struct diff_atom**)_b;192int cmp;193int rc = 0;194195/* If there's been an error (e.g. I/O error) in a previous compar, we196* have no way to abort the sort but just report the rc and stop197* comparing. Make sure to catch errors on either side. If atoms are198* from more than one diff_data, make sure the error, if any, spreads199* to all of them, so we can cut short all future comparisons. */200if (a->root->err)201rc = a->root->err;202if (b->root->err)203rc = b->root->err;204if (rc) {205a->root->err = rc;206b->root->err = rc;207/* just return 'equal' to not swap more positions */208return 0;209}210211/* Sort by the simplistic hash */212if (a->hash < b->hash)213return -1;214if (a->hash > b->hash)215return 1;216217/* If hashes are the same, the lines may still differ. Do a full cmp. */218rc = diff_atom_cmp(&cmp, a, b);219220if (rc) {221/* Mark the I/O error so that the caller can find out about it.222* For the case atoms are from more than one diff_data, mark in223* both. */224a->root->err = rc;225if (a->root != b->root)226b->root->err = rc;227return 0;228}229230return cmp;231}232233/* Sort an array of struct diff_atom* in-place. */234static int diff_atoms_sort(struct diff_atom *atoms[],235size_t atoms_count)236{237#if UNIQUE_STRATEGY == 1238qsort(atoms, atoms_count, sizeof(struct diff_atom*), diff_atoms_compar);239#else240mergesort(atoms, atoms_count, sizeof(struct diff_atom*),241diff_atoms_compar);242#endif243return atoms[0]->root->err;244}245246static int247diff_atoms_mark_unique_in_both(struct diff_data *left, struct diff_data *right,248unsigned int *unique_in_both_count_p)249{250struct diff_atom *a;251struct diff_atom *b;252struct diff_atom **all_atoms;253unsigned int len = 0;254unsigned int i;255unsigned int unique_in_both_count = 0;256int rc;257258all_atoms = calloc(left->atoms.len + right->atoms.len,259sizeof(struct diff_atom *));260if (all_atoms == NULL)261return ENOMEM;262263left->err = 0;264right->err = 0;265left->root->err = 0;266right->root->err = 0;267diff_data_foreach_atom(a, left) {268all_atoms[len++] = a;269}270diff_data_foreach_atom(b, right) {271all_atoms[len++] = b;272}273274rc = diff_atoms_sort(all_atoms, len);275if (rc)276goto free_and_exit;277278/* Now we have a sorted array of atom pointers. All similar lines are279* adjacent. Walk through the array and mark those that are unique on280* each side, but exist once in both sources. */281for (i = 0; i < len; i++) {282bool same;283unsigned int next_differing_i;284unsigned int last_identical_i;285unsigned int j;286unsigned int count_first_side = 1;287unsigned int count_other_side = 0;288a = all_atoms[i];289debug("a: ");290debug_dump_atom(a->root, NULL, a);291292/* Do as few diff_atom_cmp() as possible: first walk forward293* only using the cheap hash as indicator for differing atoms;294* then walk backwards until hitting an identical atom. */295for (next_differing_i = i + 1; next_differing_i < len;296next_differing_i++) {297b = all_atoms[next_differing_i];298if (a->hash != b->hash)299break;300}301for (last_identical_i = next_differing_i - 1;302last_identical_i > i;303last_identical_i--) {304b = all_atoms[last_identical_i];305rc = diff_atom_same(&same, a, b);306if (rc)307goto free_and_exit;308if (same)309break;310}311next_differing_i = last_identical_i + 1;312313for (j = i+1; j < next_differing_i; j++) {314b = all_atoms[j];315/* A following atom is the same. See on which side the316* repetition counts. */317if (a->root == b->root)318count_first_side ++;319else320count_other_side ++;321debug("b: ");322debug_dump_atom(b->root, NULL, b);323debug(" count_first_side=%d count_other_side=%d\n",324count_first_side, count_other_side);325}326327/* Counted a section of similar atoms, put the results back to328* the atoms. */329if ((count_first_side == 1)330&& (count_other_side == 1)) {331b = all_atoms[i+1];332PATIENCE(a).unique_in_both = true;333PATIENCE(a).pos_in_other = b;334PATIENCE(b).unique_in_both = true;335PATIENCE(b).pos_in_other = a;336unique_in_both_count++;337}338339/* j now points at the first atom after 'a' that is not340* identical to 'a'. j is always > i. */341i = j - 1;342}343*unique_in_both_count_p = unique_in_both_count;344rc = 0;345free_and_exit:346free(all_atoms);347return rc;348}349#endif /* UNIQUE_STRATEGY != 0 */350351/* binary search to find the stack to put this atom "card" on. */352static int353find_target_stack(struct diff_atom *atom,354struct diff_atom **patience_stacks,355unsigned int patience_stacks_count)356{357unsigned int lo = 0;358unsigned int hi = patience_stacks_count;359while (lo < hi) {360unsigned int mid = (lo + hi) >> 1;361362if (PATIENCE(patience_stacks[mid]).pos_in_other363< PATIENCE(atom).pos_in_other)364lo = mid + 1;365else366hi = mid;367}368return lo;369}370371/* Among the lines that appear exactly once in each side, find the longest372* streak that appear in both files in the same order (with other stuff allowed373* to interleave). Use patience sort for that, as in the Patience Diff374* algorithm.375* See https://bramcohen.livejournal.com/73318.html and, for a much more376* detailed explanation,377* https://blog.jcoglan.com/2017/09/19/the-patience-diff-algorithm/ */378int379diff_algo_patience(const struct diff_algo_config *algo_config,380struct diff_state *state)381{382int rc;383struct diff_data *left = &state->left;384struct diff_data *right = &state->right;385struct atom_patience *atom_patience_left =386calloc(left->atoms.len, sizeof(struct atom_patience));387struct atom_patience *atom_patience_right =388calloc(right->atoms.len, sizeof(struct atom_patience));389unsigned int unique_in_both_count;390struct diff_atom **lcs = NULL;391392debug("\n** %s\n", __func__);393394left->root->current = left;395right->root->current = right;396left->algo_data = atom_patience_left;397right->algo_data = atom_patience_right;398399/* Find those lines that appear exactly once in 'left' and exactly once400* in 'right'. */401rc = diff_atoms_mark_unique_in_both(left, right, &unique_in_both_count);402if (rc)403goto free_and_exit;404405debug("unique_in_both_count %u\n", unique_in_both_count);406debug("left:\n");407debug_dump(left);408debug("right:\n");409debug_dump(right);410411if (!unique_in_both_count) {412/* Cannot apply Patience, tell the caller to use fallback_algo413* instead. */414rc = DIFF_RC_USE_DIFF_ALGO_FALLBACK;415goto free_and_exit;416}417418rc = ENOMEM;419420/* An array of Longest Common Sequence is the result of the below421* subscope: */422unsigned int lcs_count = 0;423struct diff_atom *lcs_tail = NULL;424425{426/* This subscope marks the lifetime of the atom_pointers427* allocation */428429/* One chunk of storage for atom pointers */430struct diff_atom **atom_pointers;431atom_pointers = recallocarray(NULL, 0, unique_in_both_count * 2,432sizeof(struct diff_atom*));433if (atom_pointers == NULL)434return ENOMEM;435/* Half for the list of atoms that still need to be put on436* stacks */437struct diff_atom **uniques = atom_pointers;438439/* Half for the patience sort state's "card stacks" -- we440* remember only each stack's topmost "card" */441struct diff_atom **patience_stacks;442patience_stacks = atom_pointers + unique_in_both_count;443unsigned int patience_stacks_count = 0;444445/* Take all common, unique items from 'left' ... */446447struct diff_atom *atom;448struct diff_atom **uniques_end = uniques;449diff_data_foreach_atom(atom, left) {450if (!PATIENCE(atom).unique_in_both)451continue;452*uniques_end = atom;453uniques_end++;454}455456/* ...and sort them to the order found in 'right'.457* The idea is to find the leftmost stack that has a higher line458* number and add it to the stack's top.459* If there is no such stack, open a new one on the right. The460* line number is derived from the atom*, which are array items461* and hence reflect the relative position in the source file.462* So we got the common-uniques from 'left' and sort them463* according to PATIENCE(atom).pos_in_other. */464unsigned int i;465for (i = 0; i < unique_in_both_count; i++) {466atom = uniques[i];467unsigned int target_stack;468target_stack = find_target_stack(atom, patience_stacks,469patience_stacks_count);470assert(target_stack <= patience_stacks_count);471patience_stacks[target_stack] = atom;472if (target_stack == patience_stacks_count)473patience_stacks_count++;474475/* Record a back reference to the next stack on the476* left, which will form the final longest sequence477* later. */478PATIENCE(atom).prev_stack = target_stack ?479patience_stacks[target_stack - 1] : NULL;480481{482int xx;483for (xx = 0; xx < patience_stacks_count; xx++) {484debug(" %s%d",485(xx == target_stack) ? ">" : "",486diff_atom_idx(right,487PATIENCE(patience_stacks[xx]).pos_in_other));488}489debug("\n");490}491}492493/* backtrace through prev_stack references to form the final494* longest common sequence */495lcs_tail = patience_stacks[patience_stacks_count - 1];496lcs_count = patience_stacks_count;497498/* uniques and patience_stacks are no longer needed.499* Backpointers are in PATIENCE(atom).prev_stack */500free(atom_pointers);501}502503lcs = recallocarray(NULL, 0, lcs_count, sizeof(struct diff_atom*));504struct diff_atom **lcs_backtrace_pos = &lcs[lcs_count - 1];505struct diff_atom *atom;506for (atom = lcs_tail; atom; atom = PATIENCE(atom).prev_stack, lcs_backtrace_pos--) {507assert(lcs_backtrace_pos >= lcs);508*lcs_backtrace_pos = atom;509}510511unsigned int i;512if (DEBUG) {513debug("\npatience LCS:\n");514for (i = 0; i < lcs_count; i++) {515debug("\n L "); debug_dump_atom(left, right, lcs[i]);516debug(" R "); debug_dump_atom(right, left,517PATIENCE(lcs[i]).pos_in_other);518}519}520521522/* TODO: For each common-unique line found (now listed in lcs), swallow523* lines upwards and downwards that are identical on each side. Requires524* a way to represent atoms being glued to adjacent atoms. */525526debug("\ntraverse LCS, possibly recursing:\n");527528/* Now we have pinned positions in both files at which it makes sense to529* divide the diff problem into smaller chunks. Go into the next round:530* look at each section in turn, trying to again find common-unique531* lines in those smaller sections. As soon as no more are found, the532* remaining smaller sections are solved by Myers. */533/* left_pos and right_pos are indexes in left/right->atoms.head until534* which the atoms are already handled (added to result chunks). */535unsigned int left_pos = 0;536unsigned int right_pos = 0;537for (i = 0; i <= lcs_count; i++) {538struct diff_atom *atom;539struct diff_atom *atom_r;540/* left_idx and right_idx are indexes of the start of this541* section of identical lines on both sides.542* left_pos marks the index of the first still unhandled line,543* left_idx is the start of an identical section some way544* further down, and this loop adds an unsolved chunk of545* [left_pos..left_idx[ and a solved chunk of546* [left_idx..identical_lines.end[. */547unsigned int left_idx;548unsigned int right_idx;549550debug("iteration %u of %u left_pos %u right_pos %u\n",551i, lcs_count, left_pos, right_pos);552553if (i < lcs_count) {554atom = lcs[i];555atom_r = PATIENCE(atom).pos_in_other;556debug("lcs[%u] = left[%u] = right[%u]\n", i,557diff_atom_idx(left, atom), diff_atom_idx(right, atom_r));558left_idx = diff_atom_idx(left, atom);559right_idx = diff_atom_idx(right, atom_r);560} else {561/* There are no more identical lines until the end of562* left and right. */563atom = NULL;564atom_r = NULL;565left_idx = left->atoms.len;566right_idx = right->atoms.len;567}568569/* 'atom' (if not NULL) now marks an atom that matches on both570* sides according to patience-diff (a common-unique identical571* atom in both files).572* Handle the section before and the atom itself; the section573* after will be handled by the next loop iteration -- note that574* i loops to last element + 1 ("i <= lcs_count"), so that there575* will be another final iteration to pick up the last remaining576* items after the last LCS atom.577*/578579debug("iteration %u left_pos %u left_idx %u"580" right_pos %u right_idx %u\n",581i, left_pos, left_idx, right_pos, right_idx);582583/* Section before the matching atom */584struct diff_atom *left_atom = &left->atoms.head[left_pos];585unsigned int left_section_len = left_idx - left_pos;586587struct diff_atom *right_atom = &(right->atoms.head[right_pos]);588unsigned int right_section_len = right_idx - right_pos;589590if (left_section_len && right_section_len) {591/* Record an unsolved chunk, the caller will apply592* inner_algo() on this chunk. */593if (!diff_state_add_chunk(state, false,594left_atom, left_section_len,595right_atom,596right_section_len))597goto free_and_exit;598} else if (left_section_len && !right_section_len) {599/* Only left atoms and none on the right, they form a600* "minus" chunk, then. */601if (!diff_state_add_chunk(state, true,602left_atom, left_section_len,603right_atom, 0))604goto free_and_exit;605} else if (!left_section_len && right_section_len) {606/* No left atoms, only atoms on the right, they form a607* "plus" chunk, then. */608if (!diff_state_add_chunk(state, true,609left_atom, 0,610right_atom, right_section_len))611goto free_and_exit;612}613/* else: left_section_len == 0 and right_section_len == 0, i.e.614* nothing here. */615616/* The atom found to match on both sides forms a chunk of equals617* on each side. In the very last iteration of this loop, there618* is no matching atom, we were just cleaning out the remaining619* lines. */620if (atom) {621void *ok;622ok = diff_state_add_chunk(state, true,623atom, 1,624PATIENCE(atom).pos_in_other, 1);625if (!ok)626goto free_and_exit;627}628left_pos = left_idx + 1;629right_pos = right_idx + 1;630debug("end of iteration %u left_pos %u left_idx %u"631" right_pos %u right_idx %u\n",632i, left_pos, left_idx, right_pos, right_idx);633}634debug("** END %s\n", __func__);635636rc = DIFF_RC_OK;637638free_and_exit:639left->root->current = NULL;640right->root->current = NULL;641free(atom_patience_left);642free(atom_patience_right);643if (lcs)644free(lcs);645return rc;646}647648649