Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions include/pixie/split_span.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#pragma once

/**
* @file split_span.h
* @brief Allocation-free logical ranges split across at most two spans.
*/

#include <array>
#include <cstddef>
#include <span>

namespace pixie {

/**
* @brief A logical contiguous sequence stored in at most two physical spans.
*
* @details Empty input spans are canonicalized away. Iteration visits only
* non-empty physical spans in logical order. The descriptor owns no elements;
* its spans follow the lifetime and invalidation rules of their backing
* storage.
*
* @tparam T Viewed element type, optionally const-qualified.
*/
template <class T>
class SplitSpan {
public:
using span_type = std::span<T>;
using const_iterator = typename std::array<span_type, 2>::const_iterator;

/** @brief Construct an empty logical range. */
constexpr SplitSpan() = default;

/** @brief Construct a range stored in one physical span. */
constexpr explicit SplitSpan(span_type segment)
: segments_{segment, {}}, segment_count_(segment.empty() ? 0 : 1) {}

/** @brief Construct a range stored in up to two physical spans. */
constexpr SplitSpan(span_type first, span_type second) {
if (first.empty()) {
first = second;
second = {};
}
segments_ = {first, second};
segment_count_ = static_cast<std::size_t>(!first.empty()) +
static_cast<std::size_t>(!second.empty());
}

/** @brief Return the total number of logical elements. */
constexpr std::size_t size() const noexcept {
return segments_[0].size() + segments_[1].size();
}

/** @brief Return whether the logical range is empty. */
constexpr bool empty() const noexcept { return segment_count_ == 0; }

/** @brief Return the number of non-empty physical segments. */
constexpr std::size_t segment_count() const noexcept {
return segment_count_;
}

/** @brief Iterate over the non-empty physical segments in logical order. */
constexpr const_iterator begin() const noexcept { return segments_.begin(); }

/** @brief Return the end iterator for the non-empty physical segments. */
constexpr const_iterator end() const noexcept {
return segments_.begin() + static_cast<std::ptrdiff_t>(segment_count_);
}

private:
std::array<span_type, 2> segments_{};
std::size_t segment_count_ = 0;
};

} // namespace pixie
157 changes: 150 additions & 7 deletions include/pixie/storage.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

#include <pixie/serialization.h>
#include <pixie/split_span.h>

