diff --git a/.clang-format b/.clang-format index 42556a6..81a8776 100644 --- a/.clang-format +++ b/.clang-format @@ -11,3 +11,4 @@ AllowAllParametersOfDeclarationOnNextLine: true BinPackArguments: false BinPackParameters: false AlignAfterOpenBracket: BlockIndent +AlwaysBreakTemplateDeclarations: Yes diff --git a/.vscode/settings.json b/.vscode/settings.json index 21288fe..69a008f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,10 @@ "--header-insertion=iwyu" ], "files.associations": { - "algorithm": "cpp" + "algorithm": "cpp", + "array": "cpp", + "string": "cpp", + "string_view": "cpp", + "vector": "cpp" } } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cca122..242674b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ add_executable(unit_tests test/tests_main.cpp test/partitioning_tests.cpp test/algorithms_tests.cpp + test/translation_tests.cpp ) target_link_libraries(unit_tests PRIVATE positionless doctest::doctest rapidcheck) diff --git a/include/positionless/detail/algorithm_data.hpp b/include/positionless/detail/algorithm_data.hpp new file mode 100644 index 0000000..48342bc --- /dev/null +++ b/include/positionless/detail/algorithm_data.hpp @@ -0,0 +1,297 @@ +#pragma once + +#include "positionless/detail/algorithm_data_fwd.hpp" +#include "positionless/detail/precondition.hpp" +#include "positionless/partitioning.hpp" + +#include + +#define POSITIONLESS_DEBUG_PRINT false + +#if POSITIONLESS_DEBUG_PRINT +#include +#endif + +namespace positionless::detail { + +/// The algorithm data shared between the iterators. +/// +/// Invariants: the number of parts is one less than the number of iterators we have valid. +template +class algorithm_data { +public: + /// The base iterator type. + using base_iterator = BaseIterator; + + /// An instance of the algorithm data that covers the range [begin, end). + /// + /// Creates two iterators corresponding to `begin` and `end`, but never exposes them directly. + explicit algorithm_data(base_iterator begin, base_iterator end); + + /// Creates a new iterator pointing to the begin of the range, and returns its index. + size_t create_begin_iterator(); + + /// Creates a new iterator pointing to the end of the range, and returns its index. + size_t create_end_iterator(); + + /// Returns the base iterator corresponding to `iterator_index`. + /// + /// - Precondition: iterator `iterator_index` is valid. + base_iterator base(size_t iterator_index) const; + + /// Creates a copy of the base iterator at `iterator_index`, and return the index of the new + /// iterator. + /// + /// - Precondition: iterator `iterator_index` is valid. + size_t copy_iterator(size_t iterator_index); + + /// Marks the iterator at `iterator_index` as destroyed / not valid. + /// + /// - Precondition: iterator `iterator_index` is valid. + void destroy_iterator(size_t iterator_index); + + /// Increments the iterator at `iterator_index` to point to the next element. + /// + /// - Precondition: iterator `iterator_index` is valid. + /// - Precondition: `base(iterator_index)` != `base(end_index())`. + void increment(size_t iterator_index); + + /// Increments the iterator at `iterator_index` by `n` elements. + /// + /// - Precondition: iterator `iterator_index` is valid. + /// - Precondition: `base(iterator_index) + n` <= `base(end_index())`. + void increment_by(size_t iterator_index, size_t n) + requires std::random_access_iterator; + + /// Decrements the iterator at `iterator_index` to point to the next element. + /// + /// - Precondition: iterator `iterator_index` is valid. + /// - Precondition: `base(iterator_index)` != `base(end_index())`. + void decrement(size_t iterator_index); + + /// Decrements the iterator at `iterator_index` by `n` elements. + /// + /// - Precondition: iterator `iterator_index` is valid. + /// - Precondition: `base(iterator_index) + n` <= `base(end_index())`. + void decrement_by(size_t iterator_index, size_t n) + requires std::random_access_iterator; + +private: + /// The partitioning applied to the input slice. + partitioning partitioning_; + + /// Mapping from iterator index to part index. + /// We reserve here a tombstone value to indicate destroyed iterators. + /// This can contain values that are equal to `partitioning_.parts_count()` for iterators that + /// point to the element past the end of the slice. + std::vector parts_mapping_; + +#if POSITIONLESS_DEBUG_PRINT + /// Prints the current state of the data for debugging purposes. + void print_debug() const; +#endif +}; + +/// The value we use to indicate a destroyed iterator. +static constexpr size_t tombstone_part = static_cast(-1); + +template +inline algorithm_data::algorithm_data(BaseIterator begin, BaseIterator end) + : partitioning_(begin, end), parts_mapping_{0, 1} { +#if POSITIONLESS_DEBUG_PRINT + std::cout << "Initialized algorithm_data\n"; + print_debug(); +#endif +} + +template +inline size_t algorithm_data::create_begin_iterator() { + size_t r = copy_iterator(0); +#if POSITIONLESS_DEBUG_PRINT + std::cout << "created begin iterator: " << r << "\n"; + print_debug(); +#endif + return r; +} + +template +inline size_t algorithm_data::create_end_iterator() { + size_t r = copy_iterator(1); +#if POSITIONLESS_DEBUG_PRINT + std::cout << "created end iterator: " << r << "\n"; + print_debug(); +#endif + return r; +} + +template +inline typename algorithm_data::base_iterator +algorithm_data::base(size_t iterator_index) const { + PRECONDITION(iterator_index < parts_mapping_.size()); + const size_t part = parts_mapping_[iterator_index]; + PRECONDITION(part != tombstone_part); + if (part == partitioning_.parts_count()) { + return partitioning_.part(partitioning_.parts_count() - 1).second; + } else { + return partitioning_.part(part).first; + } +} + +template +inline size_t algorithm_data::copy_iterator(size_t iterator_index) { + PRECONDITION(iterator_index < parts_mapping_.size()); + const size_t part = parts_mapping_[iterator_index]; + PRECONDITION(part != tombstone_part); + // Add an empty part before the part corresponding to `iterator_index`. + partitioning_.add_part_begin(part); + // Shift all the mappings that corresponds to parts >= `part` by 1. + std::transform( + parts_mapping_.cbegin(), + parts_mapping_.cend(), + parts_mapping_.begin(), + [part](size_t p) { return p >= part && p != tombstone_part ? p + 1 : p; } + ); + + // Try to find a tombstone to reuse. + size_t r; + auto it = std::find(parts_mapping_.begin(), parts_mapping_.end(), tombstone_part); + if (it != parts_mapping_.end()) { + *it = part; + r = static_cast(std::distance(parts_mapping_.begin(), it)); + } else { + // Add a new mapping for the new iterator. + parts_mapping_.push_back(part); + // Return the index of the new iterator. + r = parts_mapping_.size() - 1; + } +#if POSITIONLESS_DEBUG_PRINT + std::cout << "copied iterator " << iterator_index << " to " << r << "\n"; + print_debug(); +#endif + return r; +} + +template +inline void algorithm_data::destroy_iterator(size_t iterator_index) { + PRECONDITION(iterator_index < parts_mapping_.size()); + parts_mapping_[iterator_index] = tombstone_part; +#if POSITIONLESS_DEBUG_PRINT + std::cout << "destroyed iterator: " << iterator_index << "\n"; + print_debug(); +#endif +} + +template +inline void algorithm_data::increment(size_t iterator_index) { + PRECONDITION(iterator_index < parts_mapping_.size()); + const size_t part = parts_mapping_[iterator_index]; + PRECONDITION(part != tombstone_part); + PRECONDITION(partitioning_.part(part).first != base(1)); + + if (!partitioning_.is_part_empty(part)) { + // Simple case: we just grow the previous part, which will move the iterator. + partitioning_.grow(part - 1); + } else { + // We need to shift some parts, to make space for growing. + size_t next_non_empty_part = part + 1; + while (next_non_empty_part < partitioning_.parts_count() && + partitioning_.is_part_empty(next_non_empty_part)) { + next_non_empty_part++; + } + PRECONDITION(next_non_empty_part < partitioning_.parts_count()); + + // Swap iterators that map to `part` and `next_non_empty_part-1`. + auto it = std::find(parts_mapping_.begin(), parts_mapping_.end(), next_non_empty_part); + PRECONDITION(it != parts_mapping_.end()); + std::swap(*it, parts_mapping_[iterator_index]); + + // Now grow the right part. + partitioning_.grow(next_non_empty_part - 1); + } + +#if POSITIONLESS_DEBUG_PRINT + std::cout << "incremented iterator: " << iterator_index << "\n"; + print_debug(); +#endif +} + +template +inline void algorithm_data::increment_by(size_t iterator_index, size_t n) + requires std::random_access_iterator +{ + // TODO: improve this + for (size_t i = 0; i < n; ++i) { + increment(iterator_index); + } +} + +template +inline void algorithm_data::decrement(size_t iterator_index) { + PRECONDITION(iterator_index < parts_mapping_.size()); + const size_t part = parts_mapping_[iterator_index]; + PRECONDITION(part != tombstone_part); + PRECONDITION(part > 0); + PRECONDITION(partitioning_.part(part).first != base(0)); + + if (part > 0 && !partitioning_.is_part_empty(part - 1)) { + // Simple case: we just shrink the previous part, which will move the iterator backward. + partitioning_.shrink(part - 1); + } else { + // We need to find a previous non-empty part to shrink. + size_t prev_non_empty_part = part - 1; + while (prev_non_empty_part > 0 && partitioning_.is_part_empty(prev_non_empty_part)) { + prev_non_empty_part--; + } + PRECONDITION(!partitioning_.is_part_empty(prev_non_empty_part)); + + // Swap iterators that map to `part` and `prev_non_empty_part+1`. + auto it = std::find(parts_mapping_.begin(), parts_mapping_.end(), prev_non_empty_part + 1); + PRECONDITION(it != parts_mapping_.end()); + std::swap(*it, parts_mapping_[iterator_index]); + + // Now shrink the non-empty part. + partitioning_.shrink(prev_non_empty_part); + } + +#if POSITIONLESS_DEBUG_PRINT + std::cout << "decremented iterator: " << iterator_index << "\n"; + print_debug(); +#endif +} + +template +inline void algorithm_data::decrement_by(size_t iterator_index, size_t n) + requires std::random_access_iterator +{ + // TODO: improve this + for (size_t i = 0; i < n; ++i) { + decrement(iterator_index); + } +} + +#if POSITIONLESS_DEBUG_PRINT +template +inline void algorithm_data::print_debug() const { + std::cout << " - data: "; + for (size_t i = 0; i < partitioning_.parts_count(); ++i) { + auto [begin, end] = partitioning_.part(i); + std::cout << "["; + for (auto it = begin; it != end; ++it) { + std::cout << *it << " "; + } + std::cout << "]"; + } + std::cout << "\n"; + std::cout << " - mapping: ["; + for (size_t i = 0; i < parts_mapping_.size(); ++i) { + if (parts_mapping_[i] == tombstone_part) { + std::cout << "x "; + } else { + std::cout << parts_mapping_[i] << " "; + } + } + std::cout << "]\n"; +} +#endif + +} // namespace positionless::detail diff --git a/include/positionless/detail/algorithm_data_fwd.hpp b/include/positionless/detail/algorithm_data_fwd.hpp new file mode 100644 index 0000000..00471bf --- /dev/null +++ b/include/positionless/detail/algorithm_data_fwd.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace positionless::detail { + +template +class algorithm_data; + +template +using algorithm_data_ptr = std::shared_ptr>; + +} // namespace positionless::detail diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 25e9a73..4aca292 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -16,7 +16,8 @@ namespace positionless { /// constructor most not be invalidated. /// /// - Invariant: parts_count() >= 1 -template class partitioning { +template +class partitioning { public: using iterator = Iterator; using value_type = std::iter_value_t; @@ -74,12 +75,20 @@ template class partitioning { void add_part_end(size_t i); /// Adds a new empty part at the beginning of the `i`th part. + /// + /// Equivalent to `add_part_end(parts_count() - 1)` if `i == parts_count()`. + /// + /// - Precondition: `i <= parts_count()` void add_part_begin(size_t i); /// Adds `count` new empty parts at the end of the `i`th part. void add_parts_end(size_t i, size_t count); /// Adds `count` new empty parts at the beginning of the `i`th part. + /// + /// Equivalent to `add_parts_end(parts_count() - 1, count)` if `i == parts_count()`. + /// + /// - Precondition: `i <= parts_count()` void add_parts_begin(size_t i, size_t count); /// Removes the `i`th part, growing the previous part to cover its range. @@ -145,7 +154,8 @@ inline size_t partitioning::part_size(size_t i) const { return std::distance(boundaries_[i], boundaries_[i + 1]); } -template inline void partitioning::grow(size_t i) { +template +inline void partitioning::grow(size_t i) { PRECONDITION(i + 1 < parts_count()); PRECONDITION(!is_part_empty(i + 1)); boundaries_[i + 1]++; @@ -192,7 +202,7 @@ inline void partitioning::add_part_end(size_t i) { template inline void partitioning::add_part_begin(size_t i) { - PRECONDITION(i < parts_count()); + PRECONDITION(i <= parts_count()); boundaries_.insert(boundaries_.begin() + i, boundaries_[i]); } @@ -204,7 +214,7 @@ inline void partitioning::add_parts_end(size_t i, size_t count) { template inline void partitioning::add_parts_begin(size_t i, size_t count) { - PRECONDITION(i < parts_count()); + PRECONDITION(i <= parts_count()); boundaries_.insert(boundaries_.begin() + i, count, boundaries_[i]); } diff --git a/include/positionless/translation.hpp b/include/positionless/translation.hpp new file mode 100644 index 0000000..6c3ef55 --- /dev/null +++ b/include/positionless/translation.hpp @@ -0,0 +1,166 @@ +#pragma once + +#include "positionless/detail/algorithm_data.hpp" +#include "positionless/detail/precondition.hpp" +#include "positionless/partitioning.hpp" + +#include +#include + +namespace positionless { + +/// An iterator that operates over a partitioned range, with safety checks. +template +class partitioning_iterator { +public: + /// The difference type of the iterator. + using difference_type = std::ptrdiff_t; + /// The value type of the iterator. + using value_type = typename std::iterator_traits::value_type; + + /// An instance coming directly from `algorithm_data`; used for begin/end iterators. + explicit partitioning_iterator( + detail::algorithm_data_ptr data, size_t iterator_index + ); + + /// Destroys `this`, notifying the shared data. + ~partitioning_iterator(); + + /// An instance that compares equal to `other`. + partitioning_iterator(const partitioning_iterator& other); + /// Assigns `other` to `this`, modifying the shared data accordingly. + partitioning_iterator& operator=(const partitioning_iterator& other); + + partitioning_iterator(partitioning_iterator&& other) noexcept = default; + partitioning_iterator& operator=(partitioning_iterator&& other) noexcept = default; + + /// Dereferences `this`. + [[nodiscard]] + value_type operator*() const; + + // Increments `this` to point to the next element. + partitioning_iterator& operator++(); + // Increments `this` to point to the next element, returning a copy of the previous state. + partitioning_iterator operator++(int); + + // Increments `this` to point to the previous element. + partitioning_iterator& operator--() + requires std::bidirectional_iterator; + // Increments `this` to point to the previous element, returning a copy of the previous state. + partitioning_iterator operator--(int) + requires std::bidirectional_iterator; + + /// Returns `true` if `this` and `other` point to the same element. + [[nodiscard]] + bool operator==(const partitioning_iterator& other) const; + /// Returns `true` if `this` and `other` point to different elements. + [[nodiscard]] + bool operator!=(const partitioning_iterator& other) const; + +private: + /// The shared algorithm data. + detail::algorithm_data_ptr data_; + + /// The index of this iterator in shared data's `parts_mapping_`. + size_t iterator_index_{0}; +}; + +/// Creates a pair of partitioning iterators for the range [`begin`, `end`). +/// +/// This allows us to express algorithms in positionless way, adding extra safety checks. +template +[[nodiscard]] +std::pair, partitioning_iterator> +make_partitioning_iterators(BaseIterator begin, BaseIterator end); + +template +inline partitioning_iterator::partitioning_iterator( + detail::algorithm_data_ptr data, size_t iterator_index +) + : data_(std::move(data)), iterator_index_(iterator_index) {} + +template +inline partitioning_iterator::~partitioning_iterator() { + if (data_) { + data_->destroy_iterator(iterator_index_); + } +} + +template +inline partitioning_iterator::partitioning_iterator(const partitioning_iterator& other +) + : data_(other.data_), iterator_index_(data_->copy_iterator(other.iterator_index_)) {} + +template +inline partitioning_iterator& +partitioning_iterator::operator=(const partitioning_iterator& other) { + if (this != &other) { + if (data_) { + data_->destroy_iterator(iterator_index_); + } + data_ = other.data_; + iterator_index_ = data_->copy_iterator(other.iterator_index_); + } + return *this; +} + +template +inline typename partitioning_iterator::value_type +partitioning_iterator::operator*() const { + return *data_->base(iterator_index_); +} + +template +inline partitioning_iterator& partitioning_iterator::operator++() { + data_->increment(iterator_index_); + return *this; +} + +template +inline partitioning_iterator partitioning_iterator::operator++(int) { + partitioning_iterator copy(*this); + data_->increment(iterator_index_); + return copy; +} + +template +inline partitioning_iterator& partitioning_iterator::operator--() + requires std::bidirectional_iterator +{ + data_->decrement(iterator_index_); + return *this; +} + +template +inline partitioning_iterator partitioning_iterator::operator--(int) + requires std::bidirectional_iterator +{ + partitioning_iterator copy(*this); + data_->decrement(iterator_index_); + return copy; +} + +template +inline bool partitioning_iterator::operator==(const partitioning_iterator& other +) const { + return data_ == other.data_ && data_->base(iterator_index_) == data_->base(other.iterator_index_); +} + +template +inline bool partitioning_iterator::operator!=(const partitioning_iterator& other +) const { + return !(*this == other); +} + +template +[[nodiscard]] +inline std::pair, partitioning_iterator> +make_partitioning_iterators(BaseIterator begin, BaseIterator end) { + using iterator_t = partitioning_iterator; + auto data = std::make_shared>(begin, end); + return { + iterator_t(data, data->create_begin_iterator()), iterator_t(data, data->create_end_iterator()) + }; +} + +} // namespace positionless diff --git a/test/translation_tests.cpp b/test/translation_tests.cpp new file mode 100644 index 0000000..0a57176 --- /dev/null +++ b/test/translation_tests.cpp @@ -0,0 +1,71 @@ +#include "positionless/translation.hpp" + +#include "detail/rapidcheck_wrapper.hpp" + +#include +#include + +using positionless::make_partitioning_iterators; +using positionless::partitioning_iterator; + +TEST_PROPERTY( + "partitioning_iterator allows accessing all the elements of the sequence", + ([](std::vector data) { + auto i0 = data.begin(); + auto [it_begin, it_end] = make_partitioning_iterators(data.begin(), data.end()); + for (auto it = it_begin; it != it_end; ++it, ++i0) { + RC_ASSERT(i0 != data.end()); + RC_ASSERT(*it == *i0); + } + }) +); + +TEST_PROPERTY( + "partitioning_iterator allows accessing all the elements of a forward sequence", + ([](std::forward_list data) { + auto i0 = data.begin(); + auto [it_begin, it_end] = make_partitioning_iterators(data.begin(), data.end()); + for (auto it = it_begin; it != it_end; ++it, ++i0) { + RC_ASSERT(i0 != data.end()); + RC_ASSERT(*it == *i0); + } + }) +); + +TEST_PROPERTY( + "partitioning_iterator's preincrement is in sync with postincrement", + ([](std::vector data) { + auto [it_begin, it_end] = make_partitioning_iterators(data.begin(), data.end()); + auto it1 = it_begin; + auto it2 = it_begin; + while (it1 != it_end && it2 != it_end) { + RC_ASSERT(*it1 == *it2); + auto it2_post = it2++; + RC_ASSERT(it1 == it2_post); + RC_ASSERT(!(it1 == it2)); + ++it1; + RC_ASSERT(it1 == it2); + } + }) +); + +TEST_PROPERTY( + "partitioning_iterator allows accessing all the elements of a bidirectional sequence in " + "reverse order", + ([](std::vector data) { + RC_PRE(!data.empty()); + auto i0 = data.crbegin(); + auto [it_begin, it_end] = make_partitioning_iterators(data.begin(), data.end()); + auto it = it_end; + it--; + while (true) { + RC_ASSERT(i0 != data.crend()); + RC_ASSERT(*it == *i0); + if (it == it_begin) { + break; + } + --it; + ++i0; + } + }) +);