diff --git a/NOTICE b/NOTICE index 3c80d5889..274880d64 100644 --- a/NOTICE +++ b/NOTICE @@ -37,3 +37,8 @@ Software deriving from third parties: This directory contains software developed by the BuildGrid authors: https://gitlab.com/BuildGrid/buildgrid + + * src/buildstream/_loader/listsort.c + + This file is derived from Python and licensed under the + Python Software Foundation License Version 2. diff --git a/src/buildstream/_loader/listsort.c b/src/buildstream/_loader/listsort.c new file mode 100644 index 000000000..34b9c5fd8 --- /dev/null +++ b/src/buildstream/_loader/listsort.c @@ -0,0 +1,1293 @@ +/* + * Based on listobject.c from Python version 3.12.9. + */ + +#include "Python.h" +#include + +/* Reverse a slice of a list in place, from lo up to (exclusive) hi. */ +static void +reverse_slice(PyObject **lo, PyObject **hi) +{ + assert(lo && hi); + + --hi; + while (lo < hi) { + PyObject *t = *lo; + *lo = *hi; + *hi = t; + ++lo; + --hi; + } +} + +/* Lots of code for an adaptive, stable, natural mergesort. There are many + * pieces to this algorithm; read listsort.txt for overviews and details. + */ + +/* A sortslice contains a pointer to an array of keys and a pointer to + * an array of corresponding values. In other words, keys[i] + * corresponds with values[i]. If values == NULL, then the keys are + * also the values. + * + * Several convenience routines are provided here, so that keys and + * values are always moved in sync. + */ + +typedef struct { + PyObject **keys; + PyObject **values; +} sortslice; + +Py_LOCAL_INLINE(void) +sortslice_copy(sortslice *s1, Py_ssize_t i, sortslice *s2, Py_ssize_t j) +{ + s1->keys[i] = s2->keys[j]; + if (s1->values != NULL) + s1->values[i] = s2->values[j]; +} + +Py_LOCAL_INLINE(void) +sortslice_copy_incr(sortslice *dst, sortslice *src) +{ + *dst->keys++ = *src->keys++; + if (dst->values != NULL) + *dst->values++ = *src->values++; +} + +Py_LOCAL_INLINE(void) +sortslice_copy_decr(sortslice *dst, sortslice *src) +{ + *dst->keys-- = *src->keys--; + if (dst->values != NULL) + *dst->values-- = *src->values--; +} + + +Py_LOCAL_INLINE(void) +sortslice_memcpy(sortslice *s1, Py_ssize_t i, sortslice *s2, Py_ssize_t j, + Py_ssize_t n) +{ + memcpy(&s1->keys[i], &s2->keys[j], sizeof(PyObject *) * n); + if (s1->values != NULL) + memcpy(&s1->values[i], &s2->values[j], sizeof(PyObject *) * n); +} + +Py_LOCAL_INLINE(void) +sortslice_memmove(sortslice *s1, Py_ssize_t i, sortslice *s2, Py_ssize_t j, + Py_ssize_t n) +{ + memmove(&s1->keys[i], &s2->keys[j], sizeof(PyObject *) * n); + if (s1->values != NULL) + memmove(&s1->values[i], &s2->values[j], sizeof(PyObject *) * n); +} + +Py_LOCAL_INLINE(void) +sortslice_advance(sortslice *slice, Py_ssize_t n) +{ + slice->keys += n; + if (slice->values != NULL) + slice->values += n; +} + +/* Comparison function: ms->key_compare, which is set at run-time in + * listsort_impl to optimize for various special cases. + * Returns -1 on error, 1 if x < y, 0 if x >= y. + */ + +#define ISLT(X, Y) (*(ms->key_compare))(X, Y, ms) + +/* Compare X to Y via "<". Goto "fail" if the comparison raises an + error. Else "k" is set to true iff X. X and Y are PyObject*s. +*/ +#define IFLT(X, Y) if ((k = ISLT(X, Y)) < 0) goto fail; \ + if (k) + +/* The maximum number of entries in a MergeState's pending-runs stack. + * For a list with n elements, this needs at most floor(log2(n)) + 1 entries + * even if we didn't force runs to a minimal length. So the number of bits + * in a Py_ssize_t is plenty large enough for all cases. + */ +#define MAX_MERGE_PENDING (SIZEOF_SIZE_T * 8) + +/* When we get into galloping mode, we stay there until both runs win less + * often than MIN_GALLOP consecutive times. See listsort.txt for more info. + */ +#define MIN_GALLOP 7 + +/* Avoid malloc for small temp arrays. */ +#define MERGESTATE_TEMP_SIZE 256 + +/* One MergeState exists on the stack per invocation of mergesort. It's just + * a convenient way to pass state around among the helper functions. + */ +struct s_slice { + sortslice base; + Py_ssize_t len; /* length of run */ + int power; /* node "level" for powersort merge strategy */ +}; + +typedef struct s_MergeState MergeState; +struct s_MergeState { + /* This controls when we get *into* galloping mode. It's initialized + * to MIN_GALLOP. merge_lo and merge_hi tend to nudge it higher for + * random data, and lower for highly structured data. + */ + Py_ssize_t min_gallop; + + Py_ssize_t listlen; /* len(input_list) - read only */ + PyObject **basekeys; /* base address of keys array - read only */ + + /* 'a' is temp storage to help with merges. It contains room for + * alloced entries. + */ + sortslice a; /* may point to temparray below */ + Py_ssize_t alloced; + + /* A stack of n pending runs yet to be merged. Run #i starts at + * address base[i] and extends for len[i] elements. It's always + * true (so long as the indices are in bounds) that + * + * pending[i].base + pending[i].len == pending[i+1].base + * + * so we could cut the storage for this, but it's a minor amount, + * and keeping all the info explicit simplifies the code. + */ + int n; + struct s_slice pending[MAX_MERGE_PENDING]; + + /* 'a' points to this when possible, rather than muck with malloc. */ + PyObject *temparray[MERGESTATE_TEMP_SIZE]; + + /* This is the function we will use to compare two keys, + * even when none of our special cases apply and we have to use + * safe_object_compare. */ + int (*key_compare)(PyObject *, PyObject *, MergeState *); + + /* This function is used by unsafe_object_compare to optimize comparisons + * when we know our list is type-homogeneous but we can't assume anything else. + * In the pre-sort check it is set equal to Py_TYPE(key)->tp_richcompare */ + PyObject *(*key_richcompare)(PyObject *, PyObject *, int); +}; + +/* binarysort is the best method for sorting small arrays: it does + few compares, but can do data movement quadratic in the number of + elements. + [lo, hi) is a contiguous slice of a list, and is sorted via + binary insertion. This sort is stable. + On entry, must have lo <= start <= hi, and that [lo, start) is already + sorted (pass start == lo if you don't know!). + If islt() complains return -1, else 0. + Even in case of error, the output slice will be some permutation of + the input (nothing is lost or duplicated). +*/ +static int +binarysort(MergeState *ms, sortslice lo, PyObject **hi, PyObject **start) +{ + Py_ssize_t k; + PyObject **l, **p, **r; + PyObject *pivot; + + assert(lo.keys <= start && start <= hi); + /* assert [lo, start) is sorted */ + if (lo.keys == start) + ++start; + for (; start < hi; ++start) { + /* set l to where *start belongs */ + l = lo.keys; + r = start; + pivot = *r; + /* Invariants: + * pivot >= all in [lo, l). + * pivot < all in [r, start). + * The second is vacuously true at the start. + */ + assert(l < r); + do { + p = l + ((r - l) >> 1); + IFLT(pivot, *p) + r = p; + else + l = p+1; + } while (l < r); + assert(l == r); + /* The invariants still hold, so pivot >= all in [lo, l) and + pivot < all in [l, start), so pivot belongs at l. Note + that if there are elements equal to pivot, l points to the + first slot after them -- that's why this sort is stable. + Slide over to make room. + Caution: using memmove is much slower under MSVC 5; + we're not usually moving many slots. */ + for (p = start; p > l; --p) + *p = *(p-1); + *l = pivot; + if (lo.values != NULL) { + Py_ssize_t offset = lo.values - lo.keys; + p = start + offset; + pivot = *p; + l += offset; + for (p = start + offset; p > l; --p) + *p = *(p-1); + *l = pivot; + } + } + return 0; + + fail: + return -1; +} + +/* +Return the length of the run beginning at lo, in the slice [lo, hi). lo < hi +is required on entry. "A run" is the longest ascending sequence, with + + lo[0] <= lo[1] <= lo[2] <= ... + +or the longest descending sequence, with + + lo[0] > lo[1] > lo[2] > ... + +Boolean *descending is set to 0 in the former case, or to 1 in the latter. +For its intended use in a stable mergesort, the strictness of the defn of +"descending" is needed so that the caller can safely reverse a descending +sequence without violating stability (strict > ensures there are no equal +elements to get out of order). + +Returns -1 in case of error. +*/ +static Py_ssize_t +count_run(MergeState *ms, PyObject **lo, PyObject **hi, int *descending) +{ + Py_ssize_t k; + Py_ssize_t n; + + assert(lo < hi); + *descending = 0; + ++lo; + if (lo == hi) + return 1; + + n = 2; + IFLT(*lo, *(lo-1)) { + *descending = 1; + for (lo = lo+1; lo < hi; ++lo, ++n) { + IFLT(*lo, *(lo-1)) + ; + else + break; + } + } + else { + for (lo = lo+1; lo < hi; ++lo, ++n) { + IFLT(*lo, *(lo-1)) + break; + } + } + + return n; +fail: + return -1; +} + +/* +Locate the proper position of key in a sorted vector; if the vector contains +an element equal to key, return the position immediately to the left of +the leftmost equal element. [gallop_right() does the same except returns +the position to the right of the rightmost equal element (if any).] + +"a" is a sorted vector with n elements, starting at a[0]. n must be > 0. + +"hint" is an index at which to begin the search, 0 <= hint < n. The closer +hint is to the final result, the faster this runs. + +The return value is the int k in 0..n such that + + a[k-1] < key <= a[k] + +pretending that *(a-1) is minus infinity and a[n] is plus infinity. IOW, +key belongs at index k; or, IOW, the first k elements of a should precede +key, and the last n-k should follow key. + +Returns -1 on error. See listsort.txt for info on the method. +*/ +static Py_ssize_t +gallop_left(MergeState *ms, PyObject *key, PyObject **a, Py_ssize_t n, Py_ssize_t hint) +{ + Py_ssize_t ofs; + Py_ssize_t lastofs; + Py_ssize_t k; + + assert(key && a && n > 0 && hint >= 0 && hint < n); + + a += hint; + lastofs = 0; + ofs = 1; + IFLT(*a, key) { + /* a[hint] < key -- gallop right, until + * a[hint + lastofs] < key <= a[hint + ofs] + */ + const Py_ssize_t maxofs = n - hint; /* &a[n-1] is highest */ + while (ofs < maxofs) { + IFLT(a[ofs], key) { + lastofs = ofs; + assert(ofs <= (PY_SSIZE_T_MAX - 1) / 2); + ofs = (ofs << 1) + 1; + } + else /* key <= a[hint + ofs] */ + break; + } + if (ofs > maxofs) + ofs = maxofs; + /* Translate back to offsets relative to &a[0]. */ + lastofs += hint; + ofs += hint; + } + else { + /* key <= a[hint] -- gallop left, until + * a[hint - ofs] < key <= a[hint - lastofs] + */ + const Py_ssize_t maxofs = hint + 1; /* &a[0] is lowest */ + while (ofs < maxofs) { + IFLT(*(a-ofs), key) + break; + /* key <= a[hint - ofs] */ + lastofs = ofs; + assert(ofs <= (PY_SSIZE_T_MAX - 1) / 2); + ofs = (ofs << 1) + 1; + } + if (ofs > maxofs) + ofs = maxofs; + /* Translate back to positive offsets relative to &a[0]. */ + k = lastofs; + lastofs = hint - ofs; + ofs = hint - k; + } + a -= hint; + + assert(-1 <= lastofs && lastofs < ofs && ofs <= n); + /* Now a[lastofs] < key <= a[ofs], so key belongs somewhere to the + * right of lastofs but no farther right than ofs. Do a binary + * search, with invariant a[lastofs-1] < key <= a[ofs]. + */ + ++lastofs; + while (lastofs < ofs) { + Py_ssize_t m = lastofs + ((ofs - lastofs) >> 1); + + IFLT(a[m], key) + lastofs = m+1; /* a[m] < key */ + else + ofs = m; /* key <= a[m] */ + } + assert(lastofs == ofs); /* so a[ofs-1] < key <= a[ofs] */ + return ofs; + +fail: + return -1; +} + +/* +Exactly like gallop_left(), except that if key already exists in a[0:n], +finds the position immediately to the right of the rightmost equal value. + +The return value is the int k in 0..n such that + + a[k-1] <= key < a[k] + +or -1 if error. + +The code duplication is massive, but this is enough different given that +we're sticking to "<" comparisons that it's much harder to follow if +written as one routine with yet another "left or right?" flag. +*/ +static Py_ssize_t +gallop_right(MergeState *ms, PyObject *key, PyObject **a, Py_ssize_t n, Py_ssize_t hint) +{ + Py_ssize_t ofs; + Py_ssize_t lastofs; + Py_ssize_t k; + + assert(key && a && n > 0 && hint >= 0 && hint < n); + + a += hint; + lastofs = 0; + ofs = 1; + IFLT(key, *a) { + /* key < a[hint] -- gallop left, until + * a[hint - ofs] <= key < a[hint - lastofs] + */ + const Py_ssize_t maxofs = hint + 1; /* &a[0] is lowest */ + while (ofs < maxofs) { + IFLT(key, *(a-ofs)) { + lastofs = ofs; + assert(ofs <= (PY_SSIZE_T_MAX - 1) / 2); + ofs = (ofs << 1) + 1; + } + else /* a[hint - ofs] <= key */ + break; + } + if (ofs > maxofs) + ofs = maxofs; + /* Translate back to positive offsets relative to &a[0]. */ + k = lastofs; + lastofs = hint - ofs; + ofs = hint - k; + } + else { + /* a[hint] <= key -- gallop right, until + * a[hint + lastofs] <= key < a[hint + ofs] + */ + const Py_ssize_t maxofs = n - hint; /* &a[n-1] is highest */ + while (ofs < maxofs) { + IFLT(key, a[ofs]) + break; + /* a[hint + ofs] <= key */ + lastofs = ofs; + assert(ofs <= (PY_SSIZE_T_MAX - 1) / 2); + ofs = (ofs << 1) + 1; + } + if (ofs > maxofs) + ofs = maxofs; + /* Translate back to offsets relative to &a[0]. */ + lastofs += hint; + ofs += hint; + } + a -= hint; + + assert(-1 <= lastofs && lastofs < ofs && ofs <= n); + /* Now a[lastofs] <= key < a[ofs], so key belongs somewhere to the + * right of lastofs but no farther right than ofs. Do a binary + * search, with invariant a[lastofs-1] <= key < a[ofs]. + */ + ++lastofs; + while (lastofs < ofs) { + Py_ssize_t m = lastofs + ((ofs - lastofs) >> 1); + + IFLT(key, a[m]) + ofs = m; /* key < a[m] */ + else + lastofs = m+1; /* a[m] <= key */ + } + assert(lastofs == ofs); /* so a[ofs-1] <= key < a[ofs] */ + return ofs; + +fail: + return -1; +} + +/* Conceptually a MergeState's constructor. */ +static void +merge_init(MergeState *ms, Py_ssize_t list_size, int has_keyfunc, + sortslice *lo) +{ + assert(ms != NULL); + if (has_keyfunc) { + /* The temporary space for merging will need at most half the list + * size rounded up. Use the minimum possible space so we can use the + * rest of temparray for other things. In particular, if there is + * enough extra space, listsort() will use it to store the keys. + */ + ms->alloced = (list_size + 1) / 2; + + /* ms->alloced describes how many keys will be stored at + ms->temparray, but we also need to store the values. Hence, + ms->alloced is capped at half of MERGESTATE_TEMP_SIZE. */ + if (MERGESTATE_TEMP_SIZE / 2 < ms->alloced) + ms->alloced = MERGESTATE_TEMP_SIZE / 2; + ms->a.values = &ms->temparray[ms->alloced]; + } + else { + ms->alloced = MERGESTATE_TEMP_SIZE; + ms->a.values = NULL; + } + ms->a.keys = ms->temparray; + ms->n = 0; + ms->min_gallop = MIN_GALLOP; + ms->listlen = list_size; + ms->basekeys = lo->keys; +} + +/* Free all the temp memory owned by the MergeState. This must be called + * when you're done with a MergeState, and may be called before then if + * you want to free the temp memory early. + */ +static void +merge_freemem(MergeState *ms) +{ + assert(ms != NULL); + if (ms->a.keys != ms->temparray) { + PyMem_Free(ms->a.keys); + ms->a.keys = NULL; + } +} + +/* Ensure enough temp memory for 'need' array slots is available. + * Returns 0 on success and -1 if the memory can't be gotten. + */ +static int +merge_getmem(MergeState *ms, Py_ssize_t need) +{ + int multiplier; + + assert(ms != NULL); + if (need <= ms->alloced) + return 0; + + multiplier = ms->a.values != NULL ? 2 : 1; + + /* Don't realloc! That can cost cycles to copy the old data, but + * we don't care what's in the block. + */ + merge_freemem(ms); + if ((size_t)need > PY_SSIZE_T_MAX / sizeof(PyObject *) / multiplier) { + PyErr_NoMemory(); + return -1; + } + ms->a.keys = (PyObject **)PyMem_Malloc(multiplier * need + * sizeof(PyObject *)); + if (ms->a.keys != NULL) { + ms->alloced = need; + if (ms->a.values != NULL) + ms->a.values = &ms->a.keys[need]; + return 0; + } + PyErr_NoMemory(); + return -1; +} +#define MERGE_GETMEM(MS, NEED) ((NEED) <= (MS)->alloced ? 0 : \ + merge_getmem(MS, NEED)) + +/* Merge the na elements starting at ssa with the nb elements starting at + * ssb.keys = ssa.keys + na in a stable way, in-place. na and nb must be > 0. + * Must also have that ssa.keys[na-1] belongs at the end of the merge, and + * should have na <= nb. See listsort.txt for more info. Return 0 if + * successful, -1 if error. + */ +static Py_ssize_t +merge_lo(MergeState *ms, sortslice ssa, Py_ssize_t na, + sortslice ssb, Py_ssize_t nb) +{ + Py_ssize_t k; + sortslice dest; + int result = -1; /* guilty until proved innocent */ + Py_ssize_t min_gallop; + + assert(ms && ssa.keys && ssb.keys && na > 0 && nb > 0); + assert(ssa.keys + na == ssb.keys); + if (MERGE_GETMEM(ms, na) < 0) + return -1; + sortslice_memcpy(&ms->a, 0, &ssa, 0, na); + dest = ssa; + ssa = ms->a; + + sortslice_copy_incr(&dest, &ssb); + --nb; + if (nb == 0) + goto Succeed; + if (na == 1) + goto CopyB; + + min_gallop = ms->min_gallop; + for (;;) { + Py_ssize_t acount = 0; /* # of times A won in a row */ + Py_ssize_t bcount = 0; /* # of times B won in a row */ + + /* Do the straightforward thing until (if ever) one run + * appears to win consistently. + */ + for (;;) { + assert(na > 1 && nb > 0); + k = ISLT(ssb.keys[0], ssa.keys[0]); + if (k) { + if (k < 0) + goto Fail; + sortslice_copy_incr(&dest, &ssb); + ++bcount; + acount = 0; + --nb; + if (nb == 0) + goto Succeed; + if (bcount >= min_gallop) + break; + } + else { + sortslice_copy_incr(&dest, &ssa); + ++acount; + bcount = 0; + --na; + if (na == 1) + goto CopyB; + if (acount >= min_gallop) + break; + } + } + + /* One run is winning so consistently that galloping may + * be a huge win. So try that, and continue galloping until + * (if ever) neither run appears to be winning consistently + * anymore. + */ + ++min_gallop; + do { + assert(na > 1 && nb > 0); + min_gallop -= min_gallop > 1; + ms->min_gallop = min_gallop; + k = gallop_right(ms, ssb.keys[0], ssa.keys, na, 0); + acount = k; + if (k) { + if (k < 0) + goto Fail; + sortslice_memcpy(&dest, 0, &ssa, 0, k); + sortslice_advance(&dest, k); + sortslice_advance(&ssa, k); + na -= k; + if (na == 1) + goto CopyB; + /* na==0 is impossible now if the comparison + * function is consistent, but we can't assume + * that it is. + */ + if (na == 0) + goto Succeed; + } + sortslice_copy_incr(&dest, &ssb); + --nb; + if (nb == 0) + goto Succeed; + + k = gallop_left(ms, ssa.keys[0], ssb.keys, nb, 0); + bcount = k; + if (k) { + if (k < 0) + goto Fail; + sortslice_memmove(&dest, 0, &ssb, 0, k); + sortslice_advance(&dest, k); + sortslice_advance(&ssb, k); + nb -= k; + if (nb == 0) + goto Succeed; + } + sortslice_copy_incr(&dest, &ssa); + --na; + if (na == 1) + goto CopyB; + } while (acount >= MIN_GALLOP || bcount >= MIN_GALLOP); + ++min_gallop; /* penalize it for leaving galloping mode */ + ms->min_gallop = min_gallop; + } +Succeed: + result = 0; +Fail: + if (na) + sortslice_memcpy(&dest, 0, &ssa, 0, na); + return result; +CopyB: + assert(na == 1 && nb > 0); + /* The last element of ssa belongs at the end of the merge. */ + sortslice_memmove(&dest, 0, &ssb, 0, nb); + sortslice_copy(&dest, nb, &ssa, 0); + return 0; +} + +/* Merge the na elements starting at pa with the nb elements starting at + * ssb.keys = ssa.keys + na in a stable way, in-place. na and nb must be > 0. + * Must also have that ssa.keys[na-1] belongs at the end of the merge, and + * should have na >= nb. See listsort.txt for more info. Return 0 if + * successful, -1 if error. + */ +static Py_ssize_t +merge_hi(MergeState *ms, sortslice ssa, Py_ssize_t na, + sortslice ssb, Py_ssize_t nb) +{ + Py_ssize_t k; + sortslice dest, basea, baseb; + int result = -1; /* guilty until proved innocent */ + Py_ssize_t min_gallop; + + assert(ms && ssa.keys && ssb.keys && na > 0 && nb > 0); + assert(ssa.keys + na == ssb.keys); + if (MERGE_GETMEM(ms, nb) < 0) + return -1; + dest = ssb; + sortslice_advance(&dest, nb-1); + sortslice_memcpy(&ms->a, 0, &ssb, 0, nb); + basea = ssa; + baseb = ms->a; + ssb.keys = ms->a.keys + nb - 1; + if (ssb.values != NULL) + ssb.values = ms->a.values + nb - 1; + sortslice_advance(&ssa, na - 1); + + sortslice_copy_decr(&dest, &ssa); + --na; + if (na == 0) + goto Succeed; + if (nb == 1) + goto CopyA; + + min_gallop = ms->min_gallop; + for (;;) { + Py_ssize_t acount = 0; /* # of times A won in a row */ + Py_ssize_t bcount = 0; /* # of times B won in a row */ + + /* Do the straightforward thing until (if ever) one run + * appears to win consistently. + */ + for (;;) { + assert(na > 0 && nb > 1); + k = ISLT(ssb.keys[0], ssa.keys[0]); + if (k) { + if (k < 0) + goto Fail; + sortslice_copy_decr(&dest, &ssa); + ++acount; + bcount = 0; + --na; + if (na == 0) + goto Succeed; + if (acount >= min_gallop) + break; + } + else { + sortslice_copy_decr(&dest, &ssb); + ++bcount; + acount = 0; + --nb; + if (nb == 1) + goto CopyA; + if (bcount >= min_gallop) + break; + } + } + + /* One run is winning so consistently that galloping may + * be a huge win. So try that, and continue galloping until + * (if ever) neither run appears to be winning consistently + * anymore. + */ + ++min_gallop; + do { + assert(na > 0 && nb > 1); + min_gallop -= min_gallop > 1; + ms->min_gallop = min_gallop; + k = gallop_right(ms, ssb.keys[0], basea.keys, na, na-1); + if (k < 0) + goto Fail; + k = na - k; + acount = k; + if (k) { + sortslice_advance(&dest, -k); + sortslice_advance(&ssa, -k); + sortslice_memmove(&dest, 1, &ssa, 1, k); + na -= k; + if (na == 0) + goto Succeed; + } + sortslice_copy_decr(&dest, &ssb); + --nb; + if (nb == 1) + goto CopyA; + + k = gallop_left(ms, ssa.keys[0], baseb.keys, nb, nb-1); + if (k < 0) + goto Fail; + k = nb - k; + bcount = k; + if (k) { + sortslice_advance(&dest, -k); + sortslice_advance(&ssb, -k); + sortslice_memcpy(&dest, 1, &ssb, 1, k); + nb -= k; + if (nb == 1) + goto CopyA; + /* nb==0 is impossible now if the comparison + * function is consistent, but we can't assume + * that it is. + */ + if (nb == 0) + goto Succeed; + } + sortslice_copy_decr(&dest, &ssa); + --na; + if (na == 0) + goto Succeed; + } while (acount >= MIN_GALLOP || bcount >= MIN_GALLOP); + ++min_gallop; /* penalize it for leaving galloping mode */ + ms->min_gallop = min_gallop; + } +Succeed: + result = 0; +Fail: + if (nb) + sortslice_memcpy(&dest, -(nb-1), &baseb, 0, nb); + return result; +CopyA: + assert(nb == 1 && na > 0); + /* The first element of ssb belongs at the front of the merge. */ + sortslice_memmove(&dest, 1-na, &ssa, 1-na, na); + sortslice_advance(&dest, -na); + sortslice_advance(&ssa, -na); + sortslice_copy(&dest, 0, &ssb, 0); + return 0; +} + +/* Merge the two runs at stack indices i and i+1. + * Returns 0 on success, -1 on error. + */ +static Py_ssize_t +merge_at(MergeState *ms, Py_ssize_t i) +{ + sortslice ssa, ssb; + Py_ssize_t na, nb; + Py_ssize_t k; + + assert(ms != NULL); + assert(ms->n >= 2); + assert(i >= 0); + assert(i == ms->n - 2 || i == ms->n - 3); + + ssa = ms->pending[i].base; + na = ms->pending[i].len; + ssb = ms->pending[i+1].base; + nb = ms->pending[i+1].len; + assert(na > 0 && nb > 0); + assert(ssa.keys + na == ssb.keys); + + /* Record the length of the combined runs; if i is the 3rd-last + * run now, also slide over the last run (which isn't involved + * in this merge). The current run i+1 goes away in any case. + */ + ms->pending[i].len = na + nb; + if (i == ms->n - 3) + ms->pending[i+1] = ms->pending[i+2]; + --ms->n; + + /* Where does b start in a? Elements in a before that can be + * ignored (already in place). + */ + k = gallop_right(ms, *ssb.keys, ssa.keys, na, 0); + if (k < 0) + return -1; + sortslice_advance(&ssa, k); + na -= k; + if (na == 0) + return 0; + + /* Where does a end in b? Elements in b after that can be + * ignored (already in place). + */ + nb = gallop_left(ms, ssa.keys[na-1], ssb.keys, nb, nb-1); + if (nb <= 0) + return nb; + + /* Merge what remains of the runs, using a temp array with + * min(na, nb) elements. + */ + if (na <= nb) + return merge_lo(ms, ssa, na, ssb, nb); + else + return merge_hi(ms, ssa, na, ssb, nb); +} + +/* Two adjacent runs begin at index s1. The first run has length n1, and + * the second run (starting at index s1+n1) has length n2. The list has total + * length n. + * Compute the "power" of the first run. See listsort.txt for details. + */ +static int +powerloop(Py_ssize_t s1, Py_ssize_t n1, Py_ssize_t n2, Py_ssize_t n) +{ + int result = 0; + assert(s1 >= 0); + assert(n1 > 0 && n2 > 0); + assert(s1 + n1 + n2 <= n); + /* midpoints a and b: + * a = s1 + n1/2 + * b = s1 + n1 + n2/2 = a + (n1 + n2)/2 + * + * Those may not be integers, though, because of the "/2". So we work with + * 2*a and 2*b instead, which are necessarily integers. It makes no + * difference to the outcome, since the bits in the expansion of (2*i)/n + * are merely shifted one position from those of i/n. + */ + Py_ssize_t a = 2 * s1 + n1; /* 2*a */ + Py_ssize_t b = a + n1 + n2; /* 2*b */ + /* Emulate a/n and b/n one bit a time, until bits differ. */ + for (;;) { + ++result; + if (a >= n) { /* both quotient bits are 1 */ + assert(b >= a); + a -= n; + b -= n; + } + else if (b >= n) { /* a/n bit is 0, b/n bit is 1 */ + break; + } /* else both quotient bits are 0 */ + assert(a < b && b < n); + a <<= 1; + b <<= 1; + } + return result; +} + +/* The next run has been identified, of length n2. + * If there's already a run on the stack, apply the "powersort" merge strategy: + * compute the topmost run's "power" (depth in a conceptual binary merge tree) + * and merge adjacent runs on the stack with greater power. See listsort.txt + * for more info. + * + * It's the caller's responsibility to push the new run on the stack when this + * returns. + * + * Returns 0 on success, -1 on error. + */ +static int +found_new_run(MergeState *ms, Py_ssize_t n2) +{ + assert(ms); + if (ms->n) { + assert(ms->n > 0); + struct s_slice *p = ms->pending; + Py_ssize_t s1 = p[ms->n - 1].base.keys - ms->basekeys; /* start index */ + Py_ssize_t n1 = p[ms->n - 1].len; + int power = powerloop(s1, n1, n2, ms->listlen); + while (ms->n > 1 && p[ms->n - 2].power > power) { + if (merge_at(ms, ms->n - 2) < 0) + return -1; + } + assert(ms->n < 2 || p[ms->n - 2].power < power); + p[ms->n - 1].power = power; + } + return 0; +} + +/* Regardless of invariants, merge all runs on the stack until only one + * remains. This is used at the end of the mergesort. + * + * Returns 0 on success, -1 on error. + */ +static int +merge_force_collapse(MergeState *ms) +{ + struct s_slice *p = ms->pending; + + assert(ms); + while (ms->n > 1) { + Py_ssize_t n = ms->n - 2; + if (n > 0 && p[n-1].len < p[n+1].len) + --n; + if (merge_at(ms, n) < 0) + return -1; + } + return 0; +} + +/* Compute a good value for the minimum run length; natural runs shorter + * than this are boosted artificially via binary insertion. + * + * If n < 64, return n (it's too small to bother with fancy stuff). + * Else if n is an exact power of 2, return 32. + * Else return an int k, 32 <= k <= 64, such that n/k is close to, but + * strictly less than, an exact power of 2. + * + * See listsort.txt for more info. + */ +static Py_ssize_t +merge_compute_minrun(Py_ssize_t n) +{ + Py_ssize_t r = 0; /* becomes 1 if any 1 bits are shifted off */ + + assert(n >= 0); + while (n >= 64) { + r |= n & 1; + n >>= 1; + } + return n + r; +} + +static void +reverse_sortslice(sortslice *s, Py_ssize_t n) +{ + reverse_slice(s->keys, &s->keys[n]); + if (s->values != NULL) + reverse_slice(s->values, &s->values[n]); +} + +/* Here we define custom comparison functions to optimize for the cases one commonly + * encounters in practice: homogeneous lists, often of one of the basic types. */ + +/* This struct holds the comparison function and helper functions + * selected in the pre-sort check. */ + +/* These are the special case compare functions. + * ms->key_compare will always point to one of these: */ + +/* Heterogeneous compare: default, always safe to fall back on. */ +static int +safe_object_compare(PyObject *v, PyObject *w, MergeState *ms) +{ + /* No assumptions necessary! */ + return PyObject_RichCompareBool(v, w, Py_LT); +} + +/* Homogeneous compare: safe for any two comparable objects of the same type. + * (ms->key_richcompare is set to ob_type->tp_richcompare in the + * pre-sort check.) + */ +static int +unsafe_object_compare(PyObject *v, PyObject *w, MergeState *ms) +{ + PyObject *res_obj; int res; + + /* No assumptions, because we check first: */ + if (Py_TYPE(v)->tp_richcompare != ms->key_richcompare) + return PyObject_RichCompareBool(v, w, Py_LT); + + assert(ms->key_richcompare != NULL); + res_obj = (*(ms->key_richcompare))(v, w, Py_LT); + + if (res_obj == Py_NotImplemented) { + Py_DECREF(res_obj); + return PyObject_RichCompareBool(v, w, Py_LT); + } + if (res_obj == NULL) + return -1; + + if (PyBool_Check(res_obj)) { + res = (res_obj == Py_True); + } + else { + res = PyObject_IsTrue(res_obj); + } + Py_DECREF(res_obj); + + /* Note that we can't assert + * res == PyObject_RichCompareBool(v, w, Py_LT); + * because of evil compare functions like this: + * lambda a, b: int(random.random() * 3) - 1) + * (which is actually in test_sort.py) */ + return res; +} + +/* An adaptive, stable, natural mergesort. See listsort.txt. + * Returns Py_None on success, NULL on error. Even in case of error, the + * list will be some permutation of its input state (nothing is lost or + * duplicated). + */ +/*[clinic input] +list.sort + + * + key as keyfunc: object = None + +Sort the list in ascending order and return None. + +The sort is in-place (i.e. the list itself is modified) and stable (i.e. the +order of two equal elements is maintained). + +If a key function is given, apply it once to each list item and sort them, +ascending or descending, according to their function values. +[clinic start generated code]*/ + +static PyObject * +list_sort_impl(PyListObject *self, PyObject *keyfunc) +/*[clinic end generated code: output=57b9f9c5e23fbe42 input=a74c4cd3ec6b5c08]*/ +{ + MergeState ms; + Py_ssize_t nremaining; + Py_ssize_t minrun; + sortslice lo; + Py_ssize_t saved_ob_size, saved_allocated; + PyObject **saved_ob_item; + PyObject **final_ob_item; + PyObject *result = NULL; /* guilty until proved innocent */ + Py_ssize_t i; + PyObject **keys; + + assert(self != NULL); + assert(PyList_Check(self)); + if (keyfunc == Py_None) + keyfunc = NULL; + + /* The list is temporarily made empty, so that mutations performed + * by comparison functions can't affect the slice of memory we're + * sorting (allowing mutations during sorting is a core-dump + * factory, since ob_item may change). + */ + saved_ob_size = Py_SIZE(self); + saved_ob_item = self->ob_item; + saved_allocated = self->allocated; + Py_SET_SIZE(self, 0); + self->ob_item = NULL; + self->allocated = -1; /* any operation will reset it to >= 0 */ + + if (keyfunc == NULL) { + keys = NULL; + lo.keys = saved_ob_item; + lo.values = NULL; + } + else { + if (saved_ob_size < MERGESTATE_TEMP_SIZE/2) + /* Leverage stack space we allocated but won't otherwise use */ + keys = &ms.temparray[saved_ob_size+1]; + else { + keys = PyMem_Malloc(sizeof(PyObject *) * saved_ob_size); + if (keys == NULL) { + PyErr_NoMemory(); + goto keyfunc_fail; + } + } + + for (i = 0; i < saved_ob_size ; i++) { + keys[i] = PyObject_CallOneArg(keyfunc, saved_ob_item[i]); + if (keys[i] == NULL) { + for (i=i-1 ; i>=0 ; i--) + Py_DECREF(keys[i]); + if (saved_ob_size >= MERGESTATE_TEMP_SIZE/2) + PyMem_Free(keys); + goto keyfunc_fail; + } + } + + lo.keys = keys; + lo.values = saved_ob_item; + } + + + /* The pre-sort check: here's where we decide which compare function to use. + * How much optimization is safe? We test for homogeneity with respect to + * several properties that are expensive to check at compare-time, and + * set ms appropriately. */ + if (saved_ob_size > 1) { + /* Assume the first element is representative of the whole list. */ + PyTypeObject* key_type = Py_TYPE(lo.keys[0]); + + int keys_are_all_same_type = 1; + + /* Prove that assumption by checking every key. */ + for (i=0; i < saved_ob_size; i++) { + PyObject *key = lo.keys[i]; + + if (!Py_IS_TYPE(key, key_type)) { + keys_are_all_same_type = 0; + break; + } + } + + /* Choose the best compare, given what we now know about the keys. */ + if (keys_are_all_same_type && + (ms.key_richcompare = key_type->tp_richcompare) != NULL) { + ms.key_compare = unsafe_object_compare; + } + else { + ms.key_compare = safe_object_compare; + } + + } + /* End of pre-sort check: ms is now set properly! */ + + merge_init(&ms, saved_ob_size, keys != NULL, &lo); + + nremaining = saved_ob_size; + if (nremaining < 2) + goto succeed; + + /* March over the array once, left to right, finding natural runs, + * and extending short natural runs to minrun elements. + */ + minrun = merge_compute_minrun(nremaining); + do { + int descending; + Py_ssize_t n; + + /* Identify next run. */ + n = count_run(&ms, lo.keys, lo.keys + nremaining, &descending); + if (n < 0) + goto fail; + if (descending) + reverse_sortslice(&lo, n); + /* If short, extend to min(minrun, nremaining). */ + if (n < minrun) { + const Py_ssize_t force = nremaining <= minrun ? + nremaining : minrun; + if (binarysort(&ms, lo, lo.keys + force, lo.keys + n) < 0) + goto fail; + n = force; + } + /* Maybe merge pending runs. */ + assert(ms.n == 0 || ms.pending[ms.n -1].base.keys + + ms.pending[ms.n-1].len == lo.keys); + if (found_new_run(&ms, n) < 0) + goto fail; + /* Push new run on stack. */ + assert(ms.n < MAX_MERGE_PENDING); + ms.pending[ms.n].base = lo; + ms.pending[ms.n].len = n; + ++ms.n; + /* Advance to find next run. */ + sortslice_advance(&lo, n); + nremaining -= n; + } while (nremaining); + + if (merge_force_collapse(&ms) < 0) + goto fail; + assert(ms.n == 1); + assert(keys == NULL + ? ms.pending[0].base.keys == saved_ob_item + : ms.pending[0].base.keys == &keys[0]); + assert(ms.pending[0].len == saved_ob_size); + lo = ms.pending[0].base; + +succeed: + result = Py_None; +fail: + if (keys != NULL) { + for (i = 0; i < saved_ob_size; i++) + Py_DECREF(keys[i]); + if (saved_ob_size >= MERGESTATE_TEMP_SIZE/2) + PyMem_Free(keys); + } + + if (self->allocated != -1 && result != NULL) { + /* The user mucked with the list during the sort, + * and we don't already have another error to report. + */ + PyErr_SetString(PyExc_ValueError, "list modified during sort"); + result = NULL; + } + + merge_freemem(&ms); + +keyfunc_fail: + final_ob_item = self->ob_item; + i = Py_SIZE(self); + Py_SET_SIZE(self, saved_ob_size); + self->ob_item = saved_ob_item; + self->allocated = saved_allocated; + if (final_ob_item != NULL) { + /* we cannot use _list_clear() for this because it does not + guarantee that the list is really empty when it returns */ + while (--i >= 0) { + Py_XDECREF(final_ob_item[i]); + } + PyMem_Free(final_ob_item); + } + Py_XINCREF(result); + return result; +} +#undef IFLT +#undef ISLT + +static int +_list_sort(PyObject *v, PyObject *keyfunc) +{ + if (v == NULL || !PyList_Check(v)) { + PyErr_BadInternalCall(); + return -1; + } + v = list_sort_impl((PyListObject *)v, keyfunc); + if (v == NULL) + return -1; + Py_DECREF(v); + return 0; +} diff --git a/src/buildstream/_loader/loadelement.pyx b/src/buildstream/_loader/loadelement.pyx index 319bb595d..52e9a8ecd 100644 --- a/src/buildstream/_loader/loadelement.pyx +++ b/src/buildstream/_loader/loadelement.pyx @@ -379,6 +379,16 @@ cdef class LoadElement: self._dep_cache = FrozenBitMap(self._dep_cache) +# Sort algorithm copied from Python 3.12 +cdef extern from "listsort.c": + int _list_sort(object list, object keyfunc) except -1 + + +# This comparison function does not impose a total ordering, which means +# that the order of the sorted list depends on the order of inputs and +# implementation details of the sort algorithm. Always use the sort +# algorithm from Python 3.12 to ensure a deterministic result for a +# given input order. def _dependency_cmp(Dependency dep_a, Dependency dep_b): cdef LoadElement element_a = dep_a.element cdef LoadElement element_b = dep_b.element @@ -456,7 +466,7 @@ def sort_dependencies(LoadElement element, set visited): visited.add(dep.element) working_elements.append(dep.element) - element.dependencies.sort(key=cmp_to_key(_dependency_cmp)) + _list_sort(element.dependencies, cmp_to_key(_dependency_cmp)) # _parse_dependency_filename(): diff --git a/tests/cachekey/cachekey.py b/tests/cachekey/cachekey.py index 4deac33d4..15d690017 100644 --- a/tests/cachekey/cachekey.py +++ b/tests/cachekey/cachekey.py @@ -59,7 +59,7 @@ from buildstream._testing._cachekeys import check_cache_key_stability, _parse_output_keys from buildstream._testing.runcli import cli # pylint: disable=unused-import -from buildstream._testing._utils.site import HAVE_BZR, HAVE_GIT, IS_LINUX, MACHINE_ARCH +from buildstream._testing._utils.site import IS_LINUX, MACHINE_ARCH from buildstream.plugin import CoreWarnings from buildstream import _yaml @@ -71,13 +71,8 @@ ) -# The cache key test uses a project which exercises all plugins, -# so we cant run it at all if we dont have them installed. -# @pytest.mark.skipif(MACHINE_ARCH != "x86-64", reason="Cache keys depend on architecture") @pytest.mark.skipif(not IS_LINUX, reason="Only available on linux") -@pytest.mark.skipif(HAVE_BZR is False, reason="bzr is not available") -@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available") @pytest.mark.datafiles(DATA_DIR) def test_cache_key(datafiles, cli): project = str(datafiles) diff --git a/tests/cachekey/project/elements/sort0.bst b/tests/cachekey/project/elements/sort0.bst new file mode 100644 index 000000000..2e01b6c9f --- /dev/null +++ b/tests/cachekey/project/elements/sort0.bst @@ -0,0 +1,7 @@ +kind: stack + +depends: +- elements/sort8.bst +- elements/sort9.bst +- elements/sort5.bst +- elements/sort3.bst diff --git a/tests/cachekey/project/elements/sort0.expected b/tests/cachekey/project/elements/sort0.expected new file mode 100644 index 000000000..b572832a7 --- /dev/null +++ b/tests/cachekey/project/elements/sort0.expected @@ -0,0 +1 @@ +57263fea3c5595e04ec22e6fa7e531f69192273df2b35d09e47dd8c8660c6215 diff --git a/tests/cachekey/project/elements/sort1.bst b/tests/cachekey/project/elements/sort1.bst new file mode 100644 index 000000000..f5df41d58 --- /dev/null +++ b/tests/cachekey/project/elements/sort1.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: +- elements/sort9.bst diff --git a/tests/cachekey/project/elements/sort1.expected b/tests/cachekey/project/elements/sort1.expected new file mode 100644 index 000000000..aa068d07e --- /dev/null +++ b/tests/cachekey/project/elements/sort1.expected @@ -0,0 +1 @@ +8cc2dc817a224aa1522b70194d7088143884d3889b164d1b164d5ff3bd62d88f diff --git a/tests/cachekey/project/elements/sort2.bst b/tests/cachekey/project/elements/sort2.bst new file mode 100644 index 000000000..5de361c39 --- /dev/null +++ b/tests/cachekey/project/elements/sort2.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: +- elements/sort6.bst diff --git a/tests/cachekey/project/elements/sort2.expected b/tests/cachekey/project/elements/sort2.expected new file mode 100644 index 000000000..f0de29958 --- /dev/null +++ b/tests/cachekey/project/elements/sort2.expected @@ -0,0 +1 @@ +c9fb7684e517c668a46ce5d0d36207fb071e6e0094b80c5477d329e6cb4c98a5 diff --git a/tests/cachekey/project/elements/sort3.bst b/tests/cachekey/project/elements/sort3.bst new file mode 100644 index 000000000..fc56cb39b --- /dev/null +++ b/tests/cachekey/project/elements/sort3.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: +- elements/sort2.bst diff --git a/tests/cachekey/project/elements/sort3.expected b/tests/cachekey/project/elements/sort3.expected new file mode 100644 index 000000000..cec88365b --- /dev/null +++ b/tests/cachekey/project/elements/sort3.expected @@ -0,0 +1 @@ +d4fb3897bd712221b8348e6beb7a51cce381906bce7dfd05dc310d2ca8b0ed8f diff --git a/tests/cachekey/project/elements/sort4.bst b/tests/cachekey/project/elements/sort4.bst new file mode 100644 index 000000000..5de361c39 --- /dev/null +++ b/tests/cachekey/project/elements/sort4.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: +- elements/sort6.bst diff --git a/tests/cachekey/project/elements/sort4.expected b/tests/cachekey/project/elements/sort4.expected new file mode 100644 index 000000000..4cf860c50 --- /dev/null +++ b/tests/cachekey/project/elements/sort4.expected @@ -0,0 +1 @@ +210117e90eb052b6d8860e51bfec3dcdac17d6867d0d067d15222b22f62f083b diff --git a/tests/cachekey/project/elements/sort5.bst b/tests/cachekey/project/elements/sort5.bst new file mode 100644 index 000000000..40eef8696 --- /dev/null +++ b/tests/cachekey/project/elements/sort5.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: +- elements/sort4.bst diff --git a/tests/cachekey/project/elements/sort5.expected b/tests/cachekey/project/elements/sort5.expected new file mode 100644 index 000000000..dc104e5d0 --- /dev/null +++ b/tests/cachekey/project/elements/sort5.expected @@ -0,0 +1 @@ +c1e365edf5318dd82d8b3906b0facceee090f794548162af4bbfd6372a2c7ffd diff --git a/tests/cachekey/project/elements/sort6.bst b/tests/cachekey/project/elements/sort6.bst new file mode 100644 index 000000000..4aebf34fb --- /dev/null +++ b/tests/cachekey/project/elements/sort6.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: +- elements/sort7.bst diff --git a/tests/cachekey/project/elements/sort6.expected b/tests/cachekey/project/elements/sort6.expected new file mode 100644 index 000000000..2f94b7ac0 --- /dev/null +++ b/tests/cachekey/project/elements/sort6.expected @@ -0,0 +1 @@ +292431a52a4f31d9e45ff46e124e74a6306424fb7175dca4166d876d3fd1ae62 diff --git a/tests/cachekey/project/elements/sort7.bst b/tests/cachekey/project/elements/sort7.bst new file mode 100644 index 000000000..db999b9b7 --- /dev/null +++ b/tests/cachekey/project/elements/sort7.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: +- elements/sort1.bst diff --git a/tests/cachekey/project/elements/sort7.expected b/tests/cachekey/project/elements/sort7.expected new file mode 100644 index 000000000..be955b973 --- /dev/null +++ b/tests/cachekey/project/elements/sort7.expected @@ -0,0 +1 @@ +08af1499d29bc396ba61b851a95388f7a394d5412d0b452eeb0546517aed1ae2 diff --git a/tests/cachekey/project/elements/sort8.bst b/tests/cachekey/project/elements/sort8.bst new file mode 100644 index 000000000..6ffe922c0 --- /dev/null +++ b/tests/cachekey/project/elements/sort8.bst @@ -0,0 +1,2 @@ +kind: stack + diff --git a/tests/cachekey/project/elements/sort8.expected b/tests/cachekey/project/elements/sort8.expected new file mode 100644 index 000000000..319b3ccc7 --- /dev/null +++ b/tests/cachekey/project/elements/sort8.expected @@ -0,0 +1 @@ +94bf67977690c7f438a494779bd418b36dd656a318d49dae525411988ce33e86 diff --git a/tests/cachekey/project/elements/sort9.bst b/tests/cachekey/project/elements/sort9.bst new file mode 100644 index 000000000..6ffe922c0 --- /dev/null +++ b/tests/cachekey/project/elements/sort9.bst @@ -0,0 +1,2 @@ +kind: stack + diff --git a/tests/cachekey/project/elements/sort9.expected b/tests/cachekey/project/elements/sort9.expected new file mode 100644 index 000000000..f59941f80 --- /dev/null +++ b/tests/cachekey/project/elements/sort9.expected @@ -0,0 +1 @@ +a75c0adad402910d65d7c7042433698d55d247fc69d9e43c3ba91c584552e879 diff --git a/tests/cachekey/project/target.bst b/tests/cachekey/project/target.bst index d068f825b..4754b0d74 100644 --- a/tests/cachekey/project/target.bst +++ b/tests/cachekey/project/target.bst @@ -22,4 +22,14 @@ depends: - elements/import2.bst - elements/import3.bst - elements/script1.bst +- elements/sort0.bst +- elements/sort1.bst +- elements/sort2.bst +- elements/sort3.bst +- elements/sort4.bst +- elements/sort5.bst +- elements/sort6.bst +- elements/sort7.bst +- elements/sort8.bst +- elements/sort9.bst - elements/variables1.bst diff --git a/tests/cachekey/project/target.expected b/tests/cachekey/project/target.expected index 617b828e1..364ea12ff 100644 --- a/tests/cachekey/project/target.expected +++ b/tests/cachekey/project/target.expected @@ -1 +1 @@ -c4f7317484ebf493139660bd002bd4d62e9fb8c305f7b76e6d814226e8abf37c \ No newline at end of file +ef4f5380ffaa634a6af1177717d874331af1e66cb4d8928611703809b3ee5dab