#include <concepts>
#include <cstddef>
Expand All @@ -21,11 +22,51 @@ namespace pixie {
/**
* @brief CRTP facade for byte-addressable storage.
*
* @details `Impl` must provide `size_bytes_impl()` and the following required
* extension points.
*
* @par Required: `begin_position_impl() const`
* Returns the absolute logical position of the first exposed byte as a
* `position_type`. The result may advance when an implementation evicts bytes,
* but positions of retained bytes do not change.
*
* @par Required: `end_position_impl() const`
* Returns the absolute logical position one past the last exposed byte as a
* `position_type`. It must be at least `begin_position_impl()`, and their
* difference must be representable by `std::size_t` and equal
* `size_bytes_impl()`.
*
* @par Required: `segments_impl(position, count_bytes) const`
* Accepts a `position_type` and a `std::size_t` and returns
* `SplitSpan<const std::byte>` containing exactly the bytes in `[position,
* position + count_bytes)`. The physical spans are ordered by logical position
* and their combined size equals `count_bytes`. Before calling this hook, the
* facade verifies without overflow that `position` is in the closed range from
* `begin_position_impl()` through `end_position_impl()` and that `count_bytes
* <= end_position_impl() - position`; an empty range at the end position is
* valid. The returned spans borrow the implementation's backing storage. The
* implementation must document the backing storage's ownership requirements
* and the operations that invalidate its spans.
*
* @par Optional: `segments_impl(position, count_bytes)`
* A mutable implementation may provide this extension point to enable
* writable segment access. It accepts the same parameter types as the const
* overload and returns `SplitSpan<std::byte>`. It has the same range, ordering,
* size, lifetime, and invalidation contract as the const overload, and writes
* through the returned spans modify the corresponding logical bytes. If the
* implementation also provides `prepare_segments_impl(position, count_bytes)`,
* the facade calls that hook before validating the range. The preparation hook
* must make the complete requested range available or throw without changing
* the implementation.
*
* @tparam Impl Concrete storage implementation.
*/
template <class Impl>
class StorageBase : public SerializationBase<Impl> {
public:
/** @brief Monotonic logical byte position used to address storage ranges. */
using position_type = std::uint64_t;

/**
* @brief Return the logical exposed storage size in bytes.
* @details An owning implementation may reserve or pad more memory; use
Expand All @@ -39,15 +80,96 @@ class StorageBase : public SerializationBase<Impl> {
/** @brief Check whether the storage is empty. */
bool empty() const { return size_bytes() == 0; }

/** @brief Return a read-only view of all logical exposed bytes. */
std::span<const std::byte> as_bytes() const { return impl().as_bytes_impl(); }
/** @brief Return the first logical byte position currently exposed. */
position_type begin_position() const { return impl().begin_position_impl(); }

/** @brief Return the position one past the last logical byte exposed. */
position_type end_position() const { return impl().end_position_impl(); }
Comment on lines +84 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the new storage extension-point requirements

Add canonical Doxygen contracts for begin_position_impl(), end_position_impl(), and both segments_impl() variants. The facade now requires these hooks—and serialization depends on them—but only documents the public wrappers, leaving implementers without the required invariants for absolute positions, range validation, segment ordering, and lifetime/invalidation behavior. Concrete-class comments do not replace the CRTP extension-point contract.

AGENTS.md reference: AGENTS.md:L86-L92

Useful? React with 👍 / 👎.


/** @brief Return whether a complete logical range is currently exposed. */
bool contains(position_type position, std::size_t count_bytes) const {
const position_type begin = begin_position();
const position_type end = end_position();
return position >= begin && position <= end &&
count_bytes <= end - position;
}

/** @brief Return a contiguous read-only view of all logical exposed bytes. */
std::span<const std::byte> as_bytes() const
requires requires(const Impl& value) { value.as_bytes_impl(); }
{
return impl().as_bytes_impl();
}

/**
* @brief Return all logical bytes as one or two writable physical spans.
* @details Available only for mutable storage implementations. Mutating or
* resizing the storage may invalidate the returned descriptor.
*/
SplitSpan<std::byte> segments()
requires requires(Impl& value) {
{
value.segments_impl(position_type{}, std::size_t{})
} -> std::same_as<SplitSpan<std::byte>>;
}
{
return segments(begin_position(), size_bytes());
}

/**
* @brief Return all logical bytes as one or two physical spans.
* @details Contiguous storage returns one segment. A ring-backed storage can
* return its tail followed by its head without allocation or copying.
*/
SplitSpan<const std::byte> segments() const {
return segments(begin_position(), size_bytes());
}

/**
* @brief Return a checked writable logical byte range as one or two spans.
* @details An implementation with a preparation hook may make a future range
* available before checking it. Any newly exposed bytes remain the caller's
* responsibility to initialize.
* @param position First logical byte position in the range.
* @param count_bytes Number of logical bytes in the range.
* @throws std::out_of_range if the range cannot be made available.
*/
SplitSpan<std::byte> segments(position_type position, std::size_t count_bytes)
requires requires(Impl& value) {
{
value.segments_impl(position_type{}, std::size_t{})
} -> std::same_as<SplitSpan<std::byte>>;
}
{
if constexpr (requires(Impl& value) {
value.prepare_segments_impl(position_type{}, std::size_t{});
}) {
impl().prepare_segments_impl(position, count_bytes);
}
validate_range(position, count_bytes);
return impl().segments_impl(position, count_bytes);
}

/**
* @brief Return a checked logical byte range as one or two physical spans.
* @param position First logical byte position in the range.
* @param count_bytes Number of logical bytes in the range.
* @throws std::out_of_range if the range is outside this storage.
*/
SplitSpan<const std::byte> segments(position_type position,
std::size_t count_bytes) const {
validate_range(position, count_bytes);
return impl().segments_impl(position, count_bytes);
}

/**
* @brief Return a read-only view as 16-bit words.
* @throws std::invalid_argument if the data is misaligned or its size is not
* divisible by the word size.
*/
std::span<const std::uint16_t> as_words16() const {
std::span<const std::uint16_t> as_words16() const
requires requires(const Impl& value) { value.as_bytes_impl(); }
{
return as_words<std::uint16_t>();
}

Expand All @@ -56,20 +178,32 @@ class StorageBase : public SerializationBase<Impl> {
* @throws std::invalid_argument if the data is misaligned or its size is not
* divisible by the word size.
*/
std::span<const std::uint64_t> as_words64() const {
std::span<const std::uint64_t> as_words64() const
requires requires(const Impl& value) { value.as_bytes_impl(); }
{
return as_words<std::uint64_t>();
}

/** @brief Return a non-owning read-only view of all exposed bytes. */
auto view() const { return impl().view_impl(0, size_bytes()); }
auto view() const
requires requires(const Impl& value) {
value.view_impl(std::size_t{}, std::size_t{});
}
{
return impl().view_impl(0, size_bytes());
}

/**
* @brief Return a non-owning read-only byte subrange.
* @param offset_bytes First byte in the view.
* @param count_bytes Number of bytes in the view.
* @throws std::out_of_range if the subrange is outside this storage.
*/
auto view(std::size_t offset_bytes, std::size_t count_bytes) const {
auto view(std::size_t offset_bytes, std::size_t count_bytes) const
requires requires(const Impl& value) {
value.view_impl(std::size_t{}, std::size_t{});
}
{
return impl().view_impl(offset_bytes, count_bytes);
}

Expand All @@ -78,7 +212,9 @@ class StorageBase : public SerializationBase<Impl> {
*/
void serialize_impl(BinaryWriter& writer) const {
writer.write_size(size_bytes());
writer.write_bytes(as_bytes());
for (const std::span<const std::byte> segment : segments()) {
writer.write_bytes(segment);
}
}

/** @brief Resize mutable storage to hold at least @p size_bits bits. */
Expand Down Expand Up @@ -124,6 +260,13 @@ class StorageBase : public SerializationBase<Impl> {
}

private:
/** @brief Validate a logical byte range without overflowing. */
void validate_range(position_type position, std::size_t count_bytes) const {
if (!contains(position, count_bytes)) {
throw std::out_of_range("Storage range is outside the working window");
}
}

/** @brief Return this facade as its concrete CRTP implementation. */
const Impl& impl() const { return static_cast<const Impl&>(*this); }

Expand Down
18 changes: 18 additions & 0 deletions include/pixie/storage/aligned.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ class AlignedStorage : public StorageBase<AlignedStorage> {
/** @brief Return the logical number of exposed bytes. */
std::size_t size_bytes_impl() const { return logical_size_bytes_; }

/** @brief Return the first logical byte position. */
position_type begin_position_impl() const { return 0; }

/** @brief Return the position one past the final logical byte. */
position_type end_position_impl() const { return logical_size_bytes_; }

/** @brief Return the logical number of exposed bytes. */
std::size_t logical_size_bytes() const { return logical_size_bytes_; }

Expand All @@ -81,6 +87,18 @@ class AlignedStorage : public StorageBase<AlignedStorage> {
.first(logical_size_bytes_);
}

/** @brief Return a checked logical byte range as one physical segment. */
SplitSpan<const std::byte> segments_impl(std::size_t offset_bytes,
std::size_t count_bytes) const {
return SplitSpan(as_bytes_impl().subspan(offset_bytes, count_bytes));
}

/** @brief Return a checked logical byte range as one writable segment. */
SplitSpan<std::byte> segments_impl(std::size_t offset_bytes,
std::size_t count_bytes) {
return SplitSpan(writable_bytes_impl().subspan(offset_bytes, count_bytes));
}

/** @brief Return a checked read-only byte subrange. */
ReadOnlyStorageView view_impl(std::size_t offset_bytes,
std::size_t count_bytes) const {
Expand Down
2 changes: 2 additions & 0 deletions include/pixie/storage/implementations.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*
* - `AlignedStorage`: owning, mutable, 64-byte-aligned storage.
* - `ReadOnlyStorageView`: non-owning read-only byte storage.
* - `SlidingWindowStorage`: owning fixed-capacity sliding byte storage.
*/

#include <pixie/storage/aligned.h>
#include <pixie/storage/read_only_view.h>
#include <pixie/storage/sliding_window.h>
12 changes: 12 additions & 0 deletions include/pixie/storage/read_only_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,21 @@ class ReadOnlyStorageView : public StorageBase<ReadOnlyStorageView> {
/** @brief Return the number of viewed bytes. */
std::size_t size_bytes_impl() const { return data_.size(); }

/** @brief Return the first logical byte position. */
position_type begin_position_impl() const { return 0; }

/** @brief Return the position one past the final logical byte. */
position_type end_position_impl() const { return data_.size(); }

/** @brief Return the viewed bytes. */
std::span<const std::byte> as_bytes_impl() const { return data_; }

/** @brief Return a checked viewed byte range as one physical segment. */
SplitSpan<const std::byte> segments_impl(std::size_t offset_bytes,
std::size_t count_bytes) const {
return SplitSpan(data_.subspan(offset_bytes, count_bytes));
}

/** @brief Return a checked read-only byte subrange. */
ReadOnlyStorageView view_impl(std::size_t offset_bytes,
std::size_t count_bytes) const {
Expand Down
Loading
Loading