diff --git a/agents/index_vertex_descriptor_plan.md b/agents/index_vertex_descriptor_plan.md new file mode 100644 index 0000000..7e78e8b --- /dev/null +++ b/agents/index_vertex_descriptor_plan.md @@ -0,0 +1,403 @@ +# Index-Only Vertex Descriptor — Implementation Plan + +This plan introduces canonical support for **index-only vertex descriptors** — +vertex descriptors parameterized on an iota iterator rather than a physical +container iterator. This eliminates the current workaround where `compressed_graph` +borrows a `row_index_vector::iterator` solely to satisfy the `vertex_descriptor` +template, even though it never uses the iterator-based access methods. + +Reference: discussion on `vertex_descriptor` design (April 2026). + +**Branch:** `vert_patterns` + +**Invariant:** After every sub-phase, `ctest` passes all existing tests. No +sub-phase may break backward compatibility. + +--- + +## Conventions + +| Symbol | Meaning | +|--------|---------| +| **File** | Path relative to repo root | +| **Read** | Files the agent must read for context before editing | +| **Create** | New files to create | +| **Modify** | Existing files to edit | +| **Verify** | Commands to run and expected outcomes | +| **Commit** | Git commit message (conventional-commit style) | + +--- + +## Background + +### Problem + +`vertex_descriptor` uses `VertexIter` for two purposes: + +1. **Selecting the storage strategy** — random-access → `size_t` index; + forward → stored iterator. +2. **Binding to a physical container** — `inner_value(container)` and + `underlying_value(container)` index into a container using the stored value. + +For graphs without a physical vertex container (e.g., `compressed_graph`, implicit +graphs, generated graphs), purpose #2 is invalid. Currently `compressed_graph` +works around this by using `row_index_vector::iterator` as the template argument, +which gives it the `size_t` storage path. But: + +- It exposes `inner_value()` and `underlying_value()` that are semantically + meaningless (they'd index into the CSR row_index array, not vertex data). +- The `vertex_descriptor_view` deduction guides require a container, so + `compressed_graph` must manually specify the iterator type. +- Any future implicit/generated graph would need a similar workaround. + +### Solution + +1. Define `index_iterator` as the canonical iterator type for index-only vertices. +2. Provide convenience aliases `index_vertex_descriptor` and + `index_vertex_descriptor_view`. +3. Constrain `inner_value()` and `underlying_value()` so they are unavailable + on index-only descriptors (compile error instead of silent misuse). +4. Migrate `compressed_graph` to use the new aliases. +5. Add tests and update documentation. + +--- + +## Phase 0 — Verify Baseline + +### 0.1 Run Full Test Suite + +| Item | Detail | +|------|--------| +| **Action** | Confirm the starting baseline is green. | +| **Verify** | `cd build/linux-gcc-debug && cmake --build . 2>&1 \| tail -5` — build succeeds | +| **Verify** | `cd build/linux-gcc-debug && ctest --output-on-failure` — all tests pass | + +--- + +## Phase 1 — Define Index Iterator and Aliases + +### 1.1 Add `index_iterator` and Type Aliases + +| Item | Detail | +|------|--------| +| **Read** | `include/graph/adj_list/vertex_descriptor.hpp`, `include/graph/adj_list/vertex_descriptor_view.hpp`, `include/graph/adj_list/descriptor.hpp` | +| **Modify** | `include/graph/adj_list/vertex_descriptor.hpp` | +| **Changes** | After the `vertex_descriptor` class definition (before the closing `} // namespace graph::adj_list`), add: | + +```cpp +/// Canonical iterator type for index-only vertices (no physical container). +/// Satisfies random_access_iterator, so vertex_descriptor stores size_t. +using index_iterator = std::ranges::iterator_t>; + +/// Vertex descriptor for index-only graphs (no physical vertex container). +using index_vertex_descriptor = vertex_descriptor; +``` + +| Item | Detail | +|------|--------| +| **Modify** | `include/graph/adj_list/vertex_descriptor_view.hpp` | +| **Changes** | After the `vertex_descriptor_view` class definition (before the closing `} // namespace graph::adj_list`), add: | + +```cpp +/// Vertex descriptor view for index-only graphs (no physical vertex container). +using index_vertex_descriptor_view = vertex_descriptor_view; +``` + +| **Verify** | Build succeeds; all existing tests pass | + +### 1.2 Verify `index_iterator` Satisfies Existing Concepts + +| Item | Detail | +|------|--------| +| **Modify** | `tests/adj_list/descriptors/test_vertex_descriptor.cpp` | +| **Changes** | Add a new test section that verifies the iota iterator works with existing concept machinery: | + +```cpp +TEST_CASE("index_vertex_descriptor type properties", "[vertex_descriptor][index]") { + using graph::adj_list::index_iterator; + using graph::adj_list::index_vertex_descriptor; + using graph::adj_list::index_vertex_descriptor_view; + + // index_iterator must satisfy the vertex_iterator concept + static_assert(graph::adj_list::vertex_iterator); + static_assert(graph::adj_list::direct_vertex_type); + static_assert(std::random_access_iterator); + + // Storage type must be size_t + static_assert(std::same_as); + + // vertex_id() returns size_t by value + index_vertex_descriptor vd{42uz}; + CHECK(vd.vertex_id() == 42); + CHECK(vd.value() == 42); + + // Increment + auto vd2 = vd; + ++vd2; + CHECK(vd2.vertex_id() == 43); + + // Comparison + CHECK(vd < vd2); + CHECK(vd != vd2); +} + +TEST_CASE("index_vertex_descriptor_view iteration", "[vertex_descriptor_view][index]") { + using graph::adj_list::index_vertex_descriptor_view; + + index_vertex_descriptor_view view(std::size_t{0}, std::size_t{5}); + CHECK(view.size() == 5); + + std::vector ids; + for (auto desc : view) { + ids.push_back(desc.vertex_id()); + } + CHECK(ids == std::vector{0, 1, 2, 3, 4}); +} +``` + +| **Verify** | Build succeeds; new tests pass; all existing tests pass | +| **Commit** | `feat: add index_iterator alias and index_vertex_descriptor types` | + +--- + +## Phase 2 — Constrain Container-Access Methods + +### 2.1 Add `index_only_vertex` Detection Trait + +| Item | Detail | +|------|--------| +| **Read** | `include/graph/adj_list/descriptor.hpp` (concept definitions), `include/graph/adj_list/vertex_descriptor.hpp` | +| **Modify** | `include/graph/adj_list/descriptor.hpp` | +| **Changes** | After the `vertex_id_type_t` alias (end of Vertex ID Type Extraction section), add: | + +```cpp +// ============================================================================= +// Index-Only Vertex Detection +// ============================================================================= + +/** + * @brief Concept for index-only vertex iterators (no physical container backing) + * + * An index-only vertex iterator is a random-access iterator whose value_type + * is an integral type (e.g., iota_view iterator). For these iterators, + * inner_value() and underlying_value() are not meaningful because there is + * no physical container to index into. + */ +template +concept index_only_vertex = std::random_access_iterator && + std::integral::value_type>; + +/** + * @brief Concept for container-backed vertex iterators + * + * The inverse of index_only_vertex. These iterators reference elements in + * a physical container, so inner_value() and underlying_value() are valid. + */ +template +concept container_backed_vertex = vertex_iterator && !index_only_vertex; +``` + +| **Verify** | Build succeeds; all existing tests pass | + +### 2.2 Add `requires` Clauses to `inner_value()` and `underlying_value()` + +| Item | Detail | +|------|--------| +| **Read** | `include/graph/adj_list/vertex_descriptor.hpp` | +| **Modify** | `include/graph/adj_list/vertex_descriptor.hpp` | +| **Changes** | Add a `requires container_backed_vertex` clause to each of the four methods: `underlying_value(Container&)`, `underlying_value(const Container&)`, `inner_value(Container&)`, `inner_value(const Container&)`. For example: | + +```cpp + template + [[nodiscard]] constexpr decltype(auto) underlying_value(Container& container) const noexcept + requires container_backed_vertex + { + // ... existing body unchanged ... + } +``` + +Apply identically to all four overloads. + +| **Verify** | Build succeeds; all existing tests pass | + +### 2.3 Add Compile-Time Tests for Constraint + +| Item | Detail | +|------|--------| +| **Modify** | `tests/adj_list/descriptors/test_vertex_descriptor.cpp` | +| **Changes** | Add tests verifying that `inner_value` and `underlying_value` are not callable on `index_vertex_descriptor`: | + +```cpp +TEST_CASE("index_vertex_descriptor does not expose container methods", "[vertex_descriptor][index]") { + using graph::adj_list::index_vertex_descriptor; + + // inner_value() and underlying_value() should NOT be available + static_assert(!requires(index_vertex_descriptor vd, std::vector& c) { + vd.inner_value(c); + }); + static_assert(!requires(index_vertex_descriptor vd, std::vector& c) { + vd.underlying_value(c); + }); + + // But vertex_id() and value() should still work + static_assert(requires(index_vertex_descriptor vd) { + { vd.vertex_id() } -> std::same_as; + { vd.value() } -> std::same_as; + }); +} +``` + +| **Verify** | Build succeeds; new tests pass; all existing tests pass | +| **Commit** | `feat: constrain inner_value/underlying_value to container-backed descriptors` | + +--- + +## Phase 3 — Migrate `compressed_graph` + +### 3.1 Switch `compressed_graph` to `index_iterator` + +| Item | Detail | +|------|--------| +| **Read** | `include/graph/container/compressed_graph.hpp` (lines 1070–1220, the `vertices()`, `find_vertex()`, and `vertex_id()` friend functions) | +| **Modify** | `include/graph/container/compressed_graph.hpp` | +| **Changes** | | + +1. Add `using adj_list::index_iterator;` to the existing `using` declarations near + line 36–40. + +2. In the `vertices(G&& g)` friend function (~line 1085), replace: + ```cpp + using vertex_iter_type = + std::conditional_t>, + typename row_index_vector::const_iterator, + typename row_index_vector::iterator>; + ``` + with: + ```cpp + using vertex_iter_type = index_iterator; + ``` + The const/non-const distinction is irrelevant for index-only descriptors + (they only store `size_t`). + +3. Apply the same change to `vertices(G&& g, const PId& pid)` (~line 1115) + and `find_vertex(G&& g, const VId2& uid)` (~line 1183). + +4. Keep the `vertex_id(const G& g, const VertexDesc& u)` friend function + unchanged — it only calls `u.vertex_id()` which remains valid. + +| **Verify** | Build succeeds; all existing `compressed_graph` tests pass; all other tests pass | +| **Commit** | `refactor: migrate compressed_graph to index_iterator` | + +--- + +## Phase 4 — Export from `graph.hpp` + +### 4.1 Add Aliases to Public Header + +| Item | Detail | +|------|--------| +| **Read** | `include/graph/graph.hpp` — find the existing `using adj_list::...` block | +| **Modify** | `include/graph/graph.hpp` | +| **Changes** | In the block of `using adj_list::...` declarations, add: | + +```cpp +using adj_list::index_iterator; +using adj_list::index_vertex_descriptor; +using adj_list::index_vertex_descriptor_view; +using adj_list::index_only_vertex; +using adj_list::container_backed_vertex; +``` + +| **Verify** | Build succeeds; all tests pass | +| **Commit** | `feat: export index vertex types from graph.hpp` | + +--- + +## Phase 5 — Update Documentation + +### 5.1 Update `vertex-patterns.md` + +| Item | Detail | +|------|--------| +| **Read** | `docs/reference/vertex-patterns.md` | +| **Modify** | `docs/reference/vertex-patterns.md` | +| **Changes** | | + +1. In the **Foundation Concepts** section, add `index_only_vertex` and + `container_backed_vertex` to the table. + +2. In the **Vertex Storage Patterns → Storage Concepts** table, add a note + that `direct_vertex_type` includes both container-backed iterators + (e.g., `vector::iterator`) and index-only iterators (`index_iterator`). + +3. In the **`vertex_descriptor` Class Reference → Member Functions** + table, add a note that `inner_value()` and `underlying_value()` require + `container_backed_vertex`. + +4. Add a new subsection **Index-Only Aliases** after the `vertex_descriptor_view` + class reference: + + ```markdown + ## Index-Only Aliases + + For graphs without a physical vertex container (e.g., `compressed_graph`, + implicit graphs), the library provides canonical aliases: + + | Alias | Definition | + |-------|------------| + | `index_iterator` | `std::ranges::iterator_t>` | + | `index_vertex_descriptor` | `vertex_descriptor` | + | `index_vertex_descriptor_view` | `vertex_descriptor_view` | + + These descriptors store only a `size_t` index. `vertex_id()` and `value()` + work normally. `inner_value()` and `underlying_value()` are not available + (constrained out by `container_backed_vertex`). + ``` + +### 5.2 Update `archive/descriptor.md` + +| Item | Detail | +|------|--------| +| **Read** | `docs/archive/descriptor.md` | +| **Modify** | `docs/archive/descriptor.md` | +| **Changes** | In the vertex descriptor spec section, add a third iterator category: | + + - **Index-only iterator** (e.g., `iota_view::iterator`): Used for + graphs with no physical vertex container. `inner_value()` and + `underlying_value()` are not available. + +| **Verify** | Review documentation renders correctly (no broken tables or links) | +| **Commit** | `docs: document index-only vertex descriptor aliases and constraints` | + +--- + +## Phase 6 — Final Verification + +### 6.1 Full Test Suite + +| Item | Detail | +|------|--------| +| **Action** | Run the full test suite across available build configurations. | +| **Verify** | `cd build/linux-gcc-debug && cmake --build . && ctest --output-on-failure` — all pass | +| **Verify** | `cd build/linux-clang-debug && cmake --build . && ctest --output-on-failure` — all pass | + +### 6.2 Verify No Regressions in `compressed_graph` Behavior + +| Item | Detail | +|------|--------| +| **Action** | Run compressed_graph tests specifically, confirming vertex iteration, find_vertex, vertex_id, edges, and partition queries all work identically. | +| **Verify** | `cd build/linux-gcc-debug && ctest -R compressed --output-on-failure` — all pass | + +--- + +## Summary + +| Phase | Description | Files Modified | New Files | +|-------|-------------|----------------|-----------| +| 0 | Verify baseline | — | — | +| 1 | Define `index_iterator`, aliases, basic tests | `vertex_descriptor.hpp`, `vertex_descriptor_view.hpp`, `test_vertex_descriptor.cpp` | — | +| 2 | Constrain `inner_value`/`underlying_value`, add concept | `descriptor.hpp`, `vertex_descriptor.hpp`, `test_vertex_descriptor.cpp` | — | +| 3 | Migrate `compressed_graph` | `compressed_graph.hpp` | — | +| 4 | Export from `graph.hpp` | `graph.hpp` | — | +| 5 | Update documentation | `vertex-patterns.md`, `archive/descriptor.md` | — | +| 6 | Final verification | — | — | diff --git a/docs/archive/descriptor.md b/docs/archive/descriptor.md index 589faea..097875a 100644 --- a/docs/archive/descriptor.md +++ b/docs/archive/descriptor.md @@ -110,25 +110,25 @@ Edges in the graph can be stored in various ways depending on graph structure: - MUST be a template with a single parameter for the underlying container's iterator type - Iterator category constraints: - **Random Access Iterator** (e.g., vector, deque): Used for index-based vertex storage - - **Bidirectional Iterator** (e.g., map, unordered_map): Used for key-value based vertex storage - - When bidirectional: the iterator's `value_type` MUST satisfy a pair-like concept where: + - **Forward Iterator** (e.g., map, unordered_map, unordered_set): Used for key-value based vertex storage + - When forward (non-random-access): the iterator's `value_type` MUST satisfy a pair-like concept where: - The type MUST have at least 2 members (accessible via tuple protocol or pair interface) - The first element serves as the vertex ID (key) - This can be checked using `std::tuple_size::value >= 2` or by requiring `.first` and `.second` members - MUST have a single member variable that: - MUST be `size_t index` when the iterator is a random access iterator - - MUST be the iterator type itself when the iterator is a bidirectional iterator (non-random access) + - MUST be the iterator type itself when the iterator is a forward iterator (non-random access) - MUST provide a `vertex_id()` member function that returns the vertex's unique identifier: - When the vertex iterator is random access: MUST return the `size_t` member value (the index) - - When the vertex iterator is bidirectional: MUST return the key (first element) from the pair-like `value_type` + - When the vertex iterator is forward (non-random-access): MUST return the key (first element) from the pair-like `value_type` - Return type SHOULD be deduced appropriately based on iterator category - MUST provide a public `value()` member function that returns the underlying storage handle: - When the vertex iterator is random access: `value()` MUST return the stored `size_t` index - - When the vertex iterator is bidirectional: `value()` MUST return the stored iterator + - When the vertex iterator is forward (non-random-access): `value()` MUST return the stored iterator - Return type SHOULD be the exact type of the underlying member (copy by value) - MUST provide pre-increment and post-increment operators (`operator++` / `operator++(int)`) whose behavior mirrors the underlying storage: - For random access iterators: increment operations MUST advance the `size_t` index by one - - For bidirectional iterators: increment operations MUST advance the stored iterator + - For forward iterators: increment operations MUST advance the stored iterator - MUST be efficiently passable by value - SHOULD support conversion to/from underlying index type (for random access case) - MUST integrate with std::hash for unordered containers diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md index 737d36c..5832a61 100644 --- a/docs/reference/concepts.md +++ b/docs/reference/concepts.md @@ -285,7 +285,7 @@ for full documentation. | Concept | Purpose | |---------|---------| | `direct_vertex_type` | Random-access, index-based vertex | -| `keyed_vertex_type` | Bidirectional, key-based vertex | +| `keyed_vertex_type` | Forward, key-based vertex | | `vertex_iterator` | Either direct or keyed | | `random_access_vertex_pattern` | Inner value: return whole element | | `pair_value_vertex_pattern` | Inner value: return `.second` | diff --git a/docs/reference/vertex-patterns.md b/docs/reference/vertex-patterns.md index 3496dc5..f20594c 100644 --- a/docs/reference/vertex-patterns.md +++ b/docs/reference/vertex-patterns.md @@ -8,7 +8,7 @@ > This page documents two complementary concept families for vertex types: **inner value patterns** (how vertex data is accessed) and **storage patterns** (how vertex IDs are stored/extracted). Both families live -in `graph::adj_list::detail` and are re-exported via ``. +in `graph::adj_list` and are re-exported via ``. @@ -17,6 +17,25 @@ in `graph::adj_list::detail` and are re-exported via ``. --- +## Foundation Concepts + +These building-block concepts determine whether a type is "pair-like" and +are used internally by both the inner value and storage pattern concepts. + +| Concept | Definition | +|---------|------------| +| `pair_like` | Has `std::tuple_size::value >= 2` and supports `std::get<0>`, `std::get<1>` (tuple protocol) | +| `has_first_second` | Has `.first` and `.second` members | +| `pair_like_value` | `pair_like \|\| has_first_second` — disjunction of both | + +```cpp +// Used to constrain keyed_vertex_type, pair_value_vertex_pattern, etc. +template +concept pair_like_value = pair_like || has_first_second; +``` + +--- + ## Inner Value Patterns The `inner_value()` method on a vertex descriptor returns the vertex data @@ -33,8 +52,8 @@ container type. | Concept | Iterator Requirements | `inner_value()` Returns | Typical Containers | |---------|----------------------|------------------------|--------------------| | `random_access_vertex_pattern` | Random-access iterator | `container[index]` — the whole element | `std::vector`, `std::deque` | -| `pair_value_vertex_pattern` | Bidirectional, pair-like value type | `.second` — the mapped value | `std::map`, `std::unordered_map` | -| `whole_value_vertex_pattern` | Bidirectional, non-pair value type | `*iter` — the whole dereferenced value | Custom bidirectional containers | +| `pair_value_vertex_pattern` | Forward, pair-like value type | `.second` — the mapped value | `std::map`, `std::unordered_map` | +| `whole_value_vertex_pattern` | Forward, non-pair value type | `*iter` — the whole dereferenced value | Custom forward containers | ```cpp // Disjunction of all three @@ -104,7 +123,7 @@ stored and extracted: | Concept | Iterator Requirements | Vertex ID Type | Access Pattern | |---------|----------------------|---------------|----------------| | `direct_vertex_type` | Random-access | `std::size_t` (the index) | `container[index]` | -| `keyed_vertex_type` | Bidirectional, pair-like value | Key type (`.first`) | `(*iter).first` for ID, `.second` for data | +| `keyed_vertex_type` | Forward, pair-like value | Key type (`.first`) | `(*iter).first` for ID, `.second` for data | ```cpp // Disjunction @@ -123,8 +142,10 @@ matches exactly one. | `vertex_storage_pattern` | Struct with `::is_direct`, `::is_keyed` booleans | | `vertex_storage_pattern_v` | Variable template shortcut | | `vertex_pattern` (enum) | `direct`, `keyed` | +| `vertex_pattern_type` | Struct with `::value` of type `vertex_pattern` | | `vertex_pattern_type_v` | Variable template returning the enum value | -| `vertex_id_type_t` | Extracts the vertex ID type: `size_t` for direct, key type for keyed | +| `vertex_id_type` | Struct with `::type` alias; specializations for direct (`.first`/`.second`) and keyed (tuple protocol) | +| `vertex_id_type_t` | Alias for `vertex_id_type::type`: `size_t` for direct, key type for keyed | ### Examples @@ -146,11 +167,11 @@ static_assert(std::same_as, std::string>); The `vertex_id()` function in `vertex_descriptor` dispatches at compile time: ```cpp -constexpr auto vertex_id() const noexcept { +constexpr decltype(auto) vertex_id() const noexcept { if constexpr (std::random_access_iterator) { return storage_; // direct: return index (size_t) } else { - return std::get<0>(*storage_); // keyed: extract key + return std::get<0>(*storage_); // keyed: return const& to key } } ``` @@ -172,6 +193,98 @@ Both families work together to give `vertex_descriptor` complete type safety: --- +## `vertex_descriptor` Class Reference + +Defined in ``. +A lightweight, type-safe handle to a vertex stored in a container. +Constrained by `vertex_iterator`. + +### Type Aliases + +| Alias | Definition | +|-------|------------| +| `iterator_type` | `VertexIter` | +| `value_type` | `typename std::iterator_traits::value_type` | +| `storage_type` | `std::size_t` for random-access iterators, `VertexIter` otherwise | + +### Constructors + +| Signature | Notes | +|-----------|-------| +| `constexpr vertex_descriptor() noexcept` | Default; requires `std::default_initializable` | +| `constexpr explicit vertex_descriptor(storage_type val) noexcept` | Construct from index or iterator | + +### Member Functions + +| Function | Returns | Description | +|----------|---------|-------------| +| `value()` | `storage_type` | The raw storage: index (`size_t`) or iterator | +| `vertex_id()` | `decltype(auto)` | Vertex ID — index by value for direct, `const&` to key for keyed | +| `underlying_value(Container&)` | `decltype(auto)` | Full container element (`container[i]` or `*iter`); const overload provided | +| `inner_value(Container&)` | `decltype(auto)` | Vertex data excluding the key (`.second` for maps, whole value for vectors); const overload provided | +| `operator++()` / `operator++(int)` | `vertex_descriptor&` / `vertex_descriptor` | Pre/post-increment | +| `operator<=>` / `operator==` | | Defaulted three-way and equality comparison | + +### `std::hash` Specialization + +A `std::hash>` specialization is provided, +hashing the index for direct storage or the `vertex_id()` for keyed storage. +This enables use in `std::unordered_set` and `std::unordered_map`. + +--- + +## `vertex_descriptor_view` Class Reference + +Defined in ``. +A forward-only view over vertex storage that synthesizes `vertex_descriptor` +objects on-the-fly. Inherits from `std::ranges::view_interface`. + +### Type Aliases + +| Alias | Definition | +|-------|------------| +| `vertex_desc` | `vertex_descriptor` | +| `storage_type` | `typename vertex_desc::storage_type` | + +### Constructors + +| Signature | Notes | +|-----------|-------| +| `constexpr vertex_descriptor_view() noexcept` | Default | +| `constexpr vertex_descriptor_view(storage_type begin, storage_type end) noexcept` | From index/iterator range; requires random-access | +| `constexpr explicit vertex_descriptor_view(Container&) noexcept` | From mutable container; requires `sized_range` or random-access | +| `constexpr explicit vertex_descriptor_view(const Container&) noexcept` | From const container | + +### Member Functions + +| Function | Description | +|----------|-------------| +| `begin()` / `end()` | Forward iterators yielding `vertex_descriptor` by value | +| `cbegin()` / `cend()` | Const equivalents | +| `size()` | O(1) — cached at construction | + +### Inner `iterator` Class + +| Property | Value | +|----------|-------| +| `iterator_category` | `std::forward_iterator_tag` | +| `value_type` | `vertex_descriptor` | +| Dereference | Returns descriptor **by value** (synthesized) | + +### Deduction Guides + +```cpp +vertex_descriptor_view(Container&) -> vertex_descriptor_view; +vertex_descriptor_view(const Container&) -> vertex_descriptor_view; +``` + +### Range Traits + +`std::ranges::enable_borrowed_range` is specialized to `true` — the view +does not own the underlying data. + +--- + ## Compile-Time Dispatch Pattern ```cpp diff --git a/include/graph/adj_list/descriptor.hpp b/include/graph/adj_list/descriptor.hpp index 2d1bac6..7893483 100644 --- a/include/graph/adj_list/descriptor.hpp +++ b/include/graph/adj_list/descriptor.hpp @@ -20,7 +20,7 @@ struct in_edge_tag {}; /** * @brief Concept to check if a type is pair-like (has at least 2 members accessible via tuple protocol) * - * This is used to constrain bidirectional iterator value_types for vertex storage. + * This is used to constrain forward iterator value_types for vertex storage. */ template concept pair_like = requires { @@ -89,7 +89,7 @@ concept keyed_vertex_type = std::forward_iterator && !std::random_access_i * * A vertex iterator must be either: * - Random access (direct/indexed storage) - * - Bidirectional with pair-like value_type (keyed storage) + * - Forward with pair-like value_type (keyed storage) */ template concept vertex_iterator = direct_vertex_type || keyed_vertex_type; @@ -111,25 +111,25 @@ concept random_access_vertex_pattern = std::random_access_iterator; /** * @brief Concept for pair-value vertex pattern (map-like) * - * Used with bidirectional iterators where the value_type is pair-like. + * Used with forward iterators where the value_type is pair-like. * inner_value returns the .second part (the data, excluding the key). * Pattern: *iterator -> pair{key, data}, inner_value returns data& - * Example: std::map where inner_value returns VertexData& (.second) + * Example: std::map or std::unordered_map where inner_value returns VertexData& (.second) */ template -concept pair_value_vertex_pattern = std::bidirectional_iterator && !std::random_access_iterator && +concept pair_value_vertex_pattern = std::forward_iterator && !std::random_access_iterator && pair_like_value::value_type>; /** - * @brief Concept for whole-value vertex pattern (custom bidirectional) + * @brief Concept for whole-value vertex pattern (custom forward) * - * Used with bidirectional iterators where the value_type is NOT pair-like. + * Used with forward iterators where the value_type is NOT pair-like. * inner_value returns the entire element (same as underlying_value). * Pattern: *iterator -> value, inner_value returns value& - * Example: Custom bidirectional container with non-pair value_type + * Example: Custom forward container with non-pair value_type */ template -concept whole_value_vertex_pattern = std::bidirectional_iterator && !std::random_access_iterator && +concept whole_value_vertex_pattern = std::forward_iterator && !std::random_access_iterator && !pair_like_value::value_type>; /** @@ -274,6 +274,36 @@ template requires vertex_iterator using vertex_id_type_t = typename vertex_id_type::type; +// ============================================================================= +// Index-Only Vertex Detection +// ============================================================================= + +/** + * @brief Concept for index-only vertex iterators (no physical container backing) + * + * An index-only vertex iterator is a random-access iterator whose value_type + * is integral AND whose reference type is not an actual reference (i.e., it + * returns by value, like iota_view iterators). This distinguishes generated + * index sequences from container iterators over integral types (e.g., + * vector::iterator has reference = int&, so it is NOT index-only). + * + * For these iterators, inner_value() and underlying_value() are not meaningful + * because there is no physical container to index into. + */ +template +concept index_only_vertex = std::random_access_iterator && + std::integral> && + !std::is_reference_v>; + +/** + * @brief Concept for container-backed vertex iterators + * + * The inverse of index_only_vertex. These iterators reference elements in + * a physical container, so inner_value() and underlying_value() are valid. + */ +template +concept container_backed_vertex = vertex_iterator && !index_only_vertex; + // ============================================================================= // Edge Value Type Concepts for target_id() Extraction // ============================================================================= diff --git a/include/graph/adj_list/detail/graph_cpo.hpp b/include/graph/adj_list/detail/graph_cpo.hpp index 2bef7ec..df74baa 100644 --- a/include/graph/adj_list/detail/graph_cpo.hpp +++ b/include/graph/adj_list/detail/graph_cpo.hpp @@ -343,7 +343,7 @@ namespace _cpo_impls { * The descriptor default works for standard containers: * - Random-access containers (vector, deque): returns the index * - Associative containers (map): returns the key - * - Bidirectional containers: returns the iterator position + * - Forward containers: returns the iterator position * * @tparam G Graph type * @tparam U Vertex descriptor type (constrained to be a vertex_descriptor_type) @@ -391,7 +391,7 @@ inline namespace _cpo_instances { * The type of the unique identifier returned by vertex_id(g, u), with references stripped. * - For random-access containers (vector, deque): size_t (index) * - For associative containers (map): key type (e.g. std::string) - * - For bidirectional containers: iterator-based ID + * - For forward containers: iterator-based ID */ template using vertex_id_t = std::remove_cvref_t< @@ -3005,7 +3005,7 @@ namespace _cpo_impls { * - Uses u.inner_value(g) which returns the actual vertex data * - For random-access containers (vector): returns container[index] * - For associative containers (map): returns the .second value - * - For bidirectional containers: returns the dereferenced value + * - For forward containers: returns the dereferenced value * * This provides access to user-defined vertex properties/data stored in the graph. * diff --git a/include/graph/adj_list/vertex_descriptor.hpp b/include/graph/adj_list/vertex_descriptor.hpp index b97568d..f24be14 100644 --- a/include/graph/adj_list/vertex_descriptor.hpp +++ b/include/graph/adj_list/vertex_descriptor.hpp @@ -26,7 +26,7 @@ class vertex_descriptor { using iterator_type = VertexIter; using value_type = typename std::iterator_traits::value_type; - // Conditional storage type: size_t for random access, iterator for bidirectional + // Conditional storage type: size_t for random access, iterator for forward using storage_type = std::conditional_t, std::size_t, VertexIter>; // Default constructor @@ -39,13 +39,13 @@ class vertex_descriptor { /** * @brief Get the underlying storage value (index or iterator) - * @return The stored index (for random access) or iterator (for bidirectional) + * @return The stored index (for random access) or iterator (for forward) */ [[nodiscard]] constexpr storage_type value() const noexcept { return storage_; } /** * @brief Get the vertex ID - * @return For random access: the index (by value). For bidirectional: const& to the key. + * @return For random access: the index (by value). For forward: const& to the key. * * Returns decltype(auto) so that map-based graphs return a const reference * to the stable key stored in the map node, avoiding copies of non-trivial @@ -53,7 +53,7 @@ class vertex_descriptor { * * For random-access iterators, `return storage_;` (non-parenthesized) ensures * decltype deduces the value type (size_t) — safe even on temporaries. - * For bidirectional iterators, std::get<0> returns a reference to the map key, + * For forward iterators, std::get<0> returns a reference to the map key, * which is stable as long as the container is alive. */ [[nodiscard]] constexpr decltype(auto) vertex_id() const noexcept { @@ -71,10 +71,12 @@ class vertex_descriptor { * @return Reference to the vertex data from the container * * For random access iterators, accesses container[index]. - * For bidirectional iterators, dereferences the stored iterator. + * For forward iterators, dereferences the stored iterator. */ template - [[nodiscard]] constexpr decltype(auto) underlying_value(Container& container) const noexcept { + [[nodiscard]] constexpr decltype(auto) underlying_value(Container& container) const noexcept + requires (container_backed_vertex && !std::is_void_v) + { if constexpr (std::random_access_iterator) { return (container[storage_]); } else { @@ -83,7 +85,9 @@ class vertex_descriptor { } template - [[nodiscard]] constexpr decltype(auto) underlying_value(const Container& container) const noexcept { + [[nodiscard]] constexpr decltype(auto) underlying_value(const Container& container) const noexcept + requires (container_backed_vertex && !std::is_void_v) + { if constexpr (std::random_access_iterator) { return (container[storage_]); } else { @@ -97,10 +101,12 @@ class vertex_descriptor { * @return Reference to the vertex data (for maps: the .second value; for vectors: the whole value) * * For random access containers (vector), returns the entire value. - * For bidirectional containers (map), returns the .second part (the actual data, not the key). + * For forward containers (map, unordered_map), returns the .second part (the actual data, not the key). */ template - [[nodiscard]] constexpr decltype(auto) inner_value(Container& container) const noexcept { + [[nodiscard]] constexpr decltype(auto) inner_value(Container& container) const noexcept + requires (container_backed_vertex && !std::is_void_v) + { using vt = typename std::iterator_traits::value_type; if constexpr (std::random_access_iterator) { @@ -108,7 +114,7 @@ class vertex_descriptor { using diff_t = typename std::iterator_traits::difference_type; return (*(std::begin(container) + static_cast(storage_))); } else { - // For bidirectional, check if it's a pair-like type + // For forward, check if it's a pair-like type if constexpr (pair_like_value) { // For map, bind the pair first, then return .second auto& element = *storage_; @@ -126,7 +132,9 @@ class vertex_descriptor { * @return Const reference to the vertex data */ template - [[nodiscard]] constexpr decltype(auto) inner_value(const Container& container) const noexcept { + [[nodiscard]] constexpr decltype(auto) inner_value(const Container& container) const noexcept + requires (container_backed_vertex && !std::is_void_v) + { using vt = typename std::iterator_traits::value_type; if constexpr (std::random_access_iterator) { @@ -163,6 +171,13 @@ class vertex_descriptor { storage_type storage_; }; +/// Canonical iterator type for index-only vertices (no physical container). +/// Satisfies random_access_iterator, so vertex_descriptor stores size_t. +using index_iterator = std::ranges::iterator_t>; + +/// Vertex descriptor for index-only graphs (no physical vertex container). +using index_vertex_descriptor = vertex_descriptor; + } // namespace graph::adj_list // Hash specialization for std::unordered containers diff --git a/include/graph/adj_list/vertex_descriptor_view.hpp b/include/graph/adj_list/vertex_descriptor_view.hpp index c41cb79..e52ce4c 100644 --- a/include/graph/adj_list/vertex_descriptor_view.hpp +++ b/include/graph/adj_list/vertex_descriptor_view.hpp @@ -153,6 +153,9 @@ vertex_descriptor_view(Container&) -> vertex_descriptor_view vertex_descriptor_view(const Container&) -> vertex_descriptor_view; +/// Vertex descriptor view for index-only graphs (no physical vertex container). +using index_vertex_descriptor_view = vertex_descriptor_view; + } // namespace graph::adj_list // Enable borrowed_range for vertex_descriptor_view since it doesn't own the data diff --git a/include/graph/container/compressed_graph.hpp b/include/graph/container/compressed_graph.hpp index a7fe21b..b9ed8bc 100644 --- a/include/graph/container/compressed_graph.hpp +++ b/include/graph/container/compressed_graph.hpp @@ -39,6 +39,7 @@ using adj_list::vertex_descriptor_view; using adj_list::edge_descriptor_view; using adj_list::vertex_descriptor_type; using adj_list::edge_descriptor_type; +using adj_list::index_iterator; /** * @ingroup graph_containers @@ -1083,11 +1084,8 @@ class compressed_graph_base template requires std::derived_from, compressed_graph_base> [[nodiscard]] friend constexpr auto vertices(G&& g) noexcept { - // Since row_index_ is a vector of csr_row structs (random access), - // we use the index-based constructor with storage_type (size_t) for vertex IDs - using vertex_iter_type = - std::conditional_t>, typename row_index_vector::const_iterator, - typename row_index_vector::iterator>; + // Index-only vertex descriptor: no physical vertex container, just size_t IDs + using vertex_iter_type = index_iterator; if (g.empty()) { return vertex_descriptor_view(static_cast(0), static_cast(0)); @@ -1115,9 +1113,7 @@ class compressed_graph_base template requires std::derived_from, compressed_graph_base> [[nodiscard]] friend constexpr auto vertices(G&& g, const PId& pid) noexcept { - using vertex_iter_type = - std::conditional_t>, typename row_index_vector::const_iterator, - typename row_index_vector::iterator>; + using vertex_iter_type = index_iterator; // Handle empty graph if (g.empty()) { @@ -1180,9 +1176,7 @@ class compressed_graph_base template requires std::derived_from, compressed_graph_base> [[nodiscard]] friend constexpr auto find_vertex([[maybe_unused]] G&& g, const VId2& uid) noexcept { - using vertex_iter_type = - std::conditional_t>, typename row_index_vector::const_iterator, - typename row_index_vector::iterator>; + using vertex_iter_type = index_iterator; using vertex_desc_view = vertex_descriptor_view; using vertex_desc_iterator = typename vertex_desc_view::iterator; @@ -1228,11 +1222,9 @@ class compressed_graph_base using edge_iter_type = std::conditional_t>, typename col_index_vector::const_iterator, typename col_index_vector::iterator>; - using vertex_iter_type = - std::conditional_t>, typename row_index_vector::const_iterator, - typename row_index_vector::iterator>; - using edge_desc_view = edge_descriptor_view; - using vertex_desc = vertex_descriptor; + using vertex_iter_type = index_iterator; + using edge_desc_view = edge_descriptor_view; + using vertex_desc = vertex_descriptor; // Get the vertex ID from the descriptor auto vid = static_cast(u.vertex_id()); diff --git a/include/graph/graph.hpp b/include/graph/graph.hpp index e16e4bb..9f62569 100644 --- a/include/graph/graph.hpp +++ b/include/graph/graph.hpp @@ -93,12 +93,19 @@ using adj_list::edge_descriptor; using adj_list::vertex_descriptor_view; using adj_list::edge_descriptor_view; +// Index-only vertex aliases +using adj_list::index_iterator; +using adj_list::index_vertex_descriptor; +using adj_list::index_vertex_descriptor_view; + // Descriptor concepts and traits using adj_list::descriptor_type; using adj_list::vertex_descriptor_type; using adj_list::edge_descriptor_type; using adj_list::vertex_iterator; using adj_list::edge_iterator; +using adj_list::index_only_vertex; +using adj_list::container_backed_vertex; using adj_list::is_descriptor_v; using adj_list::is_vertex_descriptor_v; using adj_list::is_edge_descriptor_v; diff --git a/tests/adj_list/descriptors/test_vertex_descriptor.cpp b/tests/adj_list/descriptors/test_vertex_descriptor.cpp index 4d38a13..9b38f76 100644 --- a/tests/adj_list/descriptors/test_vertex_descriptor.cpp +++ b/tests/adj_list/descriptors/test_vertex_descriptor.cpp @@ -640,3 +640,113 @@ TEST_CASE("vertex_descriptor_view - const vs non-const distinction", "[vertex_de static_assert(std::is_same_v::iterator>); static_assert(std::is_same_v::const_iterator>); } + +// ============================================================================= +// Index-Only Vertex Descriptor Tests +// ============================================================================= + +TEST_CASE("index_vertex_descriptor type properties", "[vertex_descriptor][index]") { + // index_iterator must satisfy the vertex_iterator concept + static_assert(vertex_iterator); + static_assert(direct_vertex_type); + static_assert(std::random_access_iterator); + + // Storage type must be size_t + static_assert(std::same_as); + + // vertex_id() returns size_t by value + index_vertex_descriptor vd{std::size_t{42}}; + CHECK(vd.vertex_id() == 42); + CHECK(vd.value() == 42); + + // Increment + auto vd2 = vd; + ++vd2; + CHECK(vd2.vertex_id() == 43); + + // Comparison + CHECK(vd < vd2); + CHECK(vd != vd2); +} + +TEST_CASE("index_vertex_descriptor default construction", "[vertex_descriptor][index]") { + index_vertex_descriptor vd{}; + CHECK(vd.vertex_id() == 0); + CHECK(vd.value() == 0); +} + +TEST_CASE("index_vertex_descriptor hashing", "[vertex_descriptor][index]") { + index_vertex_descriptor vd1{std::size_t{10}}; + index_vertex_descriptor vd2{std::size_t{10}}; + index_vertex_descriptor vd3{std::size_t{20}}; + + std::hash hasher; + CHECK(hasher(vd1) == hasher(vd2)); + // Different IDs should (very likely) have different hashes + CHECK(hasher(vd1) != hasher(vd3)); +} + +TEST_CASE("index_vertex_descriptor_view iteration", "[vertex_descriptor_view][index]") { + index_vertex_descriptor_view view(std::size_t{0}, std::size_t{5}); + CHECK(view.size() == 5); + + std::vector ids; + for (auto desc : view) { + ids.push_back(desc.vertex_id()); + } + CHECK(ids == std::vector{0, 1, 2, 3, 4}); +} + +TEST_CASE("index_vertex_descriptor_view empty range", "[vertex_descriptor_view][index]") { + index_vertex_descriptor_view view(std::size_t{0}, std::size_t{0}); + CHECK(view.size() == 0); + CHECK(view.begin() == view.end()); +} + +TEST_CASE("index_vertex_descriptor_view non-zero start", "[vertex_descriptor_view][index]") { + index_vertex_descriptor_view view(std::size_t{10}, std::size_t{13}); + CHECK(view.size() == 3); + + std::vector ids; + for (auto desc : view) { + ids.push_back(desc.vertex_id()); + } + CHECK(ids == std::vector{10, 11, 12}); +} + +TEST_CASE("index_vertex_descriptor concept checks", "[vertex_descriptor][index]") { + // index_iterator is correctly classified as index-only + static_assert(index_only_vertex); + static_assert(!container_backed_vertex); + + // vertex_id() and value() are always available + static_assert(requires(index_vertex_descriptor vd) { + { vd.vertex_id() } -> std::same_as; + { vd.value() } -> std::same_as; + }); + + // vector::iterator is integral but container-backed (returns by reference) + using VecIntIter = std::vector::iterator; + static_assert(!index_only_vertex); + static_assert(container_backed_vertex); +} + +TEST_CASE("container-backed descriptors still expose container methods", "[vertex_descriptor][index]") { + // Vector-backed descriptor should still have inner_value and underlying_value + using VecIter = std::vector::iterator; + static_assert(container_backed_vertex); + static_assert(!index_only_vertex); + static_assert(requires(vertex_descriptor vd, std::vector& c) { + vd.inner_value(c); + vd.underlying_value(c); + }); + + // Map-backed descriptor should still have inner_value and underlying_value + using MapIter = std::map::iterator; + static_assert(container_backed_vertex); + static_assert(!index_only_vertex); + static_assert(requires(vertex_descriptor vd, std::map& c) { + vd.inner_value(c); + vd.underlying_value(c); + }); +}