├── .clang-format ├── .gitignore ├── AUTHORS ├── LICENSE_1_0.txt ├── README.md ├── fuzz.cpp ├── quadratic_weak_ordering_check.h └── test.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse project files 2 | .cproject 3 | .project 4 | .settings 5 | 6 | # emacs temp files 7 | *~ 8 | 9 | # vim temp files 10 | .*.swp 11 | 12 | # XCode 13 | ^build/ 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata 23 | *.xccheckout 24 | *.moved-aside 25 | DerivedData 26 | *.hmap 27 | *.ipa 28 | *.xcuserstate 29 | *.DS_Store 30 | 31 | # IDE specific folder for JetBrains IDEs 32 | .idea/ 33 | cmake-build-debug/ 34 | cmake-build-release/ 35 | 36 | # Visual Studio Code artifacts 37 | .vscode/* 38 | .history/ 39 | 40 | # Visual Studio artifacts 41 | /VS/ 42 | 43 | # C/C++ build outputs 44 | .build/ 45 | bins 46 | gens 47 | libs 48 | objs 49 | 50 | # C++ ignore from https://github.com/github/gitignore/blob/master/C%2B%2B.gitignore 51 | 52 | # Prerequisites 53 | *.d 54 | 55 | # Compiled Object files 56 | *.slo 57 | *.lo 58 | *.o 59 | *.obj 60 | 61 | # Precompiled Headers 62 | *.gch 63 | *.pch 64 | 65 | # Compiled Dynamic libraries 66 | *.so 67 | *.dylib 68 | *.dll 69 | 70 | # Fortran module files 71 | *.mod 72 | *.smod 73 | 74 | # Compiled Static libraries 75 | *.lai 76 | *.la 77 | *.a 78 | *.lib 79 | 80 | # Executables 81 | *.exe 82 | *.out 83 | *.app 84 | 85 | 86 | # CMake files that may be specific to our installation 87 | 88 | # Build outputs 89 | /build*/ 90 | /visual_studio/ 91 | /benchmark/ 92 | /googletest/ 93 | 94 | # Fuzzer outputs generated by instructions in fuzz/Fuzzing.md 95 | /corpus.zip 96 | /ossfuzz-out/ 97 | /out/ 98 | 99 | # Generated docs 100 | /doc/api 101 | *.orig 102 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # List of authors for copyright purposes, in no particular order 2 | Danila Kutenin 3 | -------------------------------------------------------------------------------- /LICENSE_1_0.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Quadratic strict weak ordering check 2 | ============================================================== 3 | 4 | This repository presents a quadratic strict weak ordering check. Strict weak 5 | ordering for a comparator $comp$ and set $S$ means that it satisfies the conditions for all $x, y, z \in S$: 6 | 7 | 1. Irreflexivity: $comp(x, x)$ must be false 8 | 2. Antisymmetry: $comp(x, y)$ implies $!comp(y, x)$ 9 | 3. Transitivity: if $comp(x, y)$ and $comp(y, z)$ then $comp(x, z)$ 10 | 4. Two objects x and y are equivalent if both $comp(x, y)$ and $comp(y, x)$ are false. Then transitivity of equivalence should be met. 11 | 12 | Naive algorithm for steps 3 and 4 require $O(|S|^3)$ comparisons. However, there are faster algorithms to do that. In order to check if set $S$ satisfies this condition, we are going to do the following: 13 | 14 | 1. Sort $S$ with some algorithm that correctly sorts it if $S$ satisfies strict weak ordering. **And does not fail if it does not satisfy it.** Bubble sort or heap sort should be good. 15 | 2. Check that the range is sorted. If it's not, we know that $S$ does not satisfy strict weak ordering. 16 | 3. If it's sorted, use the following algorithm: 17 | 18 | 1. Find the minimal $P$ such that $S[0] < S[P]$. If no such $P$, set P to SIZE. 19 | 2. For all $A < B < P$ check $(!comp(S[A], S[B])$ and $!comp(S[B], S[A]))$, i.e. all elements before $P$ are equivalent. 20 | 3. For all $A < P$ and $B \geq P$, check $(comp(S[A], S[B])$ and $!comp(S[B], S[A]))$, i.e. all elements separated by $P$ follows transitivity. 21 | 4. Remove the first P elements from S. Go to step 2. 22 | 23 | If any condition at step 2 and 3 is not met, return FALSE. It means strict weak ordering is not met. 24 | 25 | The runtime of this algorithm is $O(|S|^2)$. Assume we eliminate $x_1, \ldots, x_k$ elements on each iteration and thus we made 26 | 27 | $$ 28 | O(x_1^2 + x_1(|S| - x_1) + x_2^2 + x_2(|S| - x_1 - x_2) + \ldots) = O(x_1|S| + x_2|S| + \ldots) = O(|S| \times |S|) 29 | $$ 30 | 31 | comparisons. 32 | 33 | # How can you use it? 34 | 35 | This functions can be used for testing the properties of the passed ranges. Suggested APIs: 36 | 37 | * `bool strict_weak_ordering_check(first, last, settings, comp)` 38 | 39 | `settings`, `comp` arguments are not mandatory. We support C++11. `settings` support `.prior_sort` and some other 40 | implementation defined arguments. See [test](./test.cpp) for examples. 41 | 42 | ```cpp 43 | $ clang++ test.cpp -g -std=c++11 -O3 -o test 44 | $ clang++ -g fuzz.cpp -fsanitize=address,fuzzer -std=c++11 -O3 -o fuzz 45 | ``` 46 | 47 | In standard libraries this can be used to check if the range satisfies strict weak ordering for a sampled range of 48 | $O(\sqrt{n})$ elements without hurting too much performance. 49 | 50 | [Sortcheck repository](https://github.com/yugr/sortcheck#what-are-current-results) found a lot of problems out there and this can contribute to find even more. 51 | 52 | # Proof 53 | 54 | If strict weak ordering is met, the algorithm obviously returns TRUE. Assume it returns TRUE and strict weak ordering is not met. 55 | 56 | ## Claim 1. If $comp(S[i], S[j])$, then $i < j$ 57 | 58 | Assume $i \geq j$ and $comp(S[i], S[j])$. Then there is a moment when $P$ at step 1 exceeds element $j$ for the first time. Then it holds 59 | 60 | $0 < j - \mathrm{removed index} = J < P$ 61 | 62 | Then element with index $i - \mathrm{removed index} = I = B$ and $A = J$ is checked at steps 2 or 3 and the opposite condition holds. 63 | 64 | ## Claim 2. $comp(S[i], S[i])$ is false 65 | 66 | If it's true, then $i < i$. 67 | 68 | ## Claim 3. $comp(S[i], S[j])$ and $comp(S[j], S[k])$ implies $comp(S[i], S[k])$ 69 | 70 | Assume the opposite, i.e. $comp(S[i], S[j])$ and $comp(S[j], S[k])$ and $!comp(S[i], S[k])$. 71 | According to claim 1, we have $i < j < k$. 72 | 73 | Find $P$ which exceeds element $i$ for the first time. Then at that step we have 74 | 75 | * $comp(S[i - \mathrm{removed index}], S[j - \mathrm{removed index}])$ 76 | * If $P$ exceeds element $j$, then step 2 should return false 77 | * $comp(S[j - \mathrm{removed index}], S[k - \mathrm{removed index}])$ 78 | * If $P$ exceeds element $k$, then step 2 should return false 79 | * It means $P < j - \mathrm{removed index} < k - \mathrm{removed index}$. Then for $A = i - \mathrm{removed index}$, $B = k - \mathrm{removed index}$ holds the desired condition. 80 | 81 | ## Claim 4. Assymetry $comp(S[i], S[j])$ implies $!comp(S[j], S[i])$ 82 | 83 | Obviously holds from claim 1: $i < j < i$ otherwise. 84 | 85 | ## Claim 5. Transitivity of equivalence also holds 86 | 87 | It means that $!comp(S[I], S[J])$, and $!comp(S[J], S[I])$, and $!comp(S[J], S[K])$, and $!comp(S[K], S[J])$, and ( $comp(S[I], S[K])$ or $comp(S[K], S[I])$ ). 88 | 89 | Without loss of generality assume $comp(S[I], S[K])$ (also means $I < K$ according to claim 1). If $I \leq J$, then 90 | 91 | * Find first $P$ which exceeds element at position $I$ 92 | * If $P > K$, then $!comp(S[I], S[K])$ according to step 2. 93 | * If $P \leq K$, then 94 | * If $P > J$, then $comp(S[J], S[K])$ according to step 3. 95 | * If $P \leq J$, then $comp(S[I], S[J])$ according to step 3. 96 | 97 | If $J < I$, then 98 | 99 | * Find first $P$ which exceeds element at position $J$ 100 | * If $K < P$, then $I < K < P$ and $!comp(S[I], S[K])$ 101 | * If $K \geq P$, then $comp(S[J], S[K])$ 102 | 103 | -------------------------------------------------------------------------------- /fuzz.cpp: -------------------------------------------------------------------------------- 1 | /* Copyright Danila Kutenin, 2023-. 2 | * Distributed under the Boost Software License, Version 1.0. 3 | * (See accompanying file LICENSE_1_0.txt or copy at 4 | * https://boost.org/LICENSE_1_0.txt) 5 | */ 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "quadratic_weak_ordering_check.h" 14 | 15 | static constexpr int kNumElements = 10; 16 | 17 | extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, 18 | std::size_t size) { 19 | std::vector> results(kNumElements, 20 | std::vector(kNumElements)); 21 | size_t current_size = 0; 22 | for (size_t i = 0; i < kNumElements; ++i) { 23 | for (size_t j = 0; j < kNumElements; ++j) { 24 | if (current_size < size) { 25 | results[i][j] = data[current_size]; 26 | ++current_size; 27 | } 28 | } 29 | } 30 | std::vector to_check(kNumElements); 31 | std::iota(to_check.begin(), to_check.end(), 0); 32 | StrictWeakOrderingSettings settings; 33 | settings.prior_sort = true; 34 | auto comp = [&results](const int x, const int y) { return results[x][y]; }; 35 | auto result = strict_weak_ordering_check(to_check.begin(), to_check.end(), 36 | settings, comp); 37 | bool naive_accurate = true; 38 | for (size_t i = 0; i < kNumElements; ++i) { 39 | if (comp(i, i)) naive_accurate = false; 40 | for (size_t j = 0; j < kNumElements; ++j) { 41 | if (comp(i, j) && comp(j, i)) naive_accurate = false; 42 | for (size_t k = 0; k < kNumElements; ++k) { 43 | if (comp(i, j) && comp(j, k) && !comp(i, k)) naive_accurate = false; 44 | 45 | bool eq_i_j = !comp(i, j) && !comp(j, i); 46 | bool eq_j_k = !comp(j, k) && !comp(k, j); 47 | bool eq_i_k = !comp(i, k) && !comp(k, i); 48 | if (eq_i_j && eq_j_k && !eq_i_k) naive_accurate = false; 49 | } 50 | } 51 | } 52 | if (naive_accurate != result.ok) { 53 | abort(); 54 | } 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /quadratic_weak_ordering_check.h: -------------------------------------------------------------------------------- 1 | /* Copyright Danila Kutenin, 2023-. 2 | * Distributed under the Boost Software License, Version 1.0. 3 | * (See accompanying file LICENSE_1_0.txt or copy at 4 | * https://boost.org/LICENSE_1_0.txt) 5 | */ 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | struct StrictWeakOrderingSettings { 14 | bool prior_sort = false; 15 | bool print_possible_reason = false; 16 | // Not yet supported. 17 | bool print_elements = false; 18 | }; 19 | 20 | struct StrictWeakOrderingResult { 21 | bool ok = true; 22 | std::string possible_reason; 23 | }; 24 | 25 | namespace internal { 26 | 27 | inline StrictWeakOrderingResult OkResult() { 28 | return StrictWeakOrderingResult{}; 29 | } 30 | 31 | inline StrictWeakOrderingResult CreateSimpleFailedResult() { 32 | StrictWeakOrderingResult result; 33 | result.ok = false; 34 | return result; 35 | } 36 | 37 | template 38 | inline StrictWeakOrderingResult CreateFailedResult(const std::string& reason, 39 | Index i, Index j) { 40 | StrictWeakOrderingResult result; 41 | result.ok = false; 42 | result.possible_reason = reason + ": comp(first[" + std::to_string(j) + 43 | "], first[" + std::to_string(i) + "]) is true;"; 44 | return result; 45 | } 46 | 47 | template 48 | inline StrictWeakOrderingResult strict_weak_ordering_check_loop( 49 | Iter first, Iter last, const StrictWeakOrderingSettings& settings, 50 | Compare comp) { 51 | if (settings.prior_sort) { 52 | // Should not fail is comp is not strict weak ordering. But if comp crashes, 53 | // nothing is going to help. 54 | // Strictly speaking, this is UB but all implementations in gcc/clang/msvc 55 | // are fine. 56 | std::make_heap(first, last, comp); 57 | std::sort_heap(first, last, comp); 58 | } 59 | StrictWeakOrderingResult result; 60 | if (first != last) { 61 | Iter copy_first = first; 62 | Iter next = copy_first; 63 | while (++next != last) { 64 | if (comp(*next, *copy_first)) { 65 | if (settings.print_possible_reason) { 66 | return CreateFailedResult("Unsorted array ", copy_first - first, 67 | next - first); 68 | } else { 69 | return CreateSimpleFailedResult(); 70 | } 71 | } 72 | copy_first = next; 73 | } 74 | } 75 | typedef typename std::iterator_traits::difference_type diff_type; 76 | diff_type size = last - first; 77 | diff_type P = 0; 78 | while (P < size) { 79 | diff_type Q = P + 1; 80 | while (Q < size && !comp(first[P], first[Q])) { 81 | ++Q; 82 | } 83 | for (diff_type B = P; B < Q; ++B) { 84 | for (diff_type A = P; A <= B; ++A) { 85 | if (comp(first[A], first[B]) || comp(first[B], first[A])) { 86 | return CreateSimpleFailedResult(); 87 | } 88 | } 89 | } 90 | for (diff_type A = P; A < Q; ++A) { 91 | for (diff_type B = Q; B < size; ++B) { 92 | if (!comp(first[A], first[B]) || comp(first[B], first[A])) { 93 | return CreateSimpleFailedResult(); 94 | } 95 | } 96 | } 97 | P = Q; 98 | } 99 | return OkResult(); 100 | } 101 | 102 | } // namespace internal 103 | 104 | template 105 | inline StrictWeakOrderingResult strict_weak_ordering_check( 106 | Iter first, Iter last, const StrictWeakOrderingSettings& settings, 107 | Compare comp) { 108 | return internal::strict_weak_ordering_check_loop(first, last, settings, comp); 109 | } 110 | 111 | template 112 | inline StrictWeakOrderingResult strict_weak_ordering_check( 113 | Iter first, Iter last, const StrictWeakOrderingSettings& settings) { 114 | return strict_weak_ordering_check( 115 | first, last, settings, 116 | std::less::value_type>()); 117 | } 118 | 119 | template 120 | inline StrictWeakOrderingResult strict_weak_ordering_check(Iter first, 121 | Iter last) { 122 | return strict_weak_ordering_check( 123 | first, last, StrictWeakOrderingSettings{}, 124 | std::less::value_type>()); 125 | } 126 | -------------------------------------------------------------------------------- /test.cpp: -------------------------------------------------------------------------------- 1 | /* Copyright Danila Kutenin, 2023-. 2 | * Distributed under the Boost Software License, Version 1.0. 3 | * (See accompanying file LICENSE_1_0.txt or copy at 4 | * https://boost.org/LICENSE_1_0.txt) 5 | */ 6 | #include 7 | #include 8 | #include 9 | 10 | #include "quadratic_weak_ordering_check.h" 11 | 12 | void test_empty() { 13 | int x = 0; 14 | auto result = strict_weak_ordering_check(&x, &x); 15 | assert(result.ok); 16 | } 17 | 18 | void test_clean() { 19 | std::vector v(100); 20 | for (int i = 0; i < 100; ++i) { 21 | v[i] = i; 22 | } 23 | auto result = strict_weak_ordering_check(v.begin(), v.end()); 24 | assert(result.ok); 25 | } 26 | 27 | void test_random() { 28 | std::random_device dev; 29 | std::mt19937_64 rng(dev()); 30 | std::uniform_int_distribution dist(1, 1000000); 31 | std::vector v(100); 32 | for (int i = 0; i < 100; ++i) { 33 | v[i] = dist(rng); 34 | } 35 | StrictWeakOrderingSettings settings; 36 | settings.prior_sort = true; 37 | auto result = strict_weak_ordering_check(v.begin(), v.end(), settings); 38 | assert(result.ok); 39 | } 40 | 41 | void test_nan() { 42 | std::random_device dev; 43 | std::mt19937_64 rng(dev()); 44 | std::uniform_int_distribution dist(1, 1000000); 45 | std::vector v(100); 46 | for (int i = 0; i < 100; ++i) { 47 | v[i] = dist(rng); 48 | } 49 | v[99] = std::numeric_limits::quiet_NaN(); 50 | StrictWeakOrderingSettings settings; 51 | settings.prior_sort = true; 52 | auto result = strict_weak_ordering_check(v.begin(), v.end(), settings); 53 | assert(!result.ok); 54 | assert(result.possible_reason.empty()); 55 | } 56 | 57 | void test_bad_comp() { 58 | std::random_device dev; 59 | std::mt19937_64 rng(dev()); 60 | std::uniform_int_distribution dist(1, 1000000); 61 | std::vector v(100); 62 | for (int i = 0; i < 100; ++i) { 63 | v[i] = dist(rng); 64 | } 65 | v[99] = v[98]; 66 | StrictWeakOrderingSettings settings; 67 | settings.prior_sort = true; 68 | auto result = strict_weak_ordering_check( 69 | v.begin(), v.end(), settings, 70 | [](const int x, const int y) { return x <= y; }); 71 | assert(!result.ok); 72 | assert(result.possible_reason.empty()); 73 | } 74 | 75 | int main() { 76 | for (int i = 0; i < 10; ++i) { 77 | test_empty(); 78 | test_clean(); 79 | test_random(); 80 | test_nan(); 81 | test_bad_comp(); 82 | } 83 | } 84 | --------------------------------------------------------------------------